@blamejs/core 0.14.17 → 0.14.18

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/lib/static.js CHANGED
@@ -6,7 +6,8 @@
6
6
  * quotas (cluster-shared via b.cache), Range support (RFC 7233 single-range),
7
7
  * the full conditional-request set (If-None-Match / If-Match /
8
8
  * If-Modified-Since / If-Unmodified-Since), MIME allowlist with magic-byte
9
- * verification (composes b.fileType), per-request operator hook, idle-stream
9
+ * verification (composes b.fileType), per-request operator hooks (onServe
10
+ * on the success path, onError on every refusal path), idle-stream
10
11
  * timeout, cancellation propagation, force-revoke, and compliance-retention
11
12
  * gating.
12
13
  *
@@ -16,6 +17,12 @@
16
17
  * var mw = serve.middleware; // (req, res, next)
17
18
  * await b.staticServe.integrity(absFilePath); // SRI helper (SHA-384)
18
19
  *
20
+ * // onError mirrors onServe for the refusal paths (403 / 404 / 415 /
21
+ * // 412 / 416 / 429 / 451 / 500): it receives
22
+ * // { req, res, urlPath, absPath, status, code, actor } AFTER the
23
+ * // refusal response is written. Observability-only — a throw is
24
+ * // swallowed so a broken hook can't tear down the socket.
25
+ *
19
26
  * Backwards compatible: every existing opt (root, mountPath,
20
27
  * hashedPathPattern, indexFile, defaultMaxAge, contentTypes) keeps its
21
28
  * original meaning. New opts (permissions, cache, audit, observability,
@@ -450,6 +457,7 @@ function _validateCreateOpts(opts) {
450
457
  validateOpts.auditShape(opts.audit, "staticServe.create", StaticServeError);
451
458
  validateOpts.observabilityShape(opts.observability, "staticServe.create", StaticServeError);
452
459
  validateOpts.optionalFunction(opts.onServe, "staticServe.create: onServe", StaticServeError);
460
+ validateOpts.optionalFunction(opts.onError, "staticServe.create: onError", StaticServeError);
453
461
  // contentSafety — extension-keyed gate map. Default behaviour: when
454
462
  // undefined, the framework wires b.guardAll.byExtension({ profile:
455
463
  // "strict" }) automatically so every shipped guard is ON by default.
@@ -604,7 +612,7 @@ function create(opts) {
604
612
  "root", "mountPath", "hashedPathPattern",
605
613
  "indexFile", "defaultMaxAge", "contentTypes",
606
614
  "permissions", "cache", "fileType", "retention", "revokeStore",
607
- "allowedFileTypes", "audit", "observability", "onServe",
615
+ "allowedFileTypes", "audit", "observability", "onServe", "onError",
608
616
  "acceptRanges", "auditSuccess", "auditFailures",
609
617
  "maxBytesPerActorPerWindowMs", "maxBytesAllActorsPerWindowMs",
610
618
  "bandwidthWindowMs", "maxConcurrentDownloadsPerActor", "maxIdleMs",
@@ -657,6 +665,7 @@ function create(opts) {
657
665
  contentSafety = opts.contentSafety;
658
666
  }
659
667
  var onServe = opts.onServe || null;
668
+ var onError = opts.onError || null;
660
669
  var audit = opts.audit || null;
661
670
  var auditSuccess = cfg.auditSuccess;
662
671
  var auditFailures = cfg.auditFailures;
@@ -756,6 +765,26 @@ function create(opts) {
756
765
  var actorCtx = requestHelpers.extractActorContext(req);
757
766
  var actorKey = _actorKeyFromContext(actorCtx);
758
767
 
768
+ // Request-scoped error writer. Wraps the module-level _writeError so
769
+ // every refusal path (403 / 404 / 415 / 412 / 416 / 429 / 451 / 500)
770
+ // also invokes the operator's onError hook — the success-path mirror
771
+ // of onServe. The signature matches _writeError exactly; the `code`
772
+ // argument routes through to the hook so operators can branch on the
773
+ // refusal reason. The hook is observability-only: it runs AFTER the
774
+ // response is written and a throw is swallowed so a broken sink can't
775
+ // turn a 4xx into a torn-down socket.
776
+ function writeErr(r, status, code, message, headers) {
777
+ _writeError(r, status, code, message, headers);
778
+ if (onError) {
779
+ try {
780
+ onError({
781
+ req: req, res: r, urlPath: urlPath, absPath: absPath,
782
+ status: status, code: code, actor: actorCtx,
783
+ });
784
+ } catch (_he) { /* hook best-effort */ }
785
+ }
786
+ }
787
+
759
788
  // Permission gate (403)
760
789
  var perm = await _checkPermission(req);
761
790
  if (!perm.ok) {
@@ -766,7 +795,7 @@ function create(opts) {
766
795
  outcome: "failure", reason: "permission_denied", resource: urlPath,
767
796
  }, actorCtx));
768
797
  }
769
- return _writeError(res, HTTP.FORBIDDEN, "permission_denied",
798
+ return writeErr(res, HTTP.FORBIDDEN, "permission_denied",
770
799
  "Forbidden");
771
800
  }
772
801
 
@@ -788,7 +817,7 @@ function create(opts) {
788
817
  outcome: "failure", reason: "revoked", resource: urlPath,
789
818
  }, actorCtx));
