@blamejs/core 0.7.0 → 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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.7.x
10
10
 
11
+ - **0.7.1** (2026-05-04) — `b.websocket` route opts gain `handshakeGuid` to override the RFC 6455 §1.3 magic string used in the `Sec-WebSocket-Accept` derivation. Default stays `258EAFA5-E914-47DA-95CA-C5AB0DC85B11`. Operators with closed-ecosystem clients running their own GUID (typical for migrations from frameworks that customize the handshake to namespace their own client family) drop `handshakeGuid: "<their-uuid>"` into the `router.ws(path, handler, opts)` opts and clients keep working unchanged. Throws at upgrade time if the override is malformed — UUID-shape regex with a 64-char length cap before the regex test, so a typo produces a clear error instead of silently producing a `Sec-WebSocket-Accept` the client can't match. **Tests** — 3 new layer-0 assertions in `test/00-primitives.js` (custom GUID produces different accept key, empty / null falls back to RFC default, malformed handshakeGuid rejected at config time). Smoke 7338 → 7341 / wiki e2e 178 / Linux container smoke 7341 (145s) / eslint clean / shellcheck clean.
12
+
11
13
  - **0.7.0** (2026-05-04) — Codebase-patterns hardening sweep + primitive consolidation. The duplicate-block detector in `test/layer-0-primitives/codebase-patterns.test.js` ran at MIN_DISTINCT_FILES=3 and surfaced ~50 inline-shape clusters that had proliferated across lib/ — the kind of soft drift that's invisible at higher thresholds and hides re-introduction of bug classes the framework already swept once. **New primitive families** consolidated those clusters down: opts validation (`validateOpts.requireObject` / `auditShape` / `observabilityShape` / `applyDefaults` / `optional{Boolean,Function,PositiveInt,FiniteNonNegative,PositiveFinite,NonEmptyString}` / `requireNonEmptyString` / `makeAuditEmitter`), async coordination (`safeAsync.safeInvoke` / `makeDropCallback` / `makeScheduledFlush`), and SQL execution (`dbSchema.runInTransaction` / `runSqlOnHandle`). Plus `numericBounds.requireXFiniteIntIfPresent` for opt-time numeric-shape gates that throw via the caller's framework-error class, `log.makeViaOrFallback` for operator-log routing with per-module fallback, `observability.safeEvent` for hot-path drop-silent emission, `safeBuffer.HEX_RE`/`CRLF_RE`/`TRAILING_HSPACE_RE` regex constants + `isHex` / `hasCrlf` / `stripCrlf` / `stripTrailingHspace` helpers, `time.toIso8601NoMs`, `migrationFiles.MIGRATION_FILE_RE` / `isMigrationFileName` (shared filename grammar across migrations / seeders / external-db-migrate), and `lib/object-store/http-request.js` as a shared HTTP request helper across azure-blob / gcs / sigv4 / http-put. Roughly 80 inline call sites across 50+ files refactored to route through the new primitives; ~1500 lines of duplicate inline shapes eliminated. **Catalog gate** — `KNOWN_ANTIPATTERNS` in the codebase-patterns test now has 24 entries firing at n=1, so future code re-introducing any of the registered inline shapes (even one new file) fails the gate immediately. Pre-v0.7.0 the duplicate-block detector required n>=3 to fire; the catalog closes the gap by registering each extracted primitive's inline shape so it can't drift back in. **Cluster allowlist** — `KNOWN_CLUSTERS` allowlist (n>=3 detector) has 25 entries with documented structural reasons (parser error class signatures don't fit the framework's `(code, message)` contract; framework-convention shapes like middleware factories; future consolidation candidates). **Cleanup** — deleted dead re-export shims `lib/object-store/retry.js` (re-exported `lib/retry.js`) and `lib/auth/totp.js` (re-exported `lib/totp.js`); both fit the rule "pre-v1 frameworks have no operators to compatibly upgrade — every legacy fallback is dead code." Renamed `lib/internal-sha1-hibp.js` → `lib/framework-sha1-hibp.js` to match the lib naming convention (the `internal-` prefix wasn't in the convention's five-bucket list; `framework-` is the canonical "restricted-use" bucket alongside `framework-error.js` / `framework-schema.js`). **No operator-facing API breakage** — `b.objectStoreRetry` was removed from the public surface (operators use `b.retry` directly, which has been the canonical primitive since v0.2.24). **Eslint config tightened** — added `eqeqeq` (with the `null` exception for the `== null` null-or-undefined idiom), `no-throw-literal`, `no-promise-executor-return`, `default-case`, `no-loss-of-precision`. The previous config was hiding 11 real errors: 8 sites of `return resolve()` / `return done()` inside `new Promise(executor)` (return value silently discarded) across app / dev / http-client / mail / router / wiki/integration / 00-primitives, plus a missing default-case + 2 intentional template-language `==` / `!=` operators in template.js's binary-op evaluator (now allowed via inline `// eslint-disable-next-line eqeqeq -- template language operator`). Also adds `structuredClone` to the Node-globals list so db.js's deep-clone calls don't trip `no-undef`. **Release-workflow gate added** — Linux container smoke `docker run --rm -v "/$(pwd):/blamejs" -w //blamejs node:24-alpine node test/smoke.js` is now part of the release flow. Catches lingering-handle bugs that pass on Windows / macOS but hang or error on Linux CI. **Tests** — smoke 7338 / Linux container smoke 7338 (148s) / wiki e2e 178 / per-primitive integration 16 files / wiki integration green / eslint clean / shellcheck clean.
