@blamejs/core 0.8.12 → 0.8.13
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 +2 -0
- package/lib/http-client.js +214 -33
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.8.x
|
|
10
10
|
|
|
11
|
+
- **0.8.13** (2026-05-07) — Streaming multipart uploads + `onChunk` response hook on `b.httpClient`. **Streaming multipart** — `b.httpClient.request({ multipart: { files: [...] } })` now accepts file entries in three shapes: the existing `{ field, content: Buffer | string }` (in-memory), plus new `{ field, filePath: string }` (stream-from-disk via `fs.createReadStream`) and `{ field, stream: Readable, size?: number }` (operator-supplied stream). When every entry's size is statically resolvable (Buffer length / `fs.statSync().size` / explicit `opts.size`), the framework sets `Content-Length` and uses identity transfer; otherwise the framework omits the header and Node's HTTP layer falls back to chunked transfer. Closes the `Buffer.concat` OOM class on large uploads — the body is materialized one chunk at a time through a `Readable.from(asyncIterator)` that yields boundary headers, source bytes, and CRLF in order. Fast-path preserved: when no streaming source is involved, `_buildMultipartBody` still returns a single Buffer with a known `Content-Length`. **`onChunk(chunk)` response hook** — fires for each response data chunk in BOTH `responseMode: "buffer"` and `responseMode: "stream"`. Use case: hash bytes during pipe-to-disk without an extra Transform pass (`onChunk: (c) => hasher.update(c)`). Throws inside the hook are caught and dropped — a hash-mismatch detector can raise without breaking the pipe; callers surface the error through their own pipe handler. Verified-already-shipped during the v0.8.x framework-gap audit: `b.httpClient` `responseMode: "always-resolve"`, `onRedirect({from, to, hop, headersStripped, statusCode})` hook, `body: Readable` upload path; `b.cryptoField` derived-hash domain separation (`bj-<table>-<field>:` per-field namespace prefix matches the indexed-lookup requirement); `b.config` `redactKeys` allowlist + `redacted()` view.
|
|
12
|
+
|
|
11
13
|
- **0.8.12** (2026-05-07) — WebSocket upgrade refuses credential-shaped query parameters by default. `validateUpgradeRequest(req, opts)` now scans the request URL for the credential-leak names `access_token`, `bearer`, `bearer_token`, `apikey`, `api_key`, `api-key`, `authorization` (case-insensitive, with percent-decoding) and refuses the upgrade with HTTP 400 when one is present. URL query strings leak through web-server access logs, browser history, the Referer header forwarded to third-party CDN / analytics, in-process / proxy log captures, and crash dumps — RFC 6750 §2.3 explicitly cautions against bearer tokens in URI query parameters for these reasons. Operators with a non-credential parameter that happens to share a credential-shaped name opt out per route via `opts.allowQueryAuthParams: true` with an audited operator reason. The refused list is deliberately narrow: overloaded names (`token`, `auth`, `key`, `session`) have non-credential meanings (CSRF tokens, file-share tokens, session-resume identifiers) and are NOT refused.
|
|
12
14
|
|
|
13
15
|
- **0.8.11** (2026-05-07) — Three new state-and-federal regulatory primitives + a per-primitive test-coverage gate. **`b.breach.deadline` + `b.breach.report`** — all-50-states data-breach-notification deadline registry. `b.breach.deadline.forStates(states, detectedAt)` returns per-state `{ state, kind, dueBy, citation }` records (`kind: "as-soon-as-possible"` for AS-OF / `"hard-deadline"` for fixed-day deadlines like Texas / Florida / Maine). `b.breach.report.create()` opens a multi-state breach with a single record, tracks per-state filings via `fileNotice(id, state, ...)`, exposes `pending(id)` for dashboards, and auto-closes once every affected state has filed. Every transition records a `breach.report.*` audit event. Statutory citations + day counts wired in `lib/breach-deadline.js` per-state. **`b.ai.adverseDecision`** — wraps an operator-supplied `decide(subject)` predicate, automatically attaches a consumer-rights notice when the outcome is `"adverse"` / `"denied"` / `"rejected"`. Built-in regulation templates for `gdpr-22` (Article 22 automated-decision rights), `ai-act-86` (EU AI Act high-risk consumer recourse), `ecoa-1002.9` (US Equal Credit Opportunity Act adverse-action notice), `colorado-ai-act` (CO SB 24-205 §6-1-1701), `nyc-ll-144` (NYC Local Law 144 employment AEDT), `fcra-615` (US FCRA adverse action), and `operator-defined`. Notice carries `principalReasons` + `consumerRights: { requestData, requestExplanation, contestDecision, requestHumanReview }` shaped per regime. **`b.middleware.ageGate`** — request-level age-classification middleware. Operator-supplied `getAge(req)` returns the subject age (or null/undefined when unknown); middleware classifies as `"above-threshold"` / `"below-threshold"` / `"unknown"` against `consentRequired`, sets `X-Privacy-Posture` header, and refuses with 451 + audited reason when `requireAge` is set and `hasParentalConsent(req)` is unmet. Composes upstream of session / authn for COPPA / AADC / UK Children's Code postures. **Per-primitive test-coverage gate** — new `test/layer-0-primitives/test-coverage.test.js` walks every operator-facing `b.*` primitive and refuses release unless the primitive has at least one test reference (or an explicit `UNTESTED_BACKLOG` entry naming the reason). Closes the drift class where a primitive landed on `b.*` but never gained a unit test.
|
package/lib/http-client.js
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
* maxResponseBytes, // for buffer mode (default 16 MiB control,
|
|
21
21
|
* // 1 GiB GET — operators with > 1 GiB
|
|
22
22
|
* // stored objects must use stream mode)
|
|
23
|
+
* onChunk, // (chunk: Buffer) => void — fires for each
|
|
24
|
+
* // response chunk in BOTH buffer and stream
|
|
25
|
+
* // modes. Use to hash bytes during pipe-to-disk
|
|
26
|
+
* // without an extra Transform pass.
|
|
23
27
|
* signal, // AbortSignal — propagated to req/stream
|
|
24
28
|
* errorClass, // FrameworkError subclass
|
|
25
29
|
* observer, // optional (stage, info) => void hook
|
|
@@ -426,20 +430,78 @@ function _attachJarCookie(headers, jar, url) {
|
|
|
426
430
|
// Mirrors the wire format that lib/middleware/body-parser.js's multipart
|
|
427
431
|
// parser accepts so round-trip from one blamejs app's outbound to
|
|
428
432
|
// another's inbound is exact.
|
|
433
|
+
//
|
|
434
|
+
// Two output shapes:
|
|
435
|
+
//
|
|
436
|
+
// - { boundary, body: Buffer, contentLength }
|
|
437
|
+
// When every file entry is a Buffer / string (size known
|
|
438
|
+
// up front) and no operator opted into streaming, the result
|
|
439
|
+
// is a fully-materialized body. Smaller payloads avoid the
|
|
440
|
+
// streaming overhead and let HTTP/1.1 KeepAlive reuse with a
|
|
441
|
+
// known Content-Length.
|
|
442
|
+
//
|
|
443
|
+
// - { boundary, body: Readable, contentLength }
|
|
444
|
+
// When at least one file entry is `{ filePath }` / `{ stream }`
|
|
445
|
+
// OR opts.streaming === true, the result is a Readable that
|
|
446
|
+
// emits boundary headers + content + CRLF in order. Avoids the
|
|
447
|
+
// Buffer.concat() OOM class on large uploads. contentLength is
|
|
448
|
+
// a finite number when every source's size is statically
|
|
449
|
+
// resolvable (Buffer length, fs.statSync().size, opts.size on
|
|
450
|
+
// a stream entry); null otherwise — caller falls back to
|
|
451
|
+
// chunked transfer.
|
|
452
|
+
//
|
|
453
|
+
// File entry shapes (all require `field`):
|
|
454
|
+
//
|
|
455
|
+
// { field, content: Buffer | string } — in-memory (existing)
|
|
456
|
+
// { field, filePath: string } — stream-from-disk
|
|
457
|
+
// { field, stream: Readable, size?: number } — operator-supplied stream
|
|
458
|
+
//
|
|
459
|
+
// `filename` and `contentType` apply to all three shapes; for
|
|
460
|
+
// `filePath` entries, `filename` defaults to path.basename(filePath).
|
|
429
461
|
function _buildMultipartBody(spec) {
|
|
430
462
|
var boundary = "----blamejs-mp-" + crypto.generateToken(C.BYTES.bytes(16));
|
|
431
463
|
var CRLF = "\r\n";
|
|
432
|
-
var
|
|
464
|
+
var fs = require("fs"); // allow:inline-require — only on multipart paths that touch the filesystem
|
|
465
|
+
var path = require("path"); // allow:inline-require — same
|
|
466
|
+
var nodeStream = require("stream"); // allow:inline-require — Readable subclass only when streaming
|
|
467
|
+
|
|
468
|
+
// Each entry is { headerBytes, source } where source is one of:
|
|
469
|
+
// { kind: "buffer", buf: Buffer }
|
|
470
|
+
// { kind: "filePath", filePath: string, size: number }
|
|
471
|
+
// { kind: "stream", stream: Readable, size: number | null }
|
|
472
|
+
var entries = [];
|
|
473
|
+
var anyStreaming = false;
|
|
474
|
+
var totalSize = 0;
|
|
475
|
+
var sizeKnown = true;
|
|
476
|
+
|
|
477
|
+
function _entryHeaderBytes(disposition, contentType) {
|
|
478
|
+
var head = "--" + boundary + CRLF + disposition + CRLF;
|
|
479
|
+
if (contentType) head += "Content-Type: " + contentType + CRLF;
|
|
480
|
+
head += CRLF;
|
|
481
|
+
return Buffer.from(head, "utf8");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function _addEntry(headerBytes, source) {
|
|
485
|
+
entries.push({ header: headerBytes, source: source });
|
|
486
|
+
totalSize += headerBytes.length;
|
|
487
|
+
if (source.kind === "buffer") {
|
|
488
|
+
totalSize += source.buf.length;
|
|
489
|
+
} else if (typeof source.size === "number" && isFinite(source.size) && source.size >= 0) {
|
|
490
|
+
totalSize += source.size;
|
|
491
|
+
} else {
|
|
492
|
+
sizeKnown = false;
|
|
493
|
+
}
|
|
494
|
+
totalSize += CRLF.length;
|
|
495
|
+
}
|
|
433
496
|
|
|
434
497
|
function _pushField(name, value) {
|
|
435
498
|
if (typeof name !== "string" || name.length === 0) {
|
|
436
499
|
throw new Error("multipart: field name must be a non-empty string");
|
|
437
500
|
}
|
|
438
|
-
var
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
parts.push(Buffer.from(CRLF, "utf8"));
|
|
501
|
+
var disposition = 'Content-Disposition: form-data; name="' + name + '"';
|
|
502
|
+
var head = _entryHeaderBytes(disposition, null);
|
|
503
|
+
var bodyBuf = Buffer.isBuffer(value) ? value : Buffer.from(String(value), "utf8");
|
|
504
|
+
_addEntry(head, { kind: "buffer", buf: bodyBuf });
|
|
443
505
|
}
|
|
444
506
|
|
|
445
507
|
function _pushFile(file) {
|
|
@@ -447,21 +509,50 @@ function _buildMultipartBody(spec) {
|
|
|
447
509
|
if (typeof file.field !== "string" || file.field.length === 0) {
|
|
448
510
|
throw new Error("multipart: file.field must be a non-empty string");
|
|
449
511
|
}
|
|
450
|
-
var
|
|
451
|
-
|
|
512
|
+
var hasContent = file.content !== undefined && file.content !== null;
|
|
513
|
+
var hasFilePath = typeof file.filePath === "string" && file.filePath.length > 0;
|
|
514
|
+
var hasStream = file.stream && typeof file.stream.pipe === "function";
|
|
515
|
+
var sourceCount = (hasContent ? 1 : 0) + (hasFilePath ? 1 : 0) + (hasStream ? 1 : 0);
|
|
516
|
+
if (sourceCount === 0) {
|
|
517
|
+
throw new Error("multipart: file entry requires one of { content, filePath, stream }");
|
|
518
|
+
}
|
|
519
|
+
if (sourceCount > 1) {
|
|
520
|
+
throw new Error("multipart: file entry must have exactly one of { content, filePath, stream }");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
var filename;
|
|
524
|
+
if (typeof file.filename === "string" && file.filename.length > 0) {
|
|
525
|
+
filename = file.filename;
|
|
526
|
+
} else if (hasFilePath) {
|
|
527
|
+
filename = path.basename(file.filePath);
|
|
528
|
+
} else {
|
|
529
|
+
filename = "blob";
|
|
530
|
+
}
|
|
452
531
|
var mimeType = file.contentType || file.mimeType || "application/octet-stream";
|
|
453
|
-
var
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
532
|
+
var disposition = 'Content-Disposition: form-data; name="' + file.field + '"' +
|
|
533
|
+
'; filename="' + filename.replace(/"/g, "%22") + '"';
|
|
534
|
+
var head = _entryHeaderBytes(disposition, mimeType);
|
|
535
|
+
|
|
536
|
+
if (hasContent) {
|
|
537
|
+
var content = file.content;
|
|
538
|
+
if (typeof content === "string") content = Buffer.from(content, "utf8");
|
|
539
|
+
if (!Buffer.isBuffer(content)) {
|
|
540
|
+
throw new Error("multipart: file.content must be a Buffer or string");
|
|
541
|
+
}
|
|
542
|
+
_addEntry(head, { kind: "buffer", buf: content });
|
|
543
|
+
} else if (hasFilePath) {
|
|
544
|
+
anyStreaming = true;
|
|
545
|
+
var st;
|
|
546
|
+
try { st = fs.statSync(file.filePath); }
|
|
547
|
+
catch (e) { throw new Error("multipart: file.filePath not readable: " + e.message); }
|
|
548
|
+
if (!st.isFile()) throw new Error("multipart: file.filePath is not a regular file");
|
|
549
|
+
_addEntry(head, { kind: "filePath", filePath: file.filePath, size: st.size });
|
|
550
|
+
} else {
|
|
551
|
+
anyStreaming = true;
|
|
552
|
+
var streamSize = (typeof file.size === "number" && isFinite(file.size) && file.size >= 0)
|
|
553
|
+
? file.size : null;
|
|
554
|
+
_addEntry(head, { kind: "stream", stream: file.stream, size: streamSize });
|
|
457
555
|
}
|
|
458
|
-
var head = "--" + boundary + CRLF +
|
|
459
|
-
'Content-Disposition: form-data; name="' + file.field + '"' +
|
|
460
|
-
'; filename="' + filename.replace(/"/g, "%22") + '"' + CRLF +
|
|
461
|
-
"Content-Type: " + mimeType + CRLF + CRLF;
|
|
462
|
-
parts.push(Buffer.from(head, "utf8"));
|
|
463
|
-
parts.push(content);
|
|
464
|
-
parts.push(Buffer.from(CRLF, "utf8"));
|
|
465
556
|
}
|
|
466
557
|
|
|
467
558
|
if (spec && spec.fields && typeof spec.fields === "object") {
|
|
@@ -479,8 +570,54 @@ function _buildMultipartBody(spec) {
|
|
|
479
570
|
if (spec && Array.isArray(spec.files)) {
|
|
480
571
|
for (var fi = 0; fi < spec.files.length; fi++) _pushFile(spec.files[fi]);
|
|
481
572
|
}
|
|
482
|
-
|
|
483
|
-
|
|
573
|
+
var trailer = Buffer.from("--" + boundary + "--" + CRLF, "utf8");
|
|
574
|
+
totalSize += trailer.length;
|
|
575
|
+
|
|
576
|
+
// All-buffer fast path — return a fully-materialized body when no
|
|
577
|
+
// streaming sources are involved AND the operator didn't ask for
|
|
578
|
+
// streaming explicitly. Existing callers that pass small in-memory
|
|
579
|
+
// payloads keep the buffer codepath.
|
|
580
|
+
if (!anyStreaming && !(spec && spec.streaming === true)) {
|
|
581
|
+
var parts = [];
|
|
582
|
+
for (var ei = 0; ei < entries.length; ei++) {
|
|
583
|
+
parts.push(entries[ei].header);
|
|
584
|
+
parts.push(entries[ei].source.buf);
|
|
585
|
+
parts.push(Buffer.from(CRLF, "utf8"));
|
|
586
|
+
}
|
|
587
|
+
parts.push(trailer);
|
|
588
|
+
return { boundary: boundary, body: Buffer.concat(parts), contentLength: totalSize };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Streaming path — produce a Readable from an async iterator that
|
|
592
|
+
// yields the bytes for each entry in order.
|
|
593
|
+
var crlfBuf = Buffer.from(CRLF, "utf8");
|
|
594
|
+
async function* _iter() {
|
|
595
|
+
for (var ix = 0; ix < entries.length; ix++) {
|
|
596
|
+
var entry = entries[ix];
|
|
597
|
+
yield entry.header;
|
|
598
|
+
if (entry.source.kind === "buffer") {
|
|
599
|
+
yield entry.source.buf;
|
|
600
|
+
} else if (entry.source.kind === "filePath") {
|
|
601
|
+
var rs = fs.createReadStream(entry.source.filePath);
|
|
602
|
+
try {
|
|
603
|
+
for await (var chunk of rs) yield chunk;
|
|
604
|
+
} finally {
|
|
605
|
+
try { rs.destroy(); } catch (_e) { /* best-effort cleanup */ }
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
// operator-supplied stream
|
|
609
|
+
for await (var chunk2 of entry.source.stream) yield chunk2;
|
|
610
|
+
}
|
|
611
|
+
yield crlfBuf;
|
|
612
|
+
}
|
|
613
|
+
yield trailer;
|
|
614
|
+
}
|
|
615
|
+
var body = nodeStream.Readable.from(_iter());
|
|
616
|
+
return {
|
|
617
|
+
boundary: boundary,
|
|
618
|
+
body: body,
|
|
619
|
+
contentLength: sizeKnown ? totalSize : null,
|
|
620
|
+
};
|
|
484
621
|
}
|
|
485
622
|
|
|
486
623
|
// Headers stripped on cross-origin redirect to defend against accidental
|
|
@@ -525,6 +662,10 @@ function request(opts) {
|
|
|
525
662
|
return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
|
|
526
663
|
"onDownloadProgress must be a function", true));
|
|
527
664
|
}
|
|
665
|
+
if (opts.onChunk !== undefined && typeof opts.onChunk !== "function") {
|
|
666
|
+
return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
|
|
667
|
+
"onChunk must be a function (chunk: Buffer) -> void", true));
|
|
668
|
+
}
|
|
528
669
|
if (opts.jar !== undefined && opts.jar !== null) {
|
|
529
670
|
if (typeof opts.jar !== "object" ||
|
|
530
671
|
typeof opts.jar.cookieHeaderFor !== "function" ||
|
|
@@ -567,13 +708,20 @@ function request(opts) {
|
|
|
567
708
|
catch (e) {
|
|
568
709
|
return Promise.reject(_makeError(opts.errorClass, "BAD_ARG", e.message, true));
|
|
569
710
|
}
|
|
711
|
+
var mpHeaders = Object.assign({}, opts.headers || {}, {
|
|
712
|
+
"Content-Type": "multipart/form-data; boundary=" + built.boundary,
|
|
713
|
+
});
|
|
714
|
+
// Content-Length is set when the framework can statically resolve
|
|
715
|
+
// every source's byte size. Otherwise the framework omits the
|
|
716
|
+
// header and Node's HTTP layer falls back to chunked transfer —
|
|
717
|
+
// valid HTTP/1.1, requires no operator opt-in.
|
|
718
|
+
if (typeof built.contentLength === "number" && isFinite(built.contentLength)) {
|
|
719
|
+
mpHeaders["Content-Length"] = String(built.contentLength);
|
|
720
|
+
}
|
|
570
721
|
opts = Object.assign({}, opts, {
|
|
571
|
-
method:
|
|
572
|
-
body:
|
|
573
|
-
headers:
|
|
574
|
-
"Content-Type": "multipart/form-data; boundary=" + built.boundary,
|
|
575
|
-
"Content-Length": String(built.body.length),
|
|
576
|
-
}),
|
|
722
|
+
method: opts.method || "POST",
|
|
723
|
+
body: built.body,
|
|
724
|
+
headers: mpHeaders,
|
|
577
725
|
multipart: undefined,
|
|
578
726
|
});
|
|
579
727
|
}
|
|
@@ -894,6 +1042,7 @@ function _requestH1(transport, u, opts) {
|
|
|
894
1042
|
|
|
895
1043
|
var onUploadProgress = typeof opts.onUploadProgress === "function" ? opts.onUploadProgress : null;
|
|
896
1044
|
var onDownloadProgress = typeof opts.onDownloadProgress === "function" ? opts.onDownloadProgress : null;
|
|
1045
|
+
var onChunk = typeof opts.onChunk === "function" ? opts.onChunk : null;
|
|
897
1046
|
|
|
898
1047
|
var req = transport.lib.request(reqOpts, function (res) {
|
|
899
1048
|
if (observer) observer("response:headers", { statusCode: res.statusCode, headers: res.headers });
|
|
@@ -927,13 +1076,24 @@ function _requestH1(transport, u, opts) {
|
|
|
927
1076
|
"HTTP " + res.statusCode + " " + (res.statusMessage || ""),
|
|
928
1077
|
_isPermanentStatus(res.statusCode), res.statusCode));
|
|
929
1078
|
}
|
|
930
|
-
if (onDownloadProgress) {
|
|
931
|
-
// Wrap the stream so chunks emit progress to the
|
|
932
|
-
// The framework's contract is to hand back the
|
|
933
|
-
// unmodified; fix-up via a passthrough keeps
|
|
934
|
-
// observing the chunk sizes.
|
|
1079
|
+
if (onDownloadProgress || onChunk) {
|
|
1080
|
+
// Wrap the stream so chunks emit progress + onChunk to the
|
|
1081
|
+
// operator. The framework's contract is to hand back the
|
|
1082
|
+
// response stream unmodified; fix-up via a passthrough keeps
|
|
1083
|
+
// that contract while observing the chunk sizes. onChunk
|
|
1084
|
+
// gets the buffer itself (for hash-as-you-go); a throw from
|
|
1085
|
+
// it is caught and dropped so a hash-mismatch detector can
|
|
1086
|
+
// raise without breaking the response stream — caller
|
|
1087
|
+
// surfaces the error through their own pipe handler.
|
|
935
1088
|
var passthrough = new nodeStream.PassThrough();
|
|
936
|
-
res.on("data", function (chunk) {
|
|
1089
|
+
res.on("data", function (chunk) {
|
|
1090
|
+
_emitDownload(chunk.length);
|
|
1091
|
+
if (onChunk) {
|
|
1092
|
+
try { onChunk(chunk); }
|
|
1093
|
+
catch (_e) { /* operator-supplied hook — drop-silent */ }
|
|
1094
|
+
}
|
|
1095
|
+
passthrough.write(chunk);
|
|
1096
|
+
});
|
|
937
1097
|
res.on("end", function () { passthrough.end(); });
|
|
938
1098
|
res.on("error", function (e) { passthrough.destroy(e); });
|
|
939
1099
|
return _resolve({ statusCode: res.statusCode, headers: res.headers, body: passthrough });
|
|
@@ -955,6 +1115,10 @@ function _requestH1(transport, u, opts) {
|
|
|
955
1115
|
return;
|
|
956
1116
|
}
|
|
957
1117
|
_emitDownload(chunk.length);
|
|
1118
|
+
if (onChunk) {
|
|
1119
|
+
try { onChunk(chunk); }
|
|
1120
|
+
catch (_e) { /* operator-supplied hook — drop-silent */ }
|
|
1121
|
+
}
|
|
958
1122
|
});
|
|
959
1123
|
res.on("end", function () {
|
|
960
1124
|
if (capExceeded) return;
|
|
@@ -1072,6 +1236,7 @@ function _requestH2(transport, u, opts) {
|
|
|
1072
1236
|
(method === "GET" ? DEFAULT_GET_CAP : DEFAULT_CONTROL_PLANE_CAP);
|
|
1073
1237
|
var observer = typeof opts.observer === "function" ? opts.observer : null;
|
|
1074
1238
|
var startedAt = Date.now();
|
|
1239
|
+
var onChunkH2 = typeof opts.onChunk === "function" ? opts.onChunk : null;
|
|
1075
1240
|
|
|
1076
1241
|
var signal = safeAsync.withTimeoutSignal(opts.signal || null, opts.timeoutMs);
|
|
1077
1242
|
if (signal && signal.aborted) {
|
|
@@ -1128,6 +1293,17 @@ function _requestH2(transport, u, opts) {
|
|
|
1128
1293
|
return _reject(_makeError(opts.errorClass, "HTTP_ERROR",
|
|
1129
1294
|
"HTTP " + statusCode, _isPermanentStatus(statusCode), statusCode));
|
|
1130
1295
|
}
|
|
1296
|
+
if (onChunkH2) {
|
|
1297
|
+
var passthroughH2 = new nodeStream.PassThrough();
|
|
1298
|
+
stream.on("data", function (chunk) {
|
|
1299
|
+
try { onChunkH2(chunk); }
|
|
1300
|
+
catch (_e) { /* operator-supplied hook — drop-silent */ }
|
|
1301
|
+
passthroughH2.write(chunk);
|
|
1302
|
+
});
|
|
1303
|
+
stream.on("end", function () { passthroughH2.end(); });
|
|
1304
|
+
stream.on("error", function (e) { passthroughH2.destroy(e); });
|
|
1305
|
+
return _resolve({ statusCode: statusCode, headers: responseHeaders, body: passthroughH2 });
|
|
1306
|
+
}
|
|
1131
1307
|
return _resolve({ statusCode: statusCode, headers: responseHeaders, body: stream });
|
|
1132
1308
|
}
|
|
1133
1309
|
|
|
@@ -1142,6 +1318,11 @@ function _requestH2(transport, u, opts) {
|
|
|
1142
1318
|
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e2) { /* best-effort h2 stream cancel */ }
|
|
1143
1319
|
_reject(_makeError(opts.errorClass, "RESPONSE_TOO_LARGE",
|
|
1144
1320
|
"response body exceeds " + maxResponseBytes + " bytes", true));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (onChunkH2) {
|
|
1324
|
+
try { onChunkH2(chunk); }
|
|
1325
|
+
catch (_e) { /* operator-supplied hook — drop-silent */ }
|
|
1145
1326
|
}
|
|
1146
1327
|
});
|
|
1147
1328
|
stream.on("end", function () {
|
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:52762d7e-86ab-4e69-9636-eacc1acc2d2d",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T04:
|
|
8
|
+
"timestamp": "2026-05-07T04:52:07.494Z",
|
|
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.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.13",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.13",
|
|
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.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.13",
|
|
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.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.13",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|