790
819
  }
791
- return _writeError(res, HTTP.NOT_FOUND, "not_found", "Not Found");
820
+ return writeErr(res, HTTP.NOT_FOUND, "not_found", "Not Found");
792
821
  }
793
822
 
794
823
  // Compliance retention (451)
@@ -800,7 +829,7 @@ function create(opts) {
800
829
  outcome: "failure", reason: "retention_blocked", resource: urlPath,
801
830
  }, actorCtx));
802
831
  }
803
- return _writeError(res, HTTP.UNAVAILABLE_FOR_LEGAL_REASONS,
832
+ return writeErr(res, HTTP.UNAVAILABLE_FOR_LEGAL_REASONS,
804
833
  "retention_blocked", "Unavailable For Legal Reasons");
805
834
  }
806
835
 
@@ -820,7 +849,7 @@ function create(opts) {
820
849
  detectedMime: mimeCheck.detected || null,
821
850
  }, actorCtx));
822
851
  }
823
- return _writeError(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
852
+ return writeErr(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
824
853
  "mime_rejected", "Unsupported Media Type");
825
854
  }
826
855
  }
@@ -861,7 +890,7 @@ function create(opts) {
861
890
  catch (_e) {
862
891
  stats.failures += 1;
863
892
  if (gateHandle) { try { await gateHandle.close(); } catch (_ce) { /* close best-effort */ } }
864
- return _writeError(res, HTTP.INTERNAL_SERVER_ERROR,
893
+ return writeErr(res, HTTP.INTERNAL_SERVER_ERROR,
865
894
  "read_failed", "Internal Server Error");
866
895
  }
867
896
  try { await gateHandle.close(); } catch (_ce) { /* close best-effort */ }
@@ -885,7 +914,7 @@ function create(opts) {
885
914
  error: gateErr && gateErr.message,
886
915
  }, actorCtx));
887
916
  }
888
- return _writeError(res, HTTP.INTERNAL_SERVER_ERROR,
917
+ return writeErr(res, HTTP.INTERNAL_SERVER_ERROR,
889
918
  "content_safety_threw", "Internal Server Error");
890
919
  }
891
920
  if (!gateDecision.ok || gateDecision.action === "refuse") {
@@ -898,7 +927,7 @@ function create(opts) {
898
927
  issues: gateContract.summarizeIssues(gateDecision.issues),
899
928
  }, actorCtx));
900
929
  }
901
- return _writeError(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
930
+ return writeErr(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
902
931
  "content_safety_refused", "Unsupported Media Type");
903
932
  }
