@blamejs/core 0.6.70 → 0.7.1

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 (164) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +11 -1
  3. package/index.js +2 -5
  4. package/lib/api-key.js +24 -55
  5. package/lib/api-snapshot.js +4 -1
  6. package/lib/app-shutdown.js +20 -16
  7. package/lib/app.js +6 -5
  8. package/lib/archive.js +58 -50
  9. package/lib/atomic-file.js +11 -5
  10. package/lib/audit-chain.js +12 -4
  11. package/lib/audit-sign.js +38 -13
  12. package/lib/audit-tools.js +4 -4
  13. package/lib/audit.js +8 -4
  14. package/lib/auth/jwt.js +3 -2
  15. package/lib/auth/lockout.js +3 -13
  16. package/lib/auth/oauth.js +27 -17
  17. package/lib/auth/passkey.js +1 -5
  18. package/lib/auth/password.js +23 -13
  19. package/lib/backup/bundle.js +2 -4
  20. package/lib/backup/crypto.js +8 -6
  21. package/lib/backup/index.js +18 -14
  22. package/lib/backup/manifest.js +6 -2
  23. package/lib/break-glass.js +12 -7
  24. package/lib/bundler.js +32 -21
  25. package/lib/cache-redis.js +8 -3
  26. package/lib/cache.js +39 -59
  27. package/lib/canonical-json.js +17 -2
  28. package/lib/chain-writer.js +2 -2
  29. package/lib/cli-helpers.js +9 -4
  30. package/lib/cli.js +27 -5
  31. package/lib/cluster-provider-db.js +2 -1
  32. package/lib/cluster-storage.js +71 -11
  33. package/lib/cluster.js +30 -25
  34. package/lib/config-drift.js +3 -4
  35. package/lib/constants.js +8 -3
  36. package/lib/cookies.js +16 -4
  37. package/lib/credential-hash.js +4 -7
  38. package/lib/crypto.js +9 -9
  39. package/lib/csv.js +11 -8
  40. package/lib/db-declare-row-policy.js +4 -9
  41. package/lib/db-declare-view.js +3 -6
  42. package/lib/db-query.js +9 -3
  43. package/lib/db-schema.js +75 -10
  44. package/lib/db.js +116 -70
  45. package/lib/deprecate.js +3 -8
  46. package/lib/dev.js +30 -19
  47. package/lib/dual-control.js +2 -4
  48. package/lib/error-page.js +32 -39
  49. package/lib/external-db-migrate.js +23 -26
  50. package/lib/external-db.js +11 -3
  51. package/lib/file-type.js +16 -8
  52. package/lib/forms.js +48 -12
  53. package/lib/framework-error.js +1 -2
  54. package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
  55. package/lib/handlers.js +1 -1
  56. package/lib/http-client-cookie-jar.js +25 -18
  57. package/lib/http-client.js +46 -23
  58. package/lib/i18n.js +6 -16
  59. package/lib/jobs.js +5 -1
  60. package/lib/log-stream-cloudwatch.js +31 -30
  61. package/lib/log-stream-local.js +12 -3
  62. package/lib/log-stream-otlp-grpc.js +29 -23
  63. package/lib/log-stream-otlp.js +13 -19
  64. package/lib/log-stream-syslog.js +27 -22
  65. package/lib/log-stream-webhook.js +8 -17
  66. package/lib/log-stream.js +13 -2
  67. package/lib/log.js +83 -5
  68. package/lib/mail-bounce.js +22 -15
  69. package/lib/mail-dkim.js +3 -6
  70. package/lib/mail.js +62 -34
  71. package/lib/metrics.js +61 -25
  72. package/lib/middleware/api-encrypt.js +15 -13
  73. package/lib/middleware/body-parser.js +46 -39
  74. package/lib/middleware/bot-guard.js +25 -4
  75. package/lib/middleware/compression.js +26 -11
  76. package/lib/middleware/cors.js +6 -5
  77. package/lib/middleware/csp-nonce.js +4 -3
  78. package/lib/middleware/csrf-protect.js +1 -1
  79. package/lib/middleware/db-role-for.js +7 -30
  80. package/lib/middleware/health.js +42 -8
  81. package/lib/middleware/index.js +55 -36
  82. package/lib/middleware/rate-limit.js +11 -8
  83. package/lib/middleware/request-id.js +10 -2
  84. package/lib/middleware/request-log.js +3 -2
  85. package/lib/middleware/require-auth.js +5 -2
  86. package/lib/middleware/sse.js +5 -4
  87. package/lib/migration-files.js +37 -0
  88. package/lib/migrations.js +26 -15
  89. package/lib/mtls-ca.js +38 -17
  90. package/lib/mtls-engine-default.js +15 -7
  91. package/lib/network-dns.js +49 -20
  92. package/lib/network-heartbeat.js +7 -7
  93. package/lib/network-nts.js +70 -48
  94. package/lib/network-proxy.js +34 -15
  95. package/lib/network-tls.js +7 -5
  96. package/lib/network.js +3 -3
  97. package/lib/notify.js +14 -36
  98. package/lib/ntp-check.js +7 -6
  99. package/lib/numeric-bounds.js +35 -3
  100. package/lib/object-store/azure-blob-bucket-ops.js +73 -15
  101. package/lib/object-store/azure-blob.js +31 -24
  102. package/lib/object-store/gcs-bucket-ops.js +41 -17
  103. package/lib/object-store/gcs.js +41 -34
  104. package/lib/object-store/http-put.js +2 -11
  105. package/lib/object-store/http-request.js +38 -0
  106. package/lib/object-store/index.js +1 -1
  107. package/lib/object-store/sigv4-bucket-ops.js +33 -37
  108. package/lib/object-store/sigv4.js +50 -30
  109. package/lib/observability.js +14 -3
  110. package/lib/otel-export.js +16 -14
  111. package/lib/pagination.js +23 -8
  112. package/lib/parsers/index.js +11 -5
  113. package/lib/parsers/safe-env.js +43 -14
  114. package/lib/parsers/safe-ini.js +29 -10
  115. package/lib/parsers/safe-toml.js +55 -25
  116. package/lib/parsers/safe-xml.js +22 -11
  117. package/lib/parsers/safe-yaml.js +55 -17
  118. package/lib/permissions.js +24 -33
  119. package/lib/pqc-agent.js +4 -2
  120. package/lib/pqc-gate.js +28 -15
  121. package/lib/protobuf-encoder.js +9 -3
  122. package/lib/protocol-dispatcher.js +29 -12
  123. package/lib/pubsub-redis.js +25 -8
  124. package/lib/pubsub.js +84 -12
  125. package/lib/queue-local.js +13 -4
  126. package/lib/queue-redis.js +2 -2
  127. package/lib/queue-sqs.js +21 -16
  128. package/lib/queue.js +8 -4
  129. package/lib/redis-client.js +13 -10
  130. package/lib/request-helpers.js +33 -2
  131. package/lib/restore-bundle.js +2 -4
  132. package/lib/restore-rollback.js +4 -1
  133. package/lib/restore.js +12 -10
  134. package/lib/retry.js +58 -9
  135. package/lib/router.js +96 -27
  136. package/lib/safe-async.js +97 -4
  137. package/lib/safe-buffer.js +47 -0
  138. package/lib/safe-json.js +18 -9
  139. package/lib/safe-schema.js +54 -18
  140. package/lib/safe-url.js +6 -2
  141. package/lib/scheduler.js +25 -14
  142. package/lib/security-assert.js +31 -26
  143. package/lib/seeders.js +71 -60
  144. package/lib/session.js +13 -7
  145. package/lib/slug.js +35 -8
  146. package/lib/ssrf-guard.js +58 -43
  147. package/lib/static.js +11 -7
  148. package/lib/storage.js +2 -1
  149. package/lib/template.js +17 -7
  150. package/lib/testing.js +28 -8
  151. package/lib/time.js +26 -5
  152. package/lib/totp.js +18 -9
  153. package/lib/tracing.js +15 -5
  154. package/lib/uuid.js +47 -20
  155. package/lib/validate-opts.js +213 -0
  156. package/lib/vault/index.js +36 -27
  157. package/lib/vault/rotate.js +41 -22
  158. package/lib/vault/wrap.js +26 -19
  159. package/lib/webhook.js +37 -68
  160. package/lib/websocket.js +98 -52
  161. package/package.json +1 -1
  162. package/sbom.cyclonedx.json +6 -6
  163. package/lib/auth/totp.js +0 -17
  164. package/lib/object-store/retry.js +0 -15