12
14
 
13
15
  ## v0.6.x
package/lib/websocket.js CHANGED
@@ -94,9 +94,20 @@ var { boot } = require("./log");
94
94
  var HTTP = requestHelpers.HTTP_STATUS;
95
95
  var log = boot("websocket");
96
96
 
97
- // 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.
98
101
  var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
99
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
+
100
111
  var OPCODE_CONTINUATION = 0x0;
101
112
  var OPCODE_TEXT = 0x1;
102
113
  var OPCODE_BINARY = 0x2;
@@ -165,11 +176,13 @@ class WebSocketError extends FrameworkError {
165
176
 
166
177
  // ---- Handshake helpers ----
167
178
 
168
- function computeAcceptKey(secWebSocketKey) {
179
+ function computeAcceptKey(secWebSocketKey, handshakeGuid) {
169
180
  // SHA-1 required by RFC 6455 §1.3 — see file-level note 2 above.
170
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.
171
184
  var hash = nodeCrypto.createHash("sha1");
172
- hash.update(String(secWebSocketKey) + GUID);
185
+ hash.update(String(secWebSocketKey) + (handshakeGuid || GUID));
173
186
  return hash.digest("base64");
174
187
  }
175
188
 
@@ -224,12 +237,12 @@ function isOriginAllowed(req, origins) {
224
237
  return false;
225
238
  }
226
239
 
227
- function buildUpgradeResponse(secWebSocketKey, subprotocol, extensionHeader) {
240
+ function buildUpgradeResponse(secWebSocketKey, subprotocol, extensionHeader, handshakeGuid) {
228
241
  var lines = [
229
242
  "HTTP/1.1 101 Switching Protocols",
230
243
  "Upgrade: websocket",
231
244
  "Connection: Upgrade",
232
- "Sec-WebSocket-Accept: " + computeAcceptKey(secWebSocketKey),
245
+ "Sec-WebSocket-Accept: " + computeAcceptKey(secWebSocketKey, handshakeGuid),
233
246
  ];
234
247
  if (subprotocol) lines.push("Sec-WebSocket-Protocol: " + subprotocol);
235
248
  if (extensionHeader) lines.push("Sec-WebSocket-Extensions: " + extensionHeader);
@@ -834,6 +847,25 @@ class WebSocketConnection extends EventEmitter {
834
847
  function handleUpgrade(req, socket, head, opts) {
835
848
  opts = opts || {};
836
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
+
837
869
  // Validate handshake first — refusing here writes a plain HTTP/1.1
838
870
  // response and closes the socket, matching what the upgrade-event
839
871
  // consumer would expect for a malformed request.
@@ -865,7 +897,7 @@ function handleUpgrade(req, socket, head, opts) {
865
897
  try {
866
898
  socket.write(buildUpgradeResponse(
867
899
  req.headers["sec-websocket-key"], subprotocol,
868
- pmd ? pmd.responseHeader : null));
900
+ pmd ? pmd.responseHeader : null, opts.handshakeGuid));
869
901
  } catch (err) {
870
902
  log.error("failed to write upgrade response: " + err.message);
871
903
  try { socket.destroy(); } catch (_e) { /* socket already destroyed */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.0",
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:4a5334cd-4c53-4ca4-abf6-713bbb9cfef6",
5
+ "serialNumber": "urn:uuid:818ba232-b5b3-42aa-9167-55dbae7228f0",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-04T02:46:52.648Z",
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.7.0",
22
+ "bom-ref": "@blamejs/core@0.7.1",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.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.7.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.7.0",
57
+ "ref": "@blamejs/core@0.7.1",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]