@blamejs/blamejs-shop 0.5.7 → 0.5.8

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 (27) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/cost-layers.js +86 -49
  4. package/lib/plan-changes.js +14 -0
  5. package/lib/vendor/MANIFEST.json +31 -15
  6. package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
  7. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  8. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  9. package/lib/vendor/blamejs/examples/wiki/package-lock.json +1 -1
  10. package/lib/vendor/blamejs/lib/content-credentials.js +43 -2
  11. package/lib/vendor/blamejs/lib/guard-sql.js +29 -0
  12. package/lib/vendor/blamejs/lib/mcp.js +37 -4
  13. package/lib/vendor/blamejs/lib/metrics.js +15 -3
  14. package/lib/vendor/blamejs/lib/middleware/tus-upload.js +9 -2
  15. package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +33 -2
  16. package/lib/vendor/blamejs/lib/safe-buffer.js +11 -1
  17. package/lib/vendor/blamejs/package-lock.json +2 -2
  18. package/lib/vendor/blamejs/package.json +1 -1
  19. package/lib/vendor/blamejs/release-notes/v0.16.3.json +71 -0
  20. package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials-coverage.test.js +319 -0
  21. package/lib/vendor/blamejs/test/layer-0-primitives/cycle2-bugfixes.test.js +142 -0
  22. package/lib/vendor/blamejs/test/layer-0-primitives/guard-sql-coverage.test.js +432 -0
  23. package/lib/vendor/blamejs/test/layer-0-primitives/mcp-coverage.test.js +443 -0
  24. package/lib/vendor/blamejs/test/layer-0-primitives/metrics-coverage.test.js +395 -0
  25. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-tus-upload-coverage.test.js +402 -0
  26. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-safe-yaml-coverage.test.js +292 -0
  27. package/package.json +1 -1