904
933
  if (gateDecision.action === "sanitize" && gateDecision.sanitized) {
@@ -929,7 +958,7 @@ function create(opts) {
929
958
  if (ifMatch && ifMatch !== "*" && ifMatch !== meta.etag) {
930
959
  stats.failures += 1;
931
960
  _emitObs("staticServe.precondition_failed", 1, { route: urlPath, header: "if-match" });
932
- return _writeError(res, HTTP.PRECONDITION_FAILED || 412,
961
+ return writeErr(res, HTTP.PRECONDITION_FAILED || 412,
933
962
  "precondition_failed", "Precondition Failed");
934
963
  }
935
964
 
@@ -956,7 +985,7 @@ function create(opts) {
956
985
  if (isFinite(ius) && Math.floor(meta.mtimeMs / C.TIME.seconds(1)) > Math.floor(ius / C.TIME.seconds(1))) {
957
986
  stats.failures += 1;
958
987
  _emitObs("staticServe.precondition_failed", 1, { route: urlPath, header: "if-unmodified-since" });
959
- return _writeError(res, HTTP.PRECONDITION_FAILED,
988
+ return writeErr(res, HTTP.PRECONDITION_FAILED,
960
989
  "precondition_failed", "Precondition Failed");
961
990
  }
962
991
  }
@@ -970,19 +999,19 @@ function create(opts) {
970
999
  if (range && (range.malformed || range.multi)) {
971
1000
  stats.failures += 1;
972
1001
  _emitObs("staticServe.range_invalid", 1, { route: urlPath });
973
- return _writeError(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
1002
+ return writeErr(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
974
1003
  "Range Not Satisfiable", { "Content-Range": "bytes */" + meta.size });
975
1004
  }
976
1005
  if (range && range.unsatisfiable) {
977
1006
  stats.failures += 1;
978
1007
  _emitObs("staticServe.range_invalid", 1, { route: urlPath });
979
- return _writeError(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
1008
+ return writeErr(res, HTTP.RANGE_NOT_SATISFIABLE, "range_not_satisfiable",
980
1009
  "Range Not Satisfiable", { "Content-Range": "bytes */" + meta.size });
981
1010
  }
982
1011
  if (range && cfg.maxRangeBytes !== Infinity && range.length > cfg.maxRangeBytes) {
983
1012
  stats.failures += 1;
984
1013
  _emitObs("staticServe.range_too_large", 1, { route: urlPath });
985
- return _writeError(res, HTTP.RANGE_NOT_SATISFIABLE, "range_too_large",
1014
+ return writeErr(res, HTTP.RANGE_NOT_SATISFIABLE, "range_too_large",
986
1015
  "Range Not Satisfiable", { "Content-Range": "bytes */" + meta.size });
987
1016
  }
988
1017
  if (range) {
@@ -1005,7 +1034,7 @@ function create(opts) {
1005
1034
  current: concCheck.current, cap: concCheck.cap,
1006
1035
  }, actorCtx));
1007
1036
  }
1008
- return _writeError(res, HTTP.TOO_MANY_REQUESTS,
1037
+ return writeErr(res, HTTP.TOO_MANY_REQUESTS,
1009
1038
  "concurrency_cap", "Too Many Requests",
1010
1039
  { "Retry-After": "5" });
1011
1040
  }
@@ -1021,7 +1050,7 @@ function create(opts) {
1021
1050
  scope: bwCheck.scope, used: bwCheck.used, cap: bwCheck.cap,
1022
1051
  }, actorCtx));
1023
1052
  }
1024
- return _writeError(res, HTTP.TOO_MANY_REQUESTS,
1053
+ return writeErr(res, HTTP.TOO_MANY_REQUESTS,
1025
1054
  "bandwidth_quota", "Too Many Requests",
1026
1055
  { "Retry-After": String(bwCheck.retryAfter) });
1027
1056
  }
@@ -1077,7 +1106,7 @@ function create(opts) {
1077
1106
  error: e && e.message,
1078
1107
  }, actorCtx));
1079
1108
  }
1080
- return _writeError(res, HTTP.INTERNAL_SERVER_ERROR, "onServe_threw",
1109
+ return writeErr(res, HTTP.INTERNAL_SERVER_ERROR, "onServe_threw",
1081
1110
  "Internal Server Error");
1082
1111
  }
1083
1112
  }
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  var { defineClass } = require("./framework-error");
32
+ var codepointClass = require("./codepoint-class");
32
33
 
33
34
  var UriTemplateError = defineClass("UriTemplateError", { alwaysPermanent: true });
34
35
 
@@ -59,7 +60,8 @@ function _pctEncode(str, allowReserved) {
59
60
  var ch = str.charAt(i);
60
61
  // Preserve existing percent-encoded triplets when the reserved set is
61
62
  // allowed (operators "+" and "#").
62
- if (allowReserved && ch === "%" && /^[0-9A-Fa-f]{2}$/.test(str.substr(i + 1, 2))) {
63
+ // allow:regex-no-length-cap the substr is a fixed 2-char window
64
+ if (allowReserved && ch === "%" && codepointClass.HEX_PAIR_RE.test(str.substr(i + 1, 2))) {
63
65
  out += str.substr(i, 3); i += 2; continue;
64
66
  }
65
67
  if (UNRESERVED.test(ch) || (allowReserved && RESERVED.test(ch))) { out += ch; continue; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.14.17",
3
+ "version": "0.14.18",
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:e5d17e91-0802-4373-b6b4-f26370a14472",
5
+ "serialNumber": "urn:uuid:b96f0679-8967-43f9-b412-aa598ea52508",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-01T03:23:48.524Z",
8
+ "timestamp": "2026-06-02T15:25:35.604Z",
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.14.17",
22
+ "bom-ref": "@blamejs/core@0.14.18",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.14.17",
25
+ "version": "0.14.18",
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.14.17",
29
+ "purl": "pkg:npm/%40blamejs/core@0.14.18",
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.14.17",
57
+ "ref": "@blamejs/core@0.14.18",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]