@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.
- package/CHANGELOG.md +6 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +23 -26
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +4 -3
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +26 -15
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +98 -52
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- 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(
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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 =
|
|
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 =
|
|
288
|
-
var auditSuccess =
|
|
273
|
+
var auditFailures = cfg.auditFailures;
|
|
274
|
+
var auditSuccess = cfg.auditSuccess;
|
|
289
275
|
|
|
290
|
-
|
|
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() /
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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 =
|
|
460
|
-
var clockSkewMs =
|
|
461
|
-
var sigHeader =
|
|
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 =
|
|
466
|
-
var 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
|
-
|
|
108
|
-
|
|
109
|
-
var
|
|
110
|
-
var
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
var
|
|
117
|
-
var
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
//
|
|
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":
|
|
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
package/sbom.cyclonedx.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:
|
|
5
|
+
"serialNumber": "urn:uuid:818ba232-b5b3-42aa-9167-55dbae7228f0",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.1",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
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");
|