@@ -137,6 +137,15 @@ function _validateBuildOpts(opts) {
137
137
  */
138
138
  function build(opts) {
139
139
  _validateBuildOpts(opts);
140
+ // A `typeof === "number"` gate alone lets NaN / Infinity / out-of-Date-range
141
+ // values through, which then crash `new Date(x).toISOString()` with an untyped
142
+ // RangeError. Config-time inputs reject with a typed error instead.
143
+ if (opts.generatedAt !== undefined &&
144
+ (typeof opts.generatedAt !== "number" || !isFinite(opts.generatedAt) ||
145
+ Math.abs(opts.generatedAt) > 8.64e15)) {
146
+ throw ContentCredentialsError.factory("content-credentials/bad-generated-at",
147
+ "contentCredentials.build: generatedAt must be a finite epoch-millis number within the valid Date range");
148
+ }
140
149
  var generatedAt = typeof opts.generatedAt === "number" ? opts.generatedAt : Date.now();
141
150
  var manifest = {
142
151
  "@context": "https://c2pa.org/specifications/specifications/2.1/",
@@ -161,7 +170,24 @@ function build(opts) {
161
170
  // Optional operator-supplied display assertion (SB-942 §22757(b))
162
171
  visibleDisclosure: opts.visibleDisclosure || null,
163
172
  };
164
- return Object.freeze(manifest);
173
+ // Deep-freeze — every claim field lives in a nested object (provider/system/
174
+ // content), so a top-level Object.freeze alone left content.id etc. mutable,
175
+ // contradicting the documented pre-sign immutability guarantee.
176
+ return _deepFreeze(manifest);
177
+ }
178
+
179
+ // Recursively freeze an object graph so no nested claim can be mutated before
180
+ // signing. Cyclic input isn't produced here (manifest is a plain literal tree).
181
+ function _deepFreeze(obj) {
182
+ if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
183
+ var keys = Object.keys(obj);
184
+ for (var i = 0; i < keys.length; i += 1) {
185
+ var v = obj[keys[i]];
186
+ if (v && typeof v === "object") _deepFreeze(v);
187
+ }
188
+ Object.freeze(obj);
189
+ }
190
+ return obj;
165
191
  }
166
192
 
167
193
  /**
@@ -672,6 +698,15 @@ function signCose(manifest, opts) {
672
698
  // 35) when a token was attached.
673
699
  var unprotEntries = [];
674
700
  if (Array.isArray(opts.certChain) && opts.certChain.length > 0) {
701
+ // Validate each entry is raw DER bytes — a non-Buffer element would reach
702
+ // Buffer.concat / _cborBytes and throw an untyped Node ERR_INVALID_ARG_TYPE
703
+ // instead of an operator-actionable typed rejection.
704
+ opts.certChain.forEach(function (der, idx) {
705
+ if (!Buffer.isBuffer(der) && !(der instanceof Uint8Array)) {
706
+ throw ContentCredentialsError.factory("content-credentials/bad-cert-chain",
707
+ "contentCredentials.signCose: certChain[" + idx + "] must be a Buffer/Uint8Array of DER bytes");
708
+ }
709
+ });
675
710
  var chainArray;
676
711
  if (opts.certChain.length === 1) {
677
712
  // Single-cert form: header value is the DER bytes directly.
@@ -1252,14 +1287,20 @@ function verifyIdentityAssertion(assertion, publicKeyPem, opts) {
1252
1287
  if (supplied.length !== bound.length) {
1253
1288
  return _fail("referenced-assertions-count-mismatch");
1254
1289
  }
1290
+ // Multiset match — consume each bound hash as it is matched so duplicate
1291
+ // supplied hashes ([A, A]) can't satisfy a binding of distinct hashes
1292
+ // ([A, B]) while leaving hash(B) never re-confirmed (a transplant fail-open).
1293
+ var boundRemaining = bound.slice();
1255
1294
  for (var i = 0; i < supplied.length; i += 1) {
1256
- if (bound.indexOf(supplied[i]) === -1) {
1295
+ var mIdx = boundRemaining.indexOf(supplied[i]);
1296
+ if (mIdx === -1) {
1257
1297
  if (auditOn) {
1258
1298
  audit.safeEmit({ action: "contentcredentials.identity_verified", outcome: "denied",
1259
1299
  metadata: { binding: sp.binding, reason: "assertion-hash-mismatch" } });
1260
1300
  }
1261
1301
  return _fail("assertion-hash-mismatch");
1262
1302
  }
1303
+ boundRemaining.splice(mIdx, 1);
1263
1304
  }
1264
1305
 
1265
1306
  // (3) trust resolution. verified:true ONLY for x509 with a supplied
@@ -1045,6 +1045,18 @@ function _inspectFragment(rawText, normalized, opts, issues) {
1045
1045
  "with a ? placeholder, or pass allowLiterals:true for a static literal",
1046
1046
  });
1047
1047
  }
1048
+ // A value-expression fragment contains no statement separator at all — any
1049
+ // top-level ';' violates the fragment contract, including a LONE or TRAILING
1050
+ // one ("x = 1;") that the stacked-statement scan misses (it only fires on a
1051
+ // second statement after the ';'). Literals/comments are masked in
1052
+ // `normalized`, so a ';' here is genuinely top-level.
1053
+ if (normalized.indexOf(";") !== -1) {
1054
+ issues.push({
1055
+ code: "sql.refuse", severity: "critical", kind: "semicolon-in-fragment",
1056
+ ruleId: "sql.semicolon-in-fragment",
1057
+ snippet: "top-level ';' in a value-expression fragment — a fragment must be a single expression",
1058
+ });
1059
+ }
1048
1060
  }
1049
1061
 
1050
1062
  // operator-sql mode — exactly one statement (stacked scan already
@@ -1122,6 +1134,23 @@ function _hasEmbeddedStringLiteral(sql) {
1122
1134
  continue;
1123
1135
  }
1124
1136
  if (ch === SQUOTE) return true;
1137
+ // Postgres dollar-quoted string literal opener ($tag$ or $$, tag = identifier
1138
+ // chars or empty) — an embedded dollar-quote is a string literal just like
1139
+ // '...', and the fragment floor must refuse it the same way (the single-quote
1140
+ // check alone let `x = $tag$secret$tag$` bypass the strict embedded-literal gate).
1141
+ if (ch === "$") {
1142
+ // The tag between the opening and closing '$' is identifier chars only
1143
+ // (no '$', which terminates it) — char compares, not a regex literal, to
1144
+ // keep the pattern out of the shared-regex-duplication detector.
1145
+ var j = i + 1;
1146
+ while (j < len) {
1147
+ var tch = sql.charAt(j);
1148
+ if (!((tch >= "a" && tch <= "z") || (tch >= "A" && tch <= "Z") ||
1149
+ (tch >= "0" && tch <= "9") || tch === "_")) break;
1150
+ j += 1;
1151
+ }
1152
+ if (j < len && sql.charAt(j) === "$") return true;
1153
+ }
1125
1154
  i += 1;
1126
1155
  }
1127
1156
  return false;
@@ -149,6 +149,7 @@ function refuse(res, code, message, id) {
149
149
  // HTTP status mapping for the JSON-RPC error code we reply with.
150
150
  res.statusCode = code === JSONRPC_PARSE_ERROR || code === JSONRPC_INVALID_REQUEST ? 400 : // HTTP status code (RFC 9110)
151
151
  code === JSONRPC_METHOD_NOT_FOUND ? 404 : // HTTP status code (RFC 9110)
152
+ code === JSONRPC_AUTH_REQUIRED ? 401 : // RFC 6750 §3 — bearer challenge is 401
152
153
  code === JSONRPC_INTERNAL_ERROR ? 500 : 400; // HTTP status code (RFC 9110)
153
154
  res.end(body);
154
155
  }
@@ -190,7 +191,11 @@ function _checkRedirectUri(uri, allowlist, errorClass) {
190
191
  "mcp: redirect_uri not in allowlist (OAuth 2.1 / RFC 9700 sec 4.1.1)");
191
192
  }
192
193
  var parsed;
193
- try { parsed = safeUrl.parse(uri); }
194
+ // Allow http: through the PARSE (default is https-only) so a loopback
195
+ // http://localhost redirect (RFC 8252 native-app / CLI MCP client) reaches
196
+ // the isHttps||isLocal gate below instead of failing here as "did not parse".
197
+ // Non-loopback http is still refused by that gate (RFC 9700 §4.1.1).
198
+ try { parsed = safeUrl.parse(uri, { allowedProtocols: ["https:", "http:"] }); }
194
199
  catch (_e) {
195
200
  throw errorClass.factory("mcp/bad-redirect-uri",
196
201
  "mcp: redirect_uri did not parse");
@@ -321,7 +326,11 @@ function serverGuard(opts) {
321
326
  req.mcpClaims = claims || null;
322
327
 
323
328
  var path = String(req.url || "").split("?")[0];
324
- if (path === "/register" || path.endsWith("/register")) {
329
+ // Strip trailing slashes for the match — most routers normalize
330
+ // `/register/` to the same handler, so the guard must too (an exact/
331
+ // suffix match on the raw path let `/register/` slip past the refusal).
332
+ var normPath = path.replace(/\/+$/, "");
333
+ if (normPath === "/register" || normPath.endsWith("/register")) {
325
334
  if (!allowDynamicRegister) {
326
335
  _emitDenied(req, "mcp.register.refused-static", "dynamic registration disabled", { path: path });
327
336
  return refuse(res, JSONRPC_METHOD_NOT_FOUND, "dynamic client registration is not permitted");
@@ -663,7 +672,22 @@ function _validateValueAgainstSchema(value, schema, path) {
663
672
  if (typeof schema.minimum === "number" && value < schema.minimum) return path + ": " + value + " < minimum " + schema.minimum;
664
673
  if (typeof schema.maximum === "number" && value > schema.maximum) return path + ": " + value + " > maximum " + schema.maximum;
665
674
  }
666
- if (t === "object" && value && typeof value === "object" && !Array.isArray(value)) {
675
+ // A schema with `properties`/`required`/`additionalProperties` but no explicit
676
+ // `type: "object"` is valid + common JSON Schema — treat it as an object schema
677
+ // so those constraints are ENFORCED, not silently skipped (fail-open on the
678
+ // whole validation gate, OWASP LLM07).
679
+ var describesObject = t === "object" || (t === undefined &&
680
+ (schema.properties !== undefined || schema.required !== undefined ||
681
+ schema.additionalProperties !== undefined));
682
+ if (describesObject) {
683
+ // An inferred object schema (no explicit `type`) skips the top type-match,
684
+ // so a scalar / null / array would otherwise pass by simply skipping the
685
+ // checks below — the exact fail-open this branch exists to close. Reject a
686
+ // non-object value explicitly.
687
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
688
+ return path + ": expected object, got " +
689
+ (Array.isArray(value) ? "array" : (value === null ? "null" : typeof value));
690
+ }
667
691
  if (Array.isArray(schema.required)) {
668
692
  for (var ri = 0; ri < schema.required.length; ri++) {
669
693
  if (!Object.prototype.hasOwnProperty.call(value, schema.required[ri])) {
@@ -766,7 +790,9 @@ var MCP_PROTOCOL_VERSIONS_ACCEPTED = ["2024-11-05", "2025-03-26", "2025-06-18",
766
790
  */
767
791
  function _assertProtocolVersion(req, opts) {
768
792
  opts = opts || {};
769
- var accepted = Array.isArray(opts.accepted) && opts.accepted.length > 0
793
+ // An explicit empty `accepted: []` means "accept no version" (reject all), not
794
+ // "fall back to the default set" — only an absent/non-array value defaults.
795
+ var accepted = Array.isArray(opts.accepted)
770
796
  ? opts.accepted : MCP_PROTOCOL_VERSIONS_ACCEPTED;
771
797
  var hdr = req && req.headers && req.headers["mcp-protocol-version"];
772
798
  if (typeof hdr !== "string" || hdr.length === 0) {
@@ -854,6 +880,13 @@ function _samplingGuard(opts) {
854
880
  throw new McpError("mcp/sampling-too-many-messages",
855
881
  "sampling.guard: " + messages.length + " messages > maxMessagesPerRequest=" + maxMsg);
856
882
  }
883
+ // Reject a non-number maxTokens on this untrusted, server-initiated surface
884
+ // — a string "999999" would otherwise skip the numeric cap entirely and a
885
+ // downstream SDK that coerces it would blow past the budget (type-confusion).
886
+ if (samplingRequest.maxTokens !== undefined && typeof samplingRequest.maxTokens !== "number") {
887
+ throw new McpError("mcp/sampling-bad-max-tokens",
888
+ "sampling.guard: maxTokens must be a number");
889
+ }
857
890
  if (typeof samplingRequest.maxTokens === "number" && samplingRequest.maxTokens > maxTokens) {
858
891
  throw new McpError("mcp/sampling-too-many-tokens",
859
892
  "sampling.guard: requested maxTokens " + samplingRequest.maxTokens +
@@ -282,7 +282,15 @@ function _resolveLabels(defaultLabels, declaredNames, callLabels) {
282
282
  "label '" + declaredNames[n] + "' is required (declared in labelNames)");
283
283
  }
284
284
  }
285
- return out;
285
+ // Redact credential-shaped values on the resolved labels themselves — the
286
+ // stored entry.labels are rendered verbatim into the exposition stream, so
287
+ // redacting only inside _labelsKey (the Map cardinality key) left raw bearer
288
+ // tokens / API keys / JWTs reaching /metrics. _labelsKey re-runs the same
289
+ // coercion, so this stays idempotent.
290
+ var redacted = {};
291
+ var ok = Object.keys(out);
292
+ for (var r = 0; r < ok.length; r++) redacted[ok[r]] = _validateLabelValue(out[ok[r]]);
293
+ return redacted;
286
294
  }
287
295
 
288
296
  // ---- registry factory ----
@@ -384,6 +392,10 @@ function create(opts) {
384
392
  values: values,
385
393
  inc: function (callLabels, n) {
386
394
  var arg = _normalizeLabelArg(callLabels, n, 1);
395
+ if (typeof arg.value !== "number" || !isFinite(arg.value)) {
396
+ throw new MetricsError("metrics/counter-bad-value",
397
+ "counter.inc value must be a finite number");
398
+ }
387
399
  if (arg.value < 0) {
388
400
  throw new MetricsError("metrics/counter-decrement",
389
401
  "counter.inc value must be >= 0 (got " + arg.value + ") — counters never decrease");
@@ -454,7 +466,7 @@ function create(opts) {
454
466
  values: values,
455
467
  set: function (callLabels, v) {
456
468
  var arg = _normalizeLabelArg(callLabels, v, NaN);
457
- if (typeof arg.value !== "number" || isNaN(arg.value)) {
469
+ if (typeof arg.value !== "number" || !isFinite(arg.value)) {
458
470
  throw new MetricsError("metrics/gauge-bad-value",
459
471
  "gauge.set value must be a finite number");
460
472
  }
@@ -520,7 +532,7 @@ function create(opts) {
520
532
  values: values,
521
533
  observe: function (callLabels, v, exemplar) {
522
534
  var arg = _normalizeLabelArg(callLabels, v, NaN);
523
- if (typeof arg.value !== "number" || isNaN(arg.value)) {
535
+ if (typeof arg.value !== "number" || !isFinite(arg.value)) {
524
536
  throw new MetricsError("metrics/histogram-bad-value",
525
537
  "histogram.observe value must be a finite number");
526
538
  }
@@ -157,8 +157,12 @@ function _parseChecksumHeader(headerValue, allowedSet) {
157
157
  var algo = kvp.key;
158
158
  var digestB64 = kvp.value.trim();
159
159
  if (algo.length === 0 || digestB64.length === 0) return { error: "malformed" };
160
- if (!allowedSet[algo]) return { error: "algo-unsupported" };
160
+ // hasOwnProperty-guarded lookups a bare `allowedSet[algo]` resolves
161
+ // Object.prototype for algo="__proto__"/"constructor" (truthy), bypassing the
162
+ // unsupported-algo guard and handing createHash a non-string → HTTP 500.
163
+ if (!Object.prototype.hasOwnProperty.call(allowedSet, algo)) return { error: "algo-unsupported" };
161
164
  if (!/^[A-Za-z0-9+/=]+$/.test(digestB64)) return { error: "malformed" };
165
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_CHECKSUM_ALGORITHMS, algo)) return { error: "algo-unsupported" };
162
166
  var nodeAlgo = KNOWN_CHECKSUM_ALGORITHMS[algo];
163
167
  if (!nodeAlgo) return { error: "algo-unsupported" };
164
168
  return { algo: algo, nodeAlgo: nodeAlgo, digestB64: digestB64 };
@@ -570,7 +574,10 @@ function create(opts) {
570
574
  if (rec.length === null && req.headers["upload-length"] !== undefined) {
571
575
  // Upload-Defer-Length finalization (§4.3) — declare length on first PATCH
572
576
  var declared = parseInt(req.headers["upload-length"], 10);
573
- if (!isFinite(declared) || declared < 0) {
577
+ // Same strict parse the POST creation path uses — reject trailing junk
578
+ // ("10abc" → 10, "0x10" → 0) rather than parseInt-ing leniently.
579
+ if (!isFinite(declared) || declared < 0 ||
580
+ String(declared) !== String(req.headers["upload-length"]).trim()) {
574
581
  return _writeError(res, STATUS_BAD_REQUEST, "Upload-Length must be a non-negative integer");
575
582
  }
576
583
  if (maxSize !== undefined && declared > maxSize) {
@@ -262,6 +262,14 @@ function parse(input, opts) {
262
262
  return _parseBlockSequence(k, indent, depth);
263
263
  }
264
264
 
265
+ // Root-level flow collection ({...} / [...]) — recognize the flow bracket
266
+ // BEFORE the block-mapping key scan. Otherwise a root JSON/flow object like
267
+ // '{a: 1, b: 2}' matches the block-mapping key scan (the first ': ' makes
268
+ // '{a' look like a bare key) and silently returns structurally-wrong data.
269
+ if (content.charAt(0) === "{" || content.charAt(0) === "[") {
270
+ return { value: _parseInlineValue(content, firstLine.lineNumber, indent), nextLine: k + 1 };
271
+ }
272
+
265
273
  // Block mapping starts with a key (bare or quoted) followed by `:`.
266
274
  var keyRange = _scanKeyRange(content, firstLine.lineNumber, indent);
267
275
  if (keyRange) {
@@ -493,8 +501,19 @@ function parse(input, opts) {
493
501
  // Strip trailing whitespace/comment already done by caller (mostly).
494
502
  // Handle: flow [...] / {...}, quoted strings, plain scalars.
495
503
  var t = text;
496
- if (t.charAt(0) === "[") return _parseFlowSequence(t, lineNumber, col, 0);
497
- if (t.charAt(0) === "{") return _parseFlowMapping(t, lineNumber, col, 0);
504
+ if (t.charAt(0) === "[" || t.charAt(0) === "{") {
505
+ // Reject trailing junk after a flow collection (matching the quoted-scalar
506
+ // branch below) using _parseFlowValue's end position, then return the value
507
+ // via the direct parser so the shape is identical to the pre-existing path.
508
+ var fend = _parseFlowValue(t, 0, lineNumber, col, 0).nextPos;
509
+ var afterFlow = t.slice(fend).replace(/^\s+/, "");
510
+ if (afterFlow.length > 0 && afterFlow.charAt(0) !== "#") {
511
+ throw new SafeYamlError("unexpected content after flow collection",
512
+ "yaml/trailing-content", lineNumber, col);
513
+ }
514
+ return t.charAt(0) === "[" ? _parseFlowSequence(t, lineNumber, col, 0)
515
+ : _parseFlowMapping(t, lineNumber, col, 0);
516
+ }
498
517
  if (t.charAt(0) === '"') {
499
518
  var dq = _decodeDoubleQuoted(t, lineNumber, col);
500
519
  var afterDq = _trailingAfterQuoted(t, '"');
@@ -570,6 +589,18 @@ function parse(input, opts) {
570
589
  throw new SafeYamlError("forbidden key '" + key + "'",
571
590
  "yaml/poisoned-key", lineNumber, col + p);
572
591
  }
592
+ // Mirror the block-mapping guards so flow style can't bypass them:
593
+ // the merge key '<<' is banned, and a duplicate key is refused (block
594
+ // rejects both; flow previously accepted '<<' as a literal key and
595
+ // silently kept the LAST value on a repeat — a config-override risk).
596
+ if (key === "<<") {
597
+ throw new SafeYamlError("merge key '<<' not supported (anchor-using feature)",
598
+ "yaml/merge-key-banned", lineNumber, col + p);
599
+ }
600
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
601
+ throw new SafeYamlError("duplicate mapping key '" + key + "'",
602
+ "yaml/duplicate-key", lineNumber, col + p);
603
+ }
573
604
  _bumpKeys(lineNumber);
574
605
  p = keyVal.nextPos;
575
606
  p = _flowSkipWsIndex(text, p);
@@ -69,7 +69,17 @@ class SafeBufferError extends FrameworkError {
69
69
 
70
70
  function _throw(errorClass, message, code) {
71
71
  var Cls = errorClass || SafeBufferError;
72
- throw new Cls(message, code);
72
+ var err = new Cls(message, code);
73
+ // SafeBufferError takes (message, code) but a defineClass errorClass (the
74
+ // convention for every framework-error, e.g. a caller's TusError) takes
75
+ // (code, message) — the opposite order — so `new Cls(message, code)` would
76
+ // SWAP .code/.message for such a class (a caller branching on e.code then
77
+ // never matches). Set both fields explicitly so they are correct whichever
78
+ // constructor order the passed class uses; the class-derived flags are set
79
+ // from the class options, not the code value, so this is safe.
80
+ err.code = code;
81
+ err.message = message;
82
+ throw err;
73
83
  }
74
84
 
75
85
  /**
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@blamejs/core",
9
- "version": "0.16.2",
9
+ "version": "0.16.3",
10
10
  "license": "Apache-2.0",
11
11
  "bin": {
12
12
  "blamejs": "bin/blamejs.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,71 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.16.3",
4
+ "date": "2026-07-03",
5
+ "headline": "A batch of fail-open, injection-bypass and error-path fixes across the metrics, guard-sql, MCP, YAML-parser, content-credentials, tus-upload and buffer primitives — including a credential-leak fix on the /metrics exposition surface — alongside a further expansion of the automated test suite",
6
+ "summary": "This patch closes a set of security and robustness defects concentrated in the error-handling and adversarial-input paths of the observability, SQL-guard, MCP, YAML-parsing, content-credentials and resumable-upload primitives, surfaced while continuing to expand the automated test suite. The most consequential: credential-shaped metric label values were redacted only in the internal cardinality key, not in the rendered exposition, so a raw bearer token / API key / JWT placed on a label leaked verbatim to every `/metrics` scrape reader — redaction now applies to the stored labels the exposition renders. The SQL guard's fragment mode (the `whereRaw` escape hatch) refused an embedded single-quoted literal but not a PostgreSQL dollar-quoted string (`$tag$…$tag$`) or a lone top-level `;`, so operator concatenation into a dollar-quoted context bypassed the floor that forces `?`-placeholder binding. MCP tool-input validation was fail-open when a JSON Schema omitted an explicit top-level `type:\"object\"` (a valid, common shape) — `required`/`properties`/`additionalProperties` were skipped and any input accepted; and its server-initiated sampling token cap was bypassable with a string `maxTokens`. The YAML parser's flow style (`{…}` / `[…]`) accepted duplicate keys and the merge key that block style rejects — letting a later value silently override an earlier one a preceding check had already read — and mis-parsed a root-level JSON object. content-credentials returned a only shallowly-frozen manifest (nested claim fields stayed mutable before signing), and its identity-assertion hash re-check used set membership rather than multiset matching, so duplicate referenced assertions could leave a bound assertion never re-confirmed. Robustness fixes round out the batch: metrics now reject `±Infinity`; the buffer collector preserves a caller error class's code/message (they were swapped for framework error classes, so a caller branching on the error code — e.g. a resumable-upload `413` vs `400` — never matched); resumable uploads return `413` (not a `400` that leaked the internal error code) on an oversized chunk, reject a prototype-key checksum algorithm with `400` (not `500`), and parse a deferred `Upload-Length` as strictly as creation; the MCP guard refuses `/register/` (trailing slash) like `/register`, answers a missing/invalid bearer with `401` and its `WWW-Authenticate` challenge instead of `400`, honors an explicit empty accepted-version list, and accepts a loopback `http` redirect URI for native/CLI clients while still refusing non-loopback `http`.",
7
+ "sections": [
8
+ {
9
+ "heading": "Security",
10
+ "items": [
11
+ {
12
+ "title": "Credential-shaped metric labels are redacted in the `/metrics` exposition, not just the cardinality key",
13
+ "body": "The metrics registry rewrote a credential-shaped label value to `[REDACTED-CREDENTIAL]`, but only inside the function that builds the internal Map cardinality key. The stored label object — the one the exposition renderer emits — kept the raw value, so a bearer token, API key or JWT placed on a label (e.g. an `authorization` label) was written verbatim to every `/metrics` scrape reader, contradicting the documented guarantee that the bytes never reach the scrape stream. Redaction now runs on the resolved labels themselves, so the key and the rendered output both carry the marker."
14
+ },
15
+ {
16
+ "title": "SQL guard fragment mode refuses PostgreSQL dollar-quoted literals and any top-level semicolon",
17
+ "body": "In fragment mode (`whereRaw`), the embedded-string-literal floor that forces every value through a `?` placeholder detected a single-quoted literal but not a dollar-quoted one (`$tag$…$tag$` or `$$…$$`), so operator input concatenated into a dollar-quoted context passed the strict floor that the equivalent single-quoted value is refused by. The floor now recognizes dollar-quoted literals. Fragment mode also now refuses any top-level `;` — including a lone or trailing one — rather than relying on the stacked-statement scan, which only fired when a second statement followed the `;`."
18
+ },
19
+ {
20
+ "title": "MCP tool-input validation enforces the schema without an explicit `type:\"object\"`",
21
+ "body": "`validateToolInput` gated the `required` / `properties` / `additionalProperties` checks on the schema declaring `type:\"object\"`. A schema that carries `properties`/`required` but omits `type` — a valid and common JSON Schema shape — was treated as untyped, so those constraints were skipped and any argument object was accepted, defeating the primitive's stated purpose (OWASP LLM07). Such a schema is now treated as an object schema and its constraints enforced."
22
+ },
23
+ {
24
+ "title": "MCP sampling token cap rejects a non-numeric `maxTokens`",
25
+ "body": "`sampling.guard` applied the per-request token cap only when `maxTokens` was a number, so a hostile-tool-initiated sampling request carrying `maxTokens` as a string skipped the cap entirely — and a downstream SDK that coerces the string would then exceed the budget. A non-numeric `maxTokens` on this server-initiated surface is now refused with a typed error."
26
+ },
27
+ {
28
+ "title": "content-credentials identity-assertion re-check uses multiset matching",
29
+ "body": "`verifyIdentityAssertion` re-confirmed the signed referenced-assertion hash binding with count-equality plus set membership, never consuming a matched entry. Supplying referenced assertions that hash to `[A, A]` against a signed binding of `[A, B]` passed — the counts matched and each supplied hash was present — even though the bound `hash(B)` was never re-confirmed against a real assertion (a transplant fail-open). The re-check now consumes each bound hash as it is matched, so duplicates can no longer stand in for a distinct bound assertion."
30
+ },
31
+ {
32
+ "title": "YAML flow-style mappings refuse duplicate keys and the merge key like block style",
33
+ "body": "Block mappings rejected a duplicate key (`yaml/duplicate-key`) and the anchor-merge key `<<` (`yaml/merge-key-banned`), but flow mappings (`{ a: 1, a: 2 }`) enforced neither — silently keeping the last value on a repeat and accepting `<<` as an ordinary key. A value an earlier check has already read could thus be overridden through flow style. Flow mappings now apply both guards."
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ "heading": "Fixed",
39
+ "items": [
40
+ {
41
+ "title": "Metrics reject non-finite values",
42
+ "body": "`gauge.set`, `histogram.observe` and `counter.inc` guarded their value with an `isNaN`-only check (counter only checked `< 0`), so `±Infinity` passed and produced `Infinity` / `-Infinity` in the exposition — invalid Prometheus 0.0.4 / OpenMetrics, which a scraper rejects. All three now require a finite number, matching their `must be a finite number` error text."
43
+ },
44
+ {
45
+ "title": "Buffer stream collector preserves a caller error class's code and message",
46
+ "body": "`safeBuffer.collectStream` constructs its size-limit error with `(message, code)`, matching its own error class — but a framework error class (the convention elsewhere) takes `(code, message)`, the opposite order, so a caller that passed its own error class received an error with `code` and `message` swapped. A caller branching on the error code then never matched — for example the resumable-upload handler mapped an oversized chunk to `400` instead of `413`. The collector now sets both fields explicitly so they are correct whichever constructor order the passed class uses."
47
+ },
48
+ {
49
+ "title": "Resumable upload: correct status codes and strict deferred-length parse",
50
+ "body": "An oversized `PATCH`/creation chunk now returns `413 Payload Too Large` instead of a `400` whose body leaked the internal error code; an `Upload-Checksum` naming a prototype key (e.g. `__proto__`) returns `400 unsupported` instead of a `500` from an unguarded lookup; and a deferred-length finalization now parses `Upload-Length` with the same strict check as creation, rejecting trailing junk (`\"10abc\"`) rather than silently truncating it."
51
+ },
52
+ {
53
+ "title": "MCP guard: trailing-slash registration refusal, 401 bearer challenge, empty accepted-version list",
54
+ "body": "The static-registration refusal matched `/register` exactly or by suffix, so `/register/` — which most routers normalize to the same handler — slipped through; it is now refused too. A missing or invalid bearer now returns `401 Unauthorized` with its `WWW-Authenticate` challenge (RFC 6750 §3) instead of `400`, so clients keyed on `401` re-authenticate. And an explicit empty `accepted` protocol-version list now rejects every version instead of silently widening to the default set."
55
+ },
56
+ {
57
+ "title": "MCP accepts a loopback `http` redirect URI",
58
+ "body": "`redirect_uri` validation parsed the URI with the default HTTPS-only protocol set, so a loopback `http://localhost/…` redirect (RFC 8252 native-app / CLI client, permitted by RFC 9700 §4.1.1) failed as `did not parse` before the local-host allowance could run. The parse now admits `http` so the loopback allowance applies; a non-loopback `http` redirect is still refused."
59
+ },
60
+ {
61
+ "title": "YAML: trailing content after a flow collection and root-level JSON objects",
62
+ "body": "`root: [1, 2] junk` silently dropped the trailing content where the quoted-scalar path rejects it; trailing content after a flow collection is now refused (`yaml/trailing-content`). A root-level flow/JSON object (`{\"a\": 1, \"b\": 2}`) was mis-scanned as a block mapping with a garbage key and returned structurally-wrong data; the leading flow bracket is now recognized before the block-mapping scan so a root JSON object parses correctly."
63
+ },
64
+ {
65
+ "title": "content-credentials: deep-frozen manifest and typed rejections for bad build/sign inputs",
66
+ "body": "`build` returned a shallowly-frozen manifest, leaving every nested claim field (`provider`/`system`/`content`) mutable before signing despite the documented pre-sign immutability guarantee; the manifest is now deep-frozen. A non-finite or out-of-Date-range `generatedAt` (`NaN` / `Infinity`) crashed with an untyped `RangeError`, and a non-Buffer `certChain` entry to `signCose` threw an untyped Node `TypeError`; both now reject with a typed `content-credentials/*` error at call time."
67
+ }
68
+ ]
69
+ }
70
+ ]
71
+ }