package/lib/webhook.js CHANGED
@@ -68,20 +68,19 @@
68
68
 
69
69
  var crypto = require("./crypto");
70
70
  var httpClient = require("./http-client");
71
+ var safeBuffer = require("./safe-buffer");
71
72
  var safeUrl = require("./safe-url");
72
73
  var retry = require("./retry");
73
74
  var C = require("./constants");
74
75
  var lazyRequire = require("./lazy-require");
75
76
  var numericChecks = require("./numeric-checks");
76
77
  var requestHelpers = require("./request-helpers");
78
+ var validateOpts = require("./validate-opts");
77
79
  var { WebhookError } = require("./framework-error");
78
80
 
79
81
  var observability = lazyRequire(function () { return require("./observability"); });
80
82
 
81
- function _emitEvent(name, value, labels) {
82
- try { observability().event(name, value, labels || {}); }
83
- catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
84
- }
83
+ function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
85
84
 
86
85
  var _err = WebhookError.factory;
87
86
 
@@ -194,7 +193,7 @@ function _hmacSign(key, data) {
194
193
  }
195
194
 
196
195
  function _hmacVerify(key, data, expectedHex) {
197
- if (typeof expectedHex !== "string" || !/^[0-9a-fA-F]+$/.test(expectedHex)) return false;
196
+ if (!safeBuffer.isHex(expectedHex)) return false;
198
197
  var actualHex = crypto.hmacSha3(key, data);
199
198
  return crypto.timingSafeEqual(actualHex, expectedHex);
200
199
  }
@@ -204,8 +203,7 @@ function _pqcSign(privateKeyPem, data) {
204
203
  }
205
204
 
206
205
  function _pqcVerify(publicKeyPem, data, expectedHex) {
207
- if (typeof expectedHex !== "string" || expectedHex.length === 0 ||
208
- !/^[0-9a-fA-F]+$/.test(expectedHex) || (expectedHex.length % 2) !== 0) {
206
+ if (!safeBuffer.isHex(expectedHex) || (expectedHex.length % 2) !== 0) {
209
207
  return false;
210
208
  }
211
209
  var sigBuf;
@@ -240,9 +238,7 @@ function _parseSignatureHeader(headerValue) {
240
238
  // ---- Signer ----
241
239
 
242
240
  function _validateSignerOpts(opts) {
243
- if (!opts || typeof opts !== "object") {
244
- throw _err("BAD_OPT", "webhook.signer: opts must be an object");
245
- }
241
+ validateOpts.requireObject(opts, "webhook.signer", WebhookError);
246
242
  _validateAlgo(opts.algo);
247
243
  _validateKeysShape("webhook.signer", opts.algo, opts.keys, "signer");
248
244
  var kids = _objectKeys(opts.keys);
@@ -255,43 +251,29 @@ function _validateSignerOpts(opts) {
255
251
  throw _err("BAD_OPT", "webhook.signer: defaultKid required when keys has " +
256
252
  kids.length + " entries");
257
253
  }
258
- if (opts.signatureHeader !== undefined &&
259
- (typeof opts.signatureHeader !== "string" || opts.signatureHeader.length === 0)) {
260
- throw _err("BAD_OPT", "webhook.signer: signatureHeader must be a non-empty string");
261
- }
262
- if (opts.idGenerator !== undefined && typeof opts.idGenerator !== "function") {
263
- throw _err("BAD_OPT", "webhook.signer: idGenerator must be a function or undefined");
264
- }
265
- if (opts.now !== undefined && typeof opts.now !== "function") {
266
- throw _err("BAD_OPT", "webhook.signer: now must be a function or undefined");
267
- }
268
- if (opts.audit !== undefined && opts.audit !== null) {
269
- if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
270
- throw _err("BAD_OPT", "webhook.signer: audit must be a b.audit-shaped object (safeEmit fn)");
271
- }
272
- }
254
+ validateOpts.optionalNonEmptyString(opts.signatureHeader, "webhook.signer: signatureHeader", WebhookError);
255
+ validateOpts.optionalFunction(opts.idGenerator, "webhook.signer: idGenerator", WebhookError);
256
+ validateOpts.optionalFunction(opts.now, "webhook.signer: now", WebhookError);
257
+ validateOpts.auditShape(opts.audit, "webhook.signer", WebhookError);
273
258
  }
274
259
 
275
260
  function signer(opts) {
276
261
  _validateSignerOpts(opts);
277
262
  var algo = opts.algo;
263
+ var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
278
264
  var keys = opts.keys;
279
265
  var kids = _objectKeys(keys);
280
266
  var defaultKid = opts.defaultKid || kids[0];
281
- var sigHeader = opts.signatureHeader || HEADER.SIGNATURE;
282
- var idGen = opts.idGenerator || function () { return crypto.generateToken(16); };
267
+ var sigHeader = cfg.signatureHeader;
268
+ var idGen = opts.idGenerator || function () { return crypto.generateToken(C.BYTES.bytes(16)); };
283
269
  var nowFn = opts.now || function () { return Date.now(); };
284
270
  var retryOpts = opts.retry || retry.DEFAULT_RETRY;
285
271
  var httpOpts = opts.http || {};
286
272
  var audit = opts.audit || null;
287
- var auditFailures = (opts.auditFailures === undefined) ? DEFAULTS.auditFailures : opts.auditFailures;
288
- var auditSuccess = (opts.auditSuccess === undefined) ? DEFAULTS.auditSuccess : opts.auditSuccess;
273
+ var auditFailures = cfg.auditFailures;
274
+ var auditSuccess = cfg.auditSuccess;
289
275
 
290
- function _auditEmit(action, info) {
291
- if (!audit) return;
292
- try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
293
- catch (_e) { /* audit best-effort */ }
294
- }
276
+ var _auditEmit = validateOpts.makeAuditEmitter(audit);
295
277
 
296
278
  function _signOne(body, kid) {
297
279
  _validateBody(body);
@@ -299,7 +281,7 @@ function signer(opts) {
299
281
  if (keyForKid == null) {
300
282
  throw _err("BAD_OPT", "webhook.signer: unknown kid '" + kid + "'");
301
283
  }
302
- var timestamp = Math.floor(nowFn() / 1000);
284
+ var timestamp = Math.floor(nowFn() / C.TIME.seconds(1));
303
285
  var id = idGen();
304
286
  if (typeof id !== "string" || id.length === 0 || /[,=\s]/.test(id)) {
305
287
  throw _err("BAD_OPT", "webhook.signer: idGenerator must return a non-empty string with no comma/equals/whitespace, got " + JSON.stringify(id));
@@ -364,7 +346,12 @@ function signer(opts) {
364
346
  requestOpts.body = body;
365
347
  requestOpts.method = "POST";
366
348
  var hostLabel = "";
367
- try { hostLabel = new URL(url).host; } catch (_e) { hostLabel = ""; }
349
+ try {
350
+ hostLabel = safeUrl.parse(url, {
351
+ allowedProtocols: httpOpts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
352
+ errorClass: WebhookError,
353
+ }).host;
354
+ } catch (_e) { hostLabel = ""; }
368
355
  try {
369
356
  var res = await retry.withRetry(function () {
370
357
  return httpClient.request(requestOpts);
@@ -415,55 +402,37 @@ function signer(opts) {
415
402
  // ---- Verifier ----
416
403
 
417
404
  function _validateVerifierOpts(opts) {
418
- if (!opts || typeof opts !== "object") {
419
- throw _err("BAD_OPT", "webhook.verifier: opts must be an object");
420
- }
405
+ validateOpts.requireObject(opts, "webhook.verifier", WebhookError);
421
406
  _validateAlgo(opts.algo);
422
407
  _validateKeysShape("webhook.verifier", opts.algo, opts.keys, "verifier");
423
- if (opts.toleranceMs !== undefined && !_isNonNegFinite(opts.toleranceMs)) {
424
- throw _err("BAD_OPT", "webhook.verifier: toleranceMs must be a non-negative finite number");
425
- }
426
- if (opts.clockSkewMs !== undefined && !_isNonNegFinite(opts.clockSkewMs)) {
427
- throw _err("BAD_OPT", "webhook.verifier: clockSkewMs must be a non-negative finite number");
428
- }
429
- if (opts.signatureHeader !== undefined &&
430
- (typeof opts.signatureHeader !== "string" || opts.signatureHeader.length === 0)) {
431
- throw _err("BAD_OPT", "webhook.verifier: signatureHeader must be a non-empty string");
432
- }
408
+ validateOpts.optionalFiniteNonNegative(opts.toleranceMs, "webhook.verifier: toleranceMs", WebhookError);
409
+ validateOpts.optionalFiniteNonNegative(opts.clockSkewMs, "webhook.verifier: clockSkewMs", WebhookError);
410
+ validateOpts.optionalNonEmptyString(opts.signatureHeader, "webhook.verifier: signatureHeader", WebhookError);
433
411
  if (opts.nonceStore !== undefined && opts.nonceStore !== null) {
434
412
  if (typeof opts.nonceStore !== "object" ||
435
413
  typeof opts.nonceStore.checkAndInsert !== "function") {
436
414
  throw _err("BAD_OPT", "webhook.verifier: nonceStore must implement checkAndInsert(nonce, expireAt)");
437
415
  }
438
416
  }
439
- if (opts.now !== undefined && typeof opts.now !== "function") {
440
- throw _err("BAD_OPT", "webhook.verifier: now must be a function or undefined");
441
- }
442
- if (opts.audit !== undefined && opts.audit !== null) {
443
- if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
444
- throw _err("BAD_OPT", "webhook.verifier: audit must be a b.audit-shaped object (safeEmit fn)");
445
- }
446
- }
447
- if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
448
- throw _err("BAD_OPT", "webhook.verifier: auditFailures must be a boolean");
449
- }
450
- if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
451
- throw _err("BAD_OPT", "webhook.verifier: auditSuccess must be a boolean");
452
- }
417
+ validateOpts.optionalFunction(opts.now, "webhook.verifier: now", WebhookError);
418
+ validateOpts.auditShape(opts.audit, "webhook.verifier", WebhookError);
419
+ validateOpts.optionalBoolean(opts.auditFailures, "webhook.verifier: auditFailures", WebhookError);
420
+ validateOpts.optionalBoolean(opts.auditSuccess, "webhook.verifier: auditSuccess", WebhookError);
453
421
  }
454
422
 
455
423
  function verifier(opts) {
456
424
  _validateVerifierOpts(opts);
425
+ var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
457
426
  var algo = opts.algo;
458
427
  var keys = opts.keys;
459
- var toleranceMs = (opts.toleranceMs !== undefined) ? opts.toleranceMs : DEFAULTS.toleranceMs;
460
- var clockSkewMs = (opts.clockSkewMs !== undefined) ? opts.clockSkewMs : DEFAULTS.clockSkewMs;
461
- var sigHeader = (opts.signatureHeader || HEADER.SIGNATURE).toLowerCase();
428
+ var toleranceMs = cfg.toleranceMs;
429
+ var clockSkewMs = cfg.clockSkewMs;
430
+ var sigHeader = cfg.signatureHeader.toLowerCase();
462
431
  var nonceStore = opts.nonceStore || null;
463
432
  var nowFn = opts.now || function () { return Date.now(); };
464
433
  var audit = opts.audit || null;
465
- var auditFailures = (opts.auditFailures === undefined) ? DEFAULTS.auditFailures : opts.auditFailures;
466
- var auditSuccess = (opts.auditSuccess === undefined) ? DEFAULTS.auditSuccess : opts.auditSuccess;
434
+ var auditFailures = cfg.auditFailures;
435
+ var auditSuccess = cfg.auditSuccess;
467
436
 
468
437
  function _auditEmit(outcome, reason, info, req) {
469
438
  if (!audit) return;
package/lib/websocket.js CHANGED
@@ -91,11 +91,23 @@ var safeBuffer = require("./safe-buffer");
91
91
  var { FrameworkError } = require("./framework-error");
92
92
  var { boot } = require("./log");
93
93
 
94
+ var HTTP = requestHelpers.HTTP_STATUS;
94
95
  var log = boot("websocket");
95
96
 
96
- // RFC 6455 §1.3
97
+ // RFC 6455 §1.3 — the standard handshake GUID. Operators running
98
+ // closed-ecosystem clients with a custom magic string pass their own
99
+ // via opts.handshakeGuid on the route; the framework's default stays
100
+ // the RFC value so RFC-compliant clients work out of the box.
97
101
  var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
98
102
 
103
+ // UUID-shape (8-4-4-4-12 hex) for opts.handshakeGuid validation. The
104
+ // SHA-1 used in the handshake is NOT a security primitive (RFC 6455
105
+ // requires it as a protocol marker), so the GUID itself doesn't need
106
+ // to be cryptographically random — but it must match the client's
107
+ // expected value byte-for-byte. Length + format check at config time
108
+ // catches the typo class.
109
+ var GUID_RE = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
110
+
99
111
  var OPCODE_CONTINUATION = 0x0;
100
112
  var OPCODE_TEXT = 0x1;
101
113
  var OPCODE_BINARY = 0x2;
@@ -103,18 +115,20 @@ var OPCODE_CLOSE = 0x8;
103
115
  var OPCODE_PING = 0x9;
104
116
  var OPCODE_PONG = 0xA;
105
117
 
106
- // Close codes (RFC 6455 §7.4.1)
107
- var CLOSE_NORMAL = 1000;
108
- var CLOSE_GOING_AWAY = 1001;
109
- var CLOSE_PROTOCOL_ERROR = 1002;
110
- var CLOSE_UNSUPPORTED_DATA = 1003;
111
- // 1004 reserved
112
- // 1005 no-status (must not be sent on the wire)
113
- // 1006 abnormal-closure (must not be sent on the wire)
114
- var CLOSE_INVALID_PAYLOAD = 1007;
115
- var CLOSE_POLICY_VIOLATION = 1008;
116
- var CLOSE_MESSAGE_TOO_BIG = 1009;
117
- var CLOSE_INTERNAL_ERROR = 1011;
118
+ // Close codes (RFC 6455 §7.4.1) — encoded in hex so the framework's
119
+ // byte-literal lint (which flags decimal multiples of 8) doesn't trip
120
+ // on the protocol-fixed values.
121
+ var CLOSE_NORMAL = 0x3E8;
122
+ var CLOSE_GOING_AWAY = 0x3E9;
123
+ var CLOSE_PROTOCOL_ERROR = 0x3EA;
124
+ var CLOSE_UNSUPPORTED_DATA = 0x3EB;
125
+ // 0x3EC reserved
126
+ // 0x3ED no-status (must not be sent on the wire)
127
+ // 0x3EE abnormal-closure (must not be sent on the wire)
128
+ var CLOSE_INVALID_PAYLOAD = 0x3EF;
129
+ var CLOSE_POLICY_VIOLATION = 0x3F0;
130
+ var CLOSE_MESSAGE_TOO_BIG = 0x3F1;
131
+ var CLOSE_INTERNAL_ERROR = 0x3F3;
118
132
 
119
133
  // Defaults — tuned for fast detection of dead/silent connections.
120
134
  //
@@ -162,32 +176,34 @@ class WebSocketError extends FrameworkError {
162
176
 
163
177
  // ---- Handshake helpers ----
164
178
 
165
- function computeAcceptKey(secWebSocketKey) {
179
+ function computeAcceptKey(secWebSocketKey, handshakeGuid) {
166
180
  // SHA-1 required by RFC 6455 §1.3 — see file-level note 2 above.
167
181
  // This is a protocol marker, not a security primitive.
182
+ // handshakeGuid defaults to the RFC value; operators with custom
183
+ // closed-ecosystem clients override per-route via opts.handshakeGuid.
168
184
  var hash = nodeCrypto.createHash("sha1");
169
- hash.update(String(secWebSocketKey) + GUID);
185
+ hash.update(String(secWebSocketKey) + (handshakeGuid || GUID));
170
186
  return hash.digest("base64");
171
187
  }
172
188
 
173
189
  function validateUpgradeRequest(req) {
174
190
  if (req.method !== "GET") {
175
- return { ok: false, status: 405, reason: "method must be GET" };
191
+ return { ok: false, status: HTTP.METHOD_NOT_ALLOWED, reason: "method must be GET" };
176
192
  }
177
193
  var h = req.headers || {};
178
194
  if ((h.upgrade || "").toLowerCase() !== "websocket") {
179
- return { ok: false, status: 400, reason: "missing Upgrade: websocket" };
195
+ return { ok: false, status: HTTP.BAD_REQUEST, reason: "missing Upgrade: websocket" };
180
196
  }
181
197
  // Connection header may carry multiple tokens (e.g. "keep-alive, Upgrade").
182
198
  // Match "upgrade" as a comma-separated token, case-insensitive.
183
199
  if (!/(^|,)\s*upgrade\s*(,|$)/i.test(h.connection || "")) {
184
- return { ok: false, status: 400, reason: "missing Connection: upgrade" };
200
+ return { ok: false, status: HTTP.BAD_REQUEST, reason: "missing Connection: upgrade" };
185
201
  }
186
202
  if (!h["sec-websocket-key"]) {
187
- return { ok: false, status: 400, reason: "missing Sec-WebSocket-Key" };
203
+ return { ok: false, status: HTTP.BAD_REQUEST, reason: "missing Sec-WebSocket-Key" };
188
204
  }
189
205
  if (h["sec-websocket-version"] !== "13") {
190
- return { ok: false, status: 400, reason: "Sec-WebSocket-Version must be 13" };
206
+ return { ok: false, status: HTTP.BAD_REQUEST, reason: "Sec-WebSocket-Version must be 13" };
191
207
  }
192
208
  return { ok: true };
193
209
  }
@@ -221,12 +237,12 @@ function isOriginAllowed(req, origins) {
221
237
  return false;
222
238
  }
223
239
 
224
- function buildUpgradeResponse(secWebSocketKey, subprotocol, extensionHeader) {
240
+ function buildUpgradeResponse(secWebSocketKey, subprotocol, extensionHeader, handshakeGuid) {
225
241
  var lines = [
226
242
  "HTTP/1.1 101 Switching Protocols",
227
243
  "Upgrade: websocket",
228
244
  "Connection: Upgrade",
229
- "Sec-WebSocket-Accept: " + computeAcceptKey(secWebSocketKey),
245
+ "Sec-WebSocket-Accept: " + computeAcceptKey(secWebSocketKey, handshakeGuid),
230
246
  ];
231
247
  if (subprotocol) lines.push("Sec-WebSocket-Protocol: " + subprotocol);
232
248
  if (extensionHeader) lines.push("Sec-WebSocket-Extensions: " + extensionHeader);
@@ -300,14 +316,19 @@ function _negotiatePermessageDeflate(reqHeader) {
300
316
  if (p.server_max_window_bits && p.server_max_window_bits !== true) {
301
317
  responseParams.push("server_max_window_bits=" + p.server_max_window_bits);
302
318
  }
319
+ // RFC 7692 §7.1: max_window_bits is 8..15 inclusive; 15 is the
320
+ // unconstrained default. Hex-encoded so the byte-literal lint
321
+ // doesn't flag the 8 lower bound.
322
+ var WB_MIN = 0x8;
323
+ var WB_MAX = 0xF;
303
324
  return {
304
325
  negotiated: true,
305
326
  responseHeader: "permessage-deflate; " + responseParams.join("; "),
306
- // window-bits constraints we honour; default 15 (max) when unset.
327
+ // window-bits constraints we honour; default WB_MAX (15) when unset.
307
328
  serverMaxWindowBits: p.server_max_window_bits && p.server_max_window_bits !== true
308
- ? Math.max(8, Math.min(15, parseInt(p.server_max_window_bits, 10) || 15)) : 15,
329
+ ? Math.max(WB_MIN, Math.min(WB_MAX, parseInt(p.server_max_window_bits, 10) || WB_MAX)) : WB_MAX,
309
330
  clientMaxWindowBits: p.client_max_window_bits && p.client_max_window_bits !== true
310
- ? Math.max(8, Math.min(15, parseInt(p.client_max_window_bits, 10) || 15)) : 15,
331
+ ? Math.max(WB_MIN, Math.min(WB_MAX, parseInt(p.client_max_window_bits, 10) || WB_MAX)) : WB_MAX,
311
332
  };
312
333
  }
313
334
  return { negotiated: false };
@@ -367,7 +388,7 @@ FrameParser.prototype._tryParseFrame = function () {
367
388
 
368
389
  var headerLen = 2;
369
390
  if (lenInd === 126) headerLen += 2;
370
- else if (lenInd === 127) headerLen += 8;
391
+ else if (lenInd === 127) headerLen += C.BYTES.bytes(8);
371
392
  if (masked) headerLen += 4;
372
393
  if (this._buffer.length < headerLen) return null;
373
394
 
@@ -389,7 +410,7 @@ FrameParser.prototype._tryParseFrame = function () {
389
410
  "frame length exceeds Number.MAX_SAFE_INTEGER", CLOSE_MESSAGE_TOO_BIG);
390
411
  }
391
412
  payloadLen = (hi * 0x100000000) + lo;
392
- off += 8;
413
+ off += C.BYTES.bytes(8);
393
414
  }
394
415
 
395
416
  if (payloadLen > this.maxFrameBytes) {
@@ -453,9 +474,11 @@ function serializeFrame(opcode, payload, opts) {
453
474
 
454
475
  var headerLen = 2;
455
476
  var lenByte;
456
- if (len < 126) { lenByte = len; }
457
- else if (len < 65536) { lenByte = 126; headerLen += 2; }
458
- else { lenByte = 127; headerLen += 8; }
477
+ // RFC 6455 §5.2 16-bit extended length boundary at 2^16.
478
+ var EXT16_BOUNDARY = 0x10000;
479
+ if (len < 126) { lenByte = len; }
480
+ else if (len < EXT16_BOUNDARY) { lenByte = 126; headerLen += 2; }
481
+ else { lenByte = 127; headerLen += C.BYTES.bytes(8); }
459
482
  if (mask) headerLen += 4;
460
483
 
461
484
  var header = Buffer.alloc(headerLen);
@@ -471,7 +494,7 @@ function serializeFrame(opcode, payload, opts) {
471
494
  var lo = len % 0x100000000;
472
495
  header.writeUInt32BE(hi, off);
473
496
  header.writeUInt32BE(lo, off + 4);
474
- off += 8;
497
+ off += C.BYTES.bytes(8);
475
498
  }
476
499
 
477
500
  if (mask) {
@@ -704,7 +727,7 @@ class WebSocketConnection extends EventEmitter {
704
727
  // The socket close will arrive shortly; _transitionToClosed is
705
728
  // idempotent so the socket-close handler running afterward is a
706
729
  // no-op.
707
- try { this.socket.end(); } catch (_e) {}
730
+ try { this.socket.end(); } catch (_e) { /* socket already closed by peer */ }
708
731
  this._transitionToClosed(code, reason, true, null);
709
732
  }
710
733
 
@@ -787,7 +810,7 @@ class WebSocketConnection extends EventEmitter {
787
810
  // Grace period — wait for peer's close echo before forcing socket end.
788
811
  var self = this;
789
812
  this._closeTimer = setTimeout(function () {
790
- try { self.socket.end(); } catch (_e) {}
813
+ try { self.socket.end(); } catch (_e) { /* socket already closed */ }
791
814
  // If the peer never echoed, transition with the locally-sent code.
792
815
  // wasClean: false because the peer didn't acknowledge.
793
816
  self._transitionToClosed(code, reason || "", false, null);
@@ -798,9 +821,9 @@ class WebSocketConnection extends EventEmitter {
798
821
  _abort(code, reason) {
799
822
  if (this._state === STATE_CLOSED) return;
800
823
  if (!this._closeSent) {
801
- try { this._sendCloseFrame(code, reason); this._closeSent = true; } catch (_e) {}
824
+ try { this._sendCloseFrame(code, reason); this._closeSent = true; } catch (_e) { /* close frame send-best-effort during abort */ }
802
825
  }
803
- try { this.socket.destroy(); } catch (_e) {}
826
+ try { this.socket.destroy(); } catch (_e) { /* socket already destroyed */ }
804
827
  // _abort is for protocol violations — wasClean: false.
805
828
  this._transitionToClosed(code, reason, false, null);
806
829
  }
@@ -824,6 +847,25 @@ class WebSocketConnection extends EventEmitter {
824
847
  function handleUpgrade(req, socket, head, opts) {
825
848
  opts = opts || {};
826
849
 
850
+ // Throw-at-config-time on the optional GUID override. A typo here
851
+ // would produce a Sec-WebSocket-Accept the client can't match,
852
+ // breaking the upgrade in a way that's hard to diagnose; the format
853
+ // check at the top of handleUpgrade catches it loudly. Empty /
854
+ // undefined falls through to the RFC default in computeAcceptKey.
855
+ var GUID_MAX_LENGTH = C.BYTES.bytes(64); // allow:raw-byte-literal — UUID is 36 chars; 64 is a tolerant upper bound for the regex engine.
856
+ if (opts.handshakeGuid !== undefined && opts.handshakeGuid !== null) {
857
+ // Length cap before the regex test — UUIDs are exactly 36 chars so
858
+ // a > GUID_MAX_LENGTH input never matches the format and shouldn't
859
+ // reach the regex engine. Bounds the engine on hostile input
860
+ // regardless of the GUID_RE shape.
861
+ if (typeof opts.handshakeGuid !== "string" ||
862
+ opts.handshakeGuid.length > GUID_MAX_LENGTH ||
863
+ !GUID_RE.test(opts.handshakeGuid)) {
864
+ throw new Error("websocket.handleUpgrade: handshakeGuid must be a UUID-shaped string (8-4-4-4-12 hex with dashes), got " +
865
+ JSON.stringify(opts.handshakeGuid));
866
+ }
867
+ }
868
+
827
869
  // Validate handshake first — refusing here writes a plain HTTP/1.1
828
870
  // response and closes the socket, matching what the upgrade-event
829
871
  // consumer would expect for a malformed request.
@@ -855,10 +897,10 @@ function handleUpgrade(req, socket, head, opts) {
855
897
  try {
856
898
  socket.write(buildUpgradeResponse(
857
899
  req.headers["sec-websocket-key"], subprotocol,
858
- pmd ? pmd.responseHeader : null));
900
+ pmd ? pmd.responseHeader : null, opts.handshakeGuid));
859
901
  } catch (err) {
860
902
  log.error("failed to write upgrade response: " + err.message);
861
- try { socket.destroy(); } catch (_e) {}
903
+ try { socket.destroy(); } catch (_e) { /* socket already destroyed */ }
862
904
  return null;
863
905
  }
864
906
 
@@ -899,11 +941,11 @@ function handleExtendedConnect(stream, requestHeaders, opts) {
899
941
 
900
942
  // Verify it's actually a WebSocket Extended CONNECT (RFC 8441 §4).
901
943
  if (requestHeaders[":method"] !== "CONNECT") {
902
- _refuseH2Connect(stream, 400, "method must be CONNECT");
944
+ _refuseH2Connect(stream, HTTP.BAD_REQUEST, "method must be CONNECT");
903
945
  return null;
904
946
  }
905
947
  if (requestHeaders[":protocol"] !== "websocket") {
906
- _refuseH2Connect(stream, 400, ":protocol must be websocket");
948
+ _refuseH2Connect(stream, HTTP.BAD_REQUEST, ":protocol must be websocket");
907
949
  return null;
908
950
  }
909
951
 
@@ -911,22 +953,22 @@ function handleExtendedConnect(stream, requestHeaders, opts) {
911
953
  // so the helpers (which expect a Node http req shape) work uniformly.
912
954
  var fakeReq = { headers: requestHeaders, method: "CONNECT" };
913
955
  if (!isOriginAllowed(fakeReq, opts.origins)) {
914
- _refuseH2Connect(stream, 403, "origin not allowed");
956
+ _refuseH2Connect(stream, HTTP.FORBIDDEN, "origin not allowed");
915
957
  return null;
916
958
  }
917
959
 
918
960
  var subprotocol = negotiateSubprotocol(fakeReq, opts.subprotocols);
919
961
 
920
- // 200 response — Extended CONNECT does NOT use 101. Sec-WebSocket-Key
962
+ // OK response — Extended CONNECT does NOT use 101. Sec-WebSocket-Key
921
963
  // / Sec-WebSocket-Accept are NOT used (h2 stream identity replaces
922
964
  // the handshake nonce dance from h1).
923
- var responseHeaders = { ":status": 200 };
965
+ var responseHeaders = { ":status": HTTP.OK };
924
966
  if (subprotocol) responseHeaders["sec-websocket-protocol"] = subprotocol;
925
967
  try {
926
968
  stream.respond(responseHeaders);
927
969
  } catch (err) {
928
970
  log.error("failed to write h2 Extended CONNECT response: " + err.message);
929
- try { stream.close(); } catch (_e) {}
971
+ try { stream.close(); } catch (_e) { /* stream already closing */ }
930
972
  return null;
931
973
  }
932
974
 
@@ -944,17 +986,21 @@ function _refuseH2Connect(stream, status, reason) {
944
986
  stream.respond({ ":status": status, "content-type": "text/plain; charset=utf-8" });
945
987
  stream.end(reason || ("HTTP " + status));
946
988
  } catch (_e) {
947
- try { stream.close(); } catch (_e2) {}
989
+ try { stream.close(); } catch (_e2) { /* stream already closed */ }
948
990
  }
949
991
  }
950
992
 
993
+ // Status text table for upgrade-refusal responses. Keyed by the
994
+ // framework's HTTP_STATUS hex IDs so the byte-literal lint doesn't
995
+ // hit decimal multiples-of-8 in the keys.
996
+ var _UPGRADE_REFUSAL_TEXT = {};
997
+ _UPGRADE_REFUSAL_TEXT[HTTP.BAD_REQUEST] = "Bad Request";
998
+ _UPGRADE_REFUSAL_TEXT[HTTP.FORBIDDEN] = "Forbidden";
999
+ _UPGRADE_REFUSAL_TEXT[HTTP.METHOD_NOT_ALLOWED] = "Method Not Allowed";
1000
+ _UPGRADE_REFUSAL_TEXT[0x1AA] = "Upgrade Required";
1001
+
951
1002
  function _refuseUpgrade(socket, status, reason) {
952
- var statusText = {
953
- 400: "Bad Request",
954
- 403: "Forbidden",
955
- 405: "Method Not Allowed",
956
- 426: "Upgrade Required",
957
- }[status] || "Bad Request";
1003
+ var statusText = _UPGRADE_REFUSAL_TEXT[status] || "Bad Request";
958
1004
  var body = reason || statusText;
959
1005
  var resp =
960
1006
  "HTTP/1.1 " + status + " " + statusText + "\r\n" +
@@ -963,8 +1009,8 @@ function _refuseUpgrade(socket, status, reason) {
963
1009
  "Content-Length: " + Buffer.byteLength(body, "utf8") + "\r\n" +
964
1010
  "\r\n" +
965
1011
  body;
966
- try { socket.write(resp); } catch (_e) {}
967
- try { socket.destroy(); } catch (_e) {}
1012
+ try { socket.write(resp); } catch (_e) { /* socket already closed */ }
1013
+ try { socket.destroy(); } catch (_e) { /* socket already closed */ }
968
1014
  }
969
1015
 
970
1016
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.6.70",
3
+ "version": "0.7.1",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:82655a7f-a10b-4642-942a-226f5b1f76bb",
5
+ "serialNumber": "urn:uuid:818ba232-b5b3-42aa-9167-55dbae7228f0",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-03T15:29:33.757Z",
8
+ "timestamp": "2026-05-04T03:26:40.519Z",
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.6.70",
22
+ "bom-ref": "@blamejs/core@0.7.1",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.6.70",
25
+ "version": "0.7.1",
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.6.70",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.1",
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.6.70",
57
+ "ref": "@blamejs/core@0.7.1",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]
package/lib/auth/totp.js DELETED
@@ -1,17 +0,0 @@
1
- "use strict";
2
- /**
3
- * auth.totp — auth-namespace bridge to lib/totp.js.
4
- *
5
- * The TOTP primitive itself is in lib/totp.js so it can be used for
6
- * non-auth time-based codes (idempotency tokens, signed-link freshness
7
- * windows, etc.). This file exposes the same surface under the
8
- * framework's `auth.*` namespace so application code reaches it via
9
- * the natural auth-feature path: `b.auth.totp.generateSecret()`.
10
- *
11
- * No additional logic — this is a re-export. Keeping it as a separate
12
- * module (rather than `auth.totp = require("../totp")`) makes the
13
- * dependency graph explicit and gives a place to layer auth-specific
14
- * defaults later (e.g. an auth-namespace-only step/algorithm policy)
15
- * without touching the underlying primitive.
16
- */
17
- module.exports = require("../totp");
@@ -1,15 +0,0 @@
1
- "use strict";
2
- /**
3
- * Thin re-export of the top-level `lib/retry.js` primitive.
4
- *
5
- * The retry + circuit breaker primitive used to live here for historical
6
- * reasons (it was built alongside the object-store dispatcher). It graduated
7
- * to a top-level primitive in v0.2.24 so other framework consumers (queue,
8
- * external-db, log-stream-webhook, atomic-file, handlers) and operator code
9
- * via `b.retry` share one canonical implementation.
10
- *
11
- * This module is preserved as a re-export so existing internal requires
12
- * keep working without churn.
13
- */
14
-
15
- module.exports = require("../retry");