@blamejs/core 0.8.43 → 0.8.49
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 +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/http-client.js
CHANGED
|
@@ -1,75 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* @module b.httpClient
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Http Client
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Outbound HTTP client with SSRF gate, retry, circuit breaker,
|
|
9
|
+
* wall-clock + idle timeouts, AbortSignal propagation, connection
|
|
10
|
+
* pooling, streaming, and ALPN-negotiated HTTP/2. Built on node:http,
|
|
11
|
+
* node:https, and node:http2 with zero npm runtime dependency.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
13
|
+
* Every outbound request flows through `b.ssrfGuard` out of the box:
|
|
14
|
+
* hostname → DNS lookup is pinned to vetted IP literals, RFC 1918 /
|
|
15
|
+
* loopback / link-local / IPv6 ULA destinations are refused, and the
|
|
16
|
+
* redirect chain is re-validated at every hop so a 302 to
|
|
17
|
+
* `http://169.254.169.254/` (cloud metadata) can't smuggle past the
|
|
18
|
+
* first-hop gate. The same DNS pinning applies to retries — there's
|
|
19
|
+
* no retry path that bypasses the guard.
|
|
11
20
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* idleTimeoutMs, // zero-progress idle cap (default 30s)
|
|
19
|
-
* responseMode, // "buffer" (default) | "stream" | "always-resolve"
|
|
20
|
-
* maxResponseBytes, // for buffer mode (default 16 MiB control,
|
|
21
|
-
* // 1 GiB GET — operators with > 1 GiB
|
|
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.
|
|
27
|
-
* signal, // AbortSignal — propagated to req/stream
|
|
28
|
-
* errorClass, // FrameworkError subclass
|
|
29
|
-
* observer, // optional (stage, info) => void hook
|
|
30
|
-
* agent, // override per-origin pool (h1 only)
|
|
31
|
-
* preferH2, // bool — for cleartext h2 (h2c). HTTPS origins
|
|
32
|
-
* // already attempt h2 via ALPN; this flag is
|
|
33
|
-
* // for HTTP origins (internal services, tests)
|
|
34
|
-
* // that explicitly speak h2c.
|
|
35
|
-
* })
|
|
36
|
-
* → { statusCode, headers, body }
|
|
21
|
+
* Protocol selection is automatic. HTTPS origins handshake with
|
|
22
|
+
* ALPN `['h2', 'http/1.1']` and cache the resulting transport per
|
|
23
|
+
* `<protocol>//<hostname>:<port>`. While a transport is mid-negotiate
|
|
24
|
+
* the cache holds the in-flight Promise so concurrent calls to a new
|
|
25
|
+
* origin coalesce onto a single connection. h2 GOAWAY or session
|
|
26
|
+
* error evicts the entry; the next request reconnects.
|
|
37
27
|
*
|
|
38
|
-
*
|
|
28
|
+
* Resiliency defaults: TLS 1.3 minimum, PQC-preferred `ecdhCurve`
|
|
29
|
+
* group order, split wall-clock vs zero-progress idle timeouts,
|
|
30
|
+
* request-body stream errors propagated to the returned Promise,
|
|
31
|
+
* and h2 stream cancellation via NGHTTP2_CANCEL (clean, not
|
|
32
|
+
* `stream.destroy`) when the AbortSignal fires.
|
|
39
33
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* over the same h2 session. If server picks 'h1', the cached
|
|
43
|
-
* transport is an https.Agent with keepAlive.
|
|
44
|
-
*
|
|
45
|
-
* - HTTP origin without preferH2: h1 only.
|
|
46
|
-
* - HTTP origin with preferH2: h2c (cleartext h2). No ALPN — caller
|
|
47
|
-
* attests the server speaks h2c. Used by internal services and
|
|
48
|
-
* test fixtures (mock h2 server).
|
|
49
|
-
*
|
|
50
|
-
* Per-origin transport cache:
|
|
51
|
-
*
|
|
52
|
-
* key = "<protocol>//<hostname>:<port>"
|
|
53
|
-
* value = { kind: 'h1', lib, agent } | { kind: 'h2', session }
|
|
54
|
-
*
|
|
55
|
-
* While a transport is being negotiated (TLS handshake / h2 connect)
|
|
56
|
-
* the cache holds the in-flight Promise so concurrent calls to a
|
|
57
|
-
* new origin coalesce onto the same connection.
|
|
58
|
-
*
|
|
59
|
-
* Resiliency:
|
|
60
|
-
* - Wall-clock + idle timeouts (split — slow-progress vs zero-progress)
|
|
61
|
-
* - AbortSignal propagated to req.destroy / stream.close
|
|
62
|
-
* - TLS 1.3 minimum + PQC ecdhCurve preference
|
|
63
|
-
* - h2 session GOAWAY / error → cache eviction; next request reconnects
|
|
64
|
-
* - h2 stream cancellation via NGHTTP2_CANCEL on abort (clean, not destroy)
|
|
65
|
-
* - Request-body stream errors propagated to Promise rejection
|
|
34
|
+
* @card
|
|
35
|
+
* Outbound HTTP client with SSRF gate, retry, circuit breaker, wall-clock + idle timeouts, AbortSignal propagation, connection pooling, streaming, and ALPN-negotiated HTTP/2.
|
|
66
36
|
*/
|
|
67
37
|
|
|
38
|
+
var fs = require("fs");
|
|
68
39
|
var http = require("http");
|
|
69
40
|
var https = require("https");
|
|
70
41
|
var http2 = require("http2");
|
|
42
|
+
var nodeCrypto = require("crypto");
|
|
43
|
+
var nodePath = require("path");
|
|
71
44
|
var nodeStream = require("node:stream");
|
|
45
|
+
var streamPromises = require("node:stream/promises");
|
|
72
46
|
var { URL } = require("url");
|
|
47
|
+
var atomicFile = require("./atomic-file");
|
|
73
48
|
var C = require("./constants");
|
|
74
49
|
var crypto = require("./crypto");
|
|
75
50
|
var pqcAgent = require("./pqc-agent");
|
|
@@ -78,7 +53,8 @@ var safeBuffer = require("./safe-buffer");
|
|
|
78
53
|
var safeUrl = require("./safe-url");
|
|
79
54
|
var ssrfGuard = require("./ssrf-guard");
|
|
80
55
|
var networkProxy = require("./network-proxy");
|
|
81
|
-
var
|
|
56
|
+
var validateOpts = require("./validate-opts");
|
|
57
|
+
var { FrameworkError, HttpClientError } = require("./framework-error");
|
|
82
58
|
|
|
83
59
|
// Per-origin transport cache. Entry is either the resolved transport
|
|
84
60
|
// object or a pending Promise that resolves to one. The Promise form
|
|
@@ -121,6 +97,15 @@ var _transports = new Map();
|
|
|
121
97
|
// built into the protocol, with replay protection at the QUIC
|
|
122
98
|
// layer. The framework's `b.httpClient` is HTTP/1.1 + HTTP/2 only;
|
|
123
99
|
// operators wanting h3 wire their own client.
|
|
100
|
+
//
|
|
101
|
+
// QUIC retry / address-validation (RFC 9000 §8 + RFC 9001 §6) is
|
|
102
|
+
// deferred-with-condition: outbound h3 negotiation re-opens when
|
|
103
|
+
// Node's `--experimental-quic` graduates to stable and ships a
|
|
104
|
+
// `node:http3` module. The escape hatch today is `opts.agent` —
|
|
105
|
+
// operators on internal-mesh deployments that already terminate h3
|
|
106
|
+
// pass their own h3 agent rather than the framework rolling its
|
|
107
|
+
// own implementation under an experimental Node flag. SECURITY.md
|
|
108
|
+
// "Watch list" tracks the re-open trigger.
|
|
124
109
|
|
|
125
110
|
// Pool tuning for the HTTP-client transport cache. Keep-alive is
|
|
126
111
|
// shorter than the standalone pqc-agent default (1s vs 30s) because
|
|
@@ -143,6 +128,31 @@ var DEFAULT_AGENT_OPTS = Object.freeze({
|
|
|
143
128
|
|
|
144
129
|
var HTTP_CLIENT_AGENT_OPTS = Object.assign({}, DEFAULT_AGENT_OPTS);
|
|
145
130
|
|
|
131
|
+
/**
|
|
132
|
+
* @primitive b.httpClient.configurePool
|
|
133
|
+
* @signature b.httpClient.configurePool(opts)
|
|
134
|
+
* @since 0.1.0
|
|
135
|
+
* @status stable
|
|
136
|
+
* @related b.httpClient.request
|
|
137
|
+
*
|
|
138
|
+
* Updates the keepAlive Agent options used for new h1 transports and
|
|
139
|
+
* tears down the per-origin transport cache so subsequent requests
|
|
140
|
+
* pick up the fresh values. Existing in-flight responses keep their
|
|
141
|
+
* old transport. Throws on unknown keys, non-positive integers, or a
|
|
142
|
+
* non-boolean `keepAlive`. Use at boot when the default 16/8 socket
|
|
143
|
+
* caps don't match the operator's downstream concurrency budget.
|
|
144
|
+
*
|
|
145
|
+
* @opts
|
|
146
|
+
* keepAlive: true, // boolean — whether to reuse sockets
|
|
147
|
+
* keepAliveMsecs: 1000, // positive integer ms between keep-alive probes
|
|
148
|
+
* maxSockets: 16, // positive integer — concurrent sockets per origin
|
|
149
|
+
* maxFreeSockets: 8, // positive integer — idle sockets retained per origin
|
|
150
|
+
* scheduling: "lifo", // "lifo" | "fifo"
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* b.httpClient.configurePool({ maxSockets: 64, maxFreeSockets: 32 });
|
|
154
|
+
* // → undefined (cache cleared; next request builds a 64-socket pool)
|
|
155
|
+
*/
|
|
146
156
|
function configurePool(opts) {
|
|
147
157
|
if (!opts || typeof opts !== "object") {
|
|
148
158
|
throw new Error("httpClient.configurePool: opts must be an object");
|
|
@@ -639,6 +649,49 @@ function _stripCrossOriginAuth(headers) {
|
|
|
639
649
|
return out;
|
|
640
650
|
}
|
|
641
651
|
|
|
652
|
+
/**
|
|
653
|
+
* @primitive b.httpClient.request
|
|
654
|
+
* @signature b.httpClient.request(opts)
|
|
655
|
+
* @since 0.1.0
|
|
656
|
+
* @status stable
|
|
657
|
+
* @related b.httpClient.downloadStream, b.httpClient.uploadMultipartStream, b.ssrfGuard
|
|
658
|
+
*
|
|
659
|
+
* Promise-returning, AbortSignal-aware HTTP request. Negotiates h2 /
|
|
660
|
+
* h1 per-origin via ALPN, reuses transports from the cache, runs every
|
|
661
|
+
* destination through `b.ssrfGuard` before connecting, and re-validates
|
|
662
|
+
* each redirect hop. Returns `{ statusCode, headers, body }` for the
|
|
663
|
+
* default `"buffer"` mode; `"stream"` returns a Readable for the body.
|
|
664
|
+
* Sensitive headers (Authorization / Cookie / Proxy-Authorization) are
|
|
665
|
+
* stripped on cross-origin redirect. Body-stream errors propagate to
|
|
666
|
+
* the rejected Promise.
|
|
667
|
+
*
|
|
668
|
+
* @opts
|
|
669
|
+
* method: "GET", // HTTP method
|
|
670
|
+
* url: <required>, // string or URL — destination
|
|
671
|
+
* headers: {}, // request headers
|
|
672
|
+
* body: undefined, // Buffer | string | Readable | undefined
|
|
673
|
+
* timeoutMs: undefined, // wall-clock cap; no default — operator chooses
|
|
674
|
+
* idleTimeoutMs: 30000, // zero-progress cap
|
|
675
|
+
* responseMode: "buffer", // "buffer" | "stream" | "always-resolve"
|
|
676
|
+
* maxResponseBytes: undefined, // 16 MiB control / 1 GiB GET defaults; ignored in "stream"
|
|
677
|
+
* onChunk: undefined, // (chunk: Buffer) => void — fires per response chunk
|
|
678
|
+
* signal: undefined, // AbortSignal — propagated to req / stream
|
|
679
|
+
* errorClass: HttpClientError, // FrameworkError subclass for thrown errors
|
|
680
|
+
* observer: undefined, // (stage, info) => void — lifecycle hook
|
|
681
|
+
* agent: undefined, // override per-origin Agent (h1 only)
|
|
682
|
+
* preferH2: false, // attempt h2c against an HTTP origin (no ALPN)
|
|
683
|
+
* before: undefined, // array of (opts) => opts | Promise — request mutators
|
|
684
|
+
* after: undefined, // array of (response) => response | Promise — response mutators
|
|
685
|
+
* onUploadProgress: undefined, // (bytesSent, totalBytes?) => void
|
|
686
|
+
*
|
|
687
|
+
* @example
|
|
688
|
+
* var res = await b.httpClient.request({
|
|
689
|
+
* method: "GET",
|
|
690
|
+
* url: "https://example.com/health",
|
|
691
|
+
* timeoutMs: 5000,
|
|
692
|
+
* });
|
|
693
|
+
* // → { statusCode: 200, headers: { "content-type": "application/json", ... }, body: <Buffer> }
|
|
694
|
+
*/
|
|
642
695
|
function request(opts) {
|
|
643
696
|
if (!opts || !opts.url) {
|
|
644
697
|
return Promise.reject(_makeError(opts && opts.errorClass, "BAD_ARG", "url is required", true));
|
|
@@ -1395,6 +1448,377 @@ function _requestH2(transport, u, opts) {
|
|
|
1395
1448
|
});
|
|
1396
1449
|
}
|
|
1397
1450
|
|
|
1451
|
+
// ---- Streaming primitives ----
|
|
1452
|
+
//
|
|
1453
|
+
// downloadStream — pipe a response body to a tmp file, hash-while-piping,
|
|
1454
|
+
// atomic-rename on hash match. Operators receive `{ statusCode,
|
|
1455
|
+
// bytesWritten, hash }`; on hash mismatch the tmp file is deleted and an
|
|
1456
|
+
// HttpClientError with code "httpclient/hash-mismatch" is thrown.
|
|
1457
|
+
//
|
|
1458
|
+
// uploadMultipartStream — POST a file body via multipart/form-data
|
|
1459
|
+
// without buffering. Streams from disk through the request body using
|
|
1460
|
+
// `fs.createReadStream` + `node:stream/promises` pipeline.
|
|
1461
|
+
//
|
|
1462
|
+
// Both compose through `request()` (responseMode: "stream") so safeUrl,
|
|
1463
|
+
// ssrfGuard, allowedHosts, network-proxy, audit-on-host-deny, and the
|
|
1464
|
+
// per-origin transport cache apply unchanged.
|
|
1465
|
+
|
|
1466
|
+
// Algorithms exposed to operators. Defaults to PQC-first sha3-512;
|
|
1467
|
+
// callers needing legacy-peer interop with sha-256 (S3 ETag class) opt
|
|
1468
|
+
// in explicitly.
|
|
1469
|
+
var ALLOWED_DOWNLOAD_HASH_ALGS = ["sha3-512", "sha-256", "sha-512", "shake256"];
|
|
1470
|
+
var DEFAULT_DOWNLOAD_HASH_ALG = "sha3-512";
|
|
1471
|
+
var DEFAULT_DOWNLOAD_FILE_MODE = 0o600;
|
|
1472
|
+
|
|
1473
|
+
function _hcErr(code, message, statusCode) {
|
|
1474
|
+
return new HttpClientError(code, message, true, statusCode);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Throw at config-time if opts shape is malformed — operator catches the
|
|
1478
|
+
// typo here, not inside the request loop.
|
|
1479
|
+
function _validateDownloadOpts(opts) {
|
|
1480
|
+
if (!opts || typeof opts !== "object") {
|
|
1481
|
+
throw _hcErr("httpclient/bad-opts", "downloadStream: opts must be an object");
|
|
1482
|
+
}
|
|
1483
|
+
validateOpts.requireNonEmptyString(opts.url, "downloadStream: url",
|
|
1484
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1485
|
+
validateOpts.requireNonEmptyString(opts.dest, "downloadStream: dest",
|
|
1486
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1487
|
+
validateOpts.optionalNonEmptyString(opts.hash, "downloadStream: hash",
|
|
1488
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1489
|
+
if (opts.hash !== undefined && ALLOWED_DOWNLOAD_HASH_ALGS.indexOf(opts.hash) === -1) {
|
|
1490
|
+
throw _hcErr("httpclient/bad-opts",
|
|
1491
|
+
"downloadStream: hash must be one of " + ALLOWED_DOWNLOAD_HASH_ALGS.join(", ") +
|
|
1492
|
+
"; got " + JSON.stringify(opts.hash));
|
|
1493
|
+
}
|
|
1494
|
+
if (opts.expected !== undefined) {
|
|
1495
|
+
validateOpts.requireNonEmptyString(opts.expected, "downloadStream: expected",
|
|
1496
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1497
|
+
if (!safeBuffer.isHex(opts.expected)) {
|
|
1498
|
+
throw _hcErr("httpclient/bad-opts",
|
|
1499
|
+
"downloadStream: expected must be a non-empty hex digest");
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
validateOpts.optionalPositiveFinite(opts.timeoutMs, "downloadStream: timeoutMs",
|
|
1503
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1504
|
+
if (opts.maxBytes !== undefined &&
|
|
1505
|
+
(typeof opts.maxBytes !== "number" || !isFinite(opts.maxBytes) || opts.maxBytes <= 0 ||
|
|
1506
|
+
Math.floor(opts.maxBytes) !== opts.maxBytes)) {
|
|
1507
|
+
throw _hcErr("httpclient/bad-opts",
|
|
1508
|
+
"downloadStream: maxBytes must be a positive finite integer");
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function _emitAudit(opts, action, outcome, metadata) {
|
|
1513
|
+
if (!opts || !opts.audit || typeof opts.audit.safeEmit !== "function") return;
|
|
1514
|
+
try {
|
|
1515
|
+
opts.audit.safeEmit({
|
|
1516
|
+
action: action,
|
|
1517
|
+
outcome: outcome,
|
|
1518
|
+
resource: { kind: "outbound.http", id: String(opts.url || "") },
|
|
1519
|
+
metadata: metadata || {},
|
|
1520
|
+
});
|
|
1521
|
+
} catch (_e) { /* audit best-effort */ }
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* @primitive b.httpClient.downloadStream
|
|
1526
|
+
* @signature b.httpClient.downloadStream(opts)
|
|
1527
|
+
* @since 0.1.0
|
|
1528
|
+
* @status stable
|
|
1529
|
+
* @related b.httpClient.request, b.httpClient.uploadMultipartStream, b.atomicFile.ensureDir
|
|
1530
|
+
*
|
|
1531
|
+
* Streams a remote resource to disk while hashing the bytes in flight,
|
|
1532
|
+
* then atomically renames the tmp file to `opts.dest` only after the
|
|
1533
|
+
* hash matches `opts.expected` (when supplied). Hash mismatch deletes
|
|
1534
|
+
* the tmp file and throws `httpclient/hash-mismatch`. Composes through
|
|
1535
|
+
* `request({ responseMode: "stream" })` so the SSRF gate, allowedHosts
|
|
1536
|
+
* filter, network proxy, and per-origin transport cache all apply.
|
|
1537
|
+
*
|
|
1538
|
+
* @opts
|
|
1539
|
+
* url: <required>, // string — source
|
|
1540
|
+
* dest: <required>, // absolute filesystem path — final landing
|
|
1541
|
+
* hash: "sha3-512", // "sha3-512" | "sha-256" | "sha-512" | "shake256"
|
|
1542
|
+
* expected: undefined, // hex digest; when set, verified before rename
|
|
1543
|
+
* timeoutMs: undefined, // wall-clock cap
|
|
1544
|
+
* maxBytes: undefined, // positive integer — abort past this size
|
|
1545
|
+
* audit: undefined, // audit sink with safeEmit({...})
|
|
1546
|
+
*
|
|
1547
|
+
* @example
|
|
1548
|
+
* var result = await b.httpClient.downloadStream({
|
|
1549
|
+
* url: "https://example.com/release.tar.gz",
|
|
1550
|
+
* dest: "/var/lib/blamejs/release.tar.gz",
|
|
1551
|
+
* hash: "sha3-512",
|
|
1552
|
+
* expected: "9f86d081884c7d65...d4e5",
|
|
1553
|
+
* });
|
|
1554
|
+
* // → { statusCode: 200, bytesWritten: 1048576, hash: "9f86d081884c7d65...d4e5" }
|
|
1555
|
+
*/
|
|
1556
|
+
async function downloadStream(opts) {
|
|
1557
|
+
_validateDownloadOpts(opts);
|
|
1558
|
+
var alg = opts.hash || DEFAULT_DOWNLOAD_HASH_ALG;
|
|
1559
|
+
var dest = opts.dest;
|
|
1560
|
+
var tmpPath = dest + ".tmp-" + crypto.generateToken(C.BYTES.bytes(8));
|
|
1561
|
+
var dir = nodePath.dirname(dest);
|
|
1562
|
+
|
|
1563
|
+
atomicFile.ensureDir(dir);
|
|
1564
|
+
|
|
1565
|
+
// Stream-mode request — body is a Readable that emits the response
|
|
1566
|
+
// chunks. The framework's onChunk path is intentionally NOT used here
|
|
1567
|
+
// because we own the destination tmp file and need precise error
|
|
1568
|
+
// ordering between hash + write-fsync + rename.
|
|
1569
|
+
var res;
|
|
1570
|
+
try {
|
|
1571
|
+
res = await request({
|
|
1572
|
+
method: "GET",
|
|
1573
|
+
url: opts.url,
|
|
1574
|
+
headers: opts.headers || {},
|
|
1575
|
+
responseMode: "stream",
|
|
1576
|
+
timeoutMs: opts.timeoutMs,
|
|
1577
|
+
idleTimeoutMs: opts.idleTimeoutMs,
|
|
1578
|
+
signal: opts.signal,
|
|
1579
|
+
agent: opts.agent,
|
|
1580
|
+
allowedProtocols: opts.allowedProtocols,
|
|
1581
|
+
allowedHosts: opts.allowedHosts,
|
|
1582
|
+
allowInternal: opts.allowInternal,
|
|
1583
|
+
audit: opts.audit,
|
|
1584
|
+
errorClass: HttpClientError,
|
|
1585
|
+
});
|
|
1586
|
+
} catch (e) {
|
|
1587
|
+
_emitAudit(opts, "system.httpclient.download_stream.refused", "denied", {
|
|
1588
|
+
reason: "request-failed", message: e.message, code: e.code,
|
|
1589
|
+
});
|
|
1590
|
+
throw e;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
1594
|
+
// Stream mode of request() already rejected on >=400 above, so this
|
|
1595
|
+
// branch covers 1xx/3xx surfaces that slipped through. Drain + refuse.
|
|
1596
|
+
if (res.body && typeof res.body.resume === "function") res.body.resume();
|
|
1597
|
+
_emitAudit(opts, "system.httpclient.download_stream.refused", "denied", {
|
|
1598
|
+
reason: "non-2xx", statusCode: res.statusCode,
|
|
1599
|
+
});
|
|
1600
|
+
throw _hcErr("httpclient/http-error",
|
|
1601
|
+
"downloadStream: upstream returned HTTP " + res.statusCode, res.statusCode);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
var hasher = nodeCrypto.createHash(alg);
|
|
1605
|
+
var counter = new nodeStream.Transform({
|
|
1606
|
+
transform: function (chunk, _enc, cb) {
|
|
1607
|
+
hasher.update(chunk);
|
|
1608
|
+
counter.bytesWritten += chunk.length;
|
|
1609
|
+
if (typeof opts.maxBytes === "number" && counter.bytesWritten > opts.maxBytes) {
|
|
1610
|
+
return cb(_hcErr("httpclient/response-too-large",
|
|
1611
|
+
"downloadStream: response body exceeds maxBytes " + opts.maxBytes, res.statusCode));
|
|
1612
|
+
}
|
|
1613
|
+
cb(null, chunk);
|
|
1614
|
+
},
|
|
1615
|
+
});
|
|
1616
|
+
counter.bytesWritten = 0;
|
|
1617
|
+
|
|
1618
|
+
var fileStream = fs.createWriteStream(tmpPath, { mode: DEFAULT_DOWNLOAD_FILE_MODE, flags: "w" });
|
|
1619
|
+
|
|
1620
|
+
try {
|
|
1621
|
+
await streamPromises.pipeline(res.body, counter, fileStream);
|
|
1622
|
+
} catch (e) {
|
|
1623
|
+
// Pipeline failure → tmp may be partially written. Remove + audit.
|
|
1624
|
+
try { fs.unlinkSync(tmpPath); } catch (_u) { /* best-effort cleanup */ }
|
|
1625
|
+
_emitAudit(opts, "system.httpclient.download_stream.refused", "denied", {
|
|
1626
|
+
reason: "pipeline-failed", message: e.message, code: e.code,
|
|
1627
|
+
});
|
|
1628
|
+
if (e && e.isHttpClientError) throw e;
|
|
1629
|
+
throw _hcErr(e.code || "httpclient/pipeline-failed",
|
|
1630
|
+
"downloadStream: pipeline failed: " + (e.message || String(e)), res.statusCode);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// fsync the file's data + close. atomicFile.fsync is best-effort
|
|
1634
|
+
// across platforms but matches the discipline of the rest of the
|
|
1635
|
+
// framework's atomic-write paths.
|
|
1636
|
+
try {
|
|
1637
|
+
var fd = fs.openSync(tmpPath, "r+");
|
|
1638
|
+
try { atomicFile.fsync(fd); } finally { try { fs.closeSync(fd); } catch (_c) { /* best-effort fd close */ } }
|
|
1639
|
+
} catch (_fe) { /* fsync best-effort */ }
|
|
1640
|
+
|
|
1641
|
+
var actualHex = hasher.digest("hex");
|
|
1642
|
+
if (typeof opts.expected === "string" && opts.expected.length > 0) {
|
|
1643
|
+
var expected = opts.expected.toLowerCase();
|
|
1644
|
+
if (actualHex.toLowerCase() !== expected) {
|
|
1645
|
+
try { fs.unlinkSync(tmpPath); } catch (_u) { /* best-effort cleanup */ }
|
|
1646
|
+
_emitAudit(opts, "system.httpclient.download_stream.refused", "denied", {
|
|
1647
|
+
reason: "hash-mismatch", alg: alg, expected: expected, actual: actualHex,
|
|
1648
|
+
statusCode: res.statusCode, bytesWritten: counter.bytesWritten,
|
|
1649
|
+
});
|
|
1650
|
+
throw _hcErr("httpclient/hash-mismatch",
|
|
1651
|
+
"downloadStream: hash mismatch (alg=" + alg + ", expected=" + expected +
|
|
1652
|
+
", actual=" + actualHex + ")", res.statusCode);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Atomic rename + dir fsync.
|
|
1657
|
+
try {
|
|
1658
|
+
fs.renameSync(tmpPath, dest);
|
|
1659
|
+
atomicFile.fsyncDir(dir);
|
|
1660
|
+
} catch (e) {
|
|
1661
|
+
try { fs.unlinkSync(tmpPath); } catch (_u) { /* best-effort cleanup */ }
|
|
1662
|
+
_emitAudit(opts, "system.httpclient.download_stream.refused", "denied", {
|
|
1663
|
+
reason: "rename-failed", message: e.message,
|
|
1664
|
+
});
|
|
1665
|
+
throw _hcErr("httpclient/rename-failed",
|
|
1666
|
+
"downloadStream: rename to " + dest + " failed: " + e.message, res.statusCode);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
_emitAudit(opts, "system.httpclient.download_stream.completed", "allowed", {
|
|
1670
|
+
statusCode: res.statusCode,
|
|
1671
|
+
bytesWritten: counter.bytesWritten,
|
|
1672
|
+
alg: alg,
|
|
1673
|
+
hashVerified: typeof opts.expected === "string" && opts.expected.length > 0,
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
return {
|
|
1677
|
+
statusCode: res.statusCode,
|
|
1678
|
+
bytesWritten: counter.bytesWritten,
|
|
1679
|
+
hash: actualHex,
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// ---- uploadMultipartStream ----
|
|
1684
|
+
|
|
1685
|
+
function _validateUploadOpts(opts) {
|
|
1686
|
+
if (!opts || typeof opts !== "object") {
|
|
1687
|
+
throw _hcErr("httpclient/bad-opts", "uploadMultipartStream: opts must be an object");
|
|
1688
|
+
}
|
|
1689
|
+
validateOpts.requireNonEmptyString(opts.url, "uploadMultipartStream: url",
|
|
1690
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1691
|
+
if (!opts.file || typeof opts.file !== "object") {
|
|
1692
|
+
throw _hcErr("httpclient/bad-opts", "uploadMultipartStream: file must be an object");
|
|
1693
|
+
}
|
|
1694
|
+
validateOpts.requireNonEmptyString(opts.file.path, "uploadMultipartStream: file.path",
|
|
1695
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1696
|
+
validateOpts.requireNonEmptyString(opts.file.fieldName, "uploadMultipartStream: file.fieldName",
|
|
1697
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1698
|
+
if (opts.fields !== undefined && (typeof opts.fields !== "object" || opts.fields === null || Array.isArray(opts.fields))) {
|
|
1699
|
+
throw _hcErr("httpclient/bad-opts", "uploadMultipartStream: fields must be an object");
|
|
1700
|
+
}
|
|
1701
|
+
validateOpts.optionalPositiveFinite(opts.timeoutMs, "uploadMultipartStream: timeoutMs",
|
|
1702
|
+
HttpClientError, "httpclient/bad-opts");
|
|
1703
|
+
if (opts.maxBytes !== undefined &&
|
|
1704
|
+
(typeof opts.maxBytes !== "number" || !isFinite(opts.maxBytes) || opts.maxBytes <= 0 ||
|
|
1705
|
+
Math.floor(opts.maxBytes) !== opts.maxBytes)) {
|
|
1706
|
+
throw _hcErr("httpclient/bad-opts",
|
|
1707
|
+
"uploadMultipartStream: maxBytes must be a positive finite integer");
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* @primitive b.httpClient.uploadMultipartStream
|
|
1713
|
+
* @signature b.httpClient.uploadMultipartStream(opts)
|
|
1714
|
+
* @since 0.1.0
|
|
1715
|
+
* @status stable
|
|
1716
|
+
* @related b.httpClient.request, b.httpClient.downloadStream
|
|
1717
|
+
*
|
|
1718
|
+
* POSTs a file body via `multipart/form-data` without buffering the
|
|
1719
|
+
* file in memory. Streams from disk through the request body using
|
|
1720
|
+
* `fs.createReadStream` + `node:stream/promises` pipeline. Throws
|
|
1721
|
+
* `httpclient/missing-file` when `opts.file.path` doesn't exist or
|
|
1722
|
+
* isn't a regular file. Composes through `request()` so SSRF gating,
|
|
1723
|
+
* proxy routing, and the per-origin transport cache apply unchanged.
|
|
1724
|
+
*
|
|
1725
|
+
* @opts
|
|
1726
|
+
* url: <required>, // string — destination
|
|
1727
|
+
* file: <required>, // { path, fieldName, filename?, contentType? }
|
|
1728
|
+
* fields: undefined, // object — extra form fields { name: value, ... }
|
|
1729
|
+
* timeoutMs: undefined, // wall-clock cap
|
|
1730
|
+
* maxBytes: undefined, // positive integer — refuse files larger than this
|
|
1731
|
+
* audit: undefined, // audit sink with safeEmit({...})
|
|
1732
|
+
*
|
|
1733
|
+
* @example
|
|
1734
|
+
* var res = await b.httpClient.uploadMultipartStream({
|
|
1735
|
+
* url: "https://example.com/upload",
|
|
1736
|
+
* file: {
|
|
1737
|
+
* path: "/var/lib/blamejs/release.tar.gz",
|
|
1738
|
+
* fieldName: "artifact",
|
|
1739
|
+
* contentType: "application/gzip",
|
|
1740
|
+
* },
|
|
1741
|
+
* fields: { releaseTag: "v1.2.3" },
|
|
1742
|
+
* });
|
|
1743
|
+
* // → { statusCode: 200, headers: { ... }, body: <Buffer> }
|
|
1744
|
+
*/
|
|
1745
|
+
async function uploadMultipartStream(opts) {
|
|
1746
|
+
_validateUploadOpts(opts);
|
|
1747
|
+
|
|
1748
|
+
var filePath = opts.file.path;
|
|
1749
|
+
var st;
|
|
1750
|
+
try { st = fs.statSync(filePath); }
|
|
1751
|
+
catch (e) {
|
|
1752
|
+
_emitAudit(opts, "system.httpclient.upload_stream.refused", "denied", {
|
|
1753
|
+
reason: "missing-file", path: filePath, message: e.message,
|
|
1754
|
+
});
|
|
1755
|
+
throw _hcErr("httpclient/missing-file",
|
|
1756
|
+
"uploadMultipartStream: file.path not readable: " + e.message);
|
|
1757
|
+
}
|
|
1758
|
+
if (!st.isFile()) {
|
|
1759
|
+
_emitAudit(opts, "system.httpclient.upload_stream.refused", "denied", {
|
|
1760
|
+
reason: "not-a-regular-file", path: filePath,
|
|
1761
|
+
});
|
|
1762
|
+
throw _hcErr("httpclient/missing-file",
|
|
1763
|
+
"uploadMultipartStream: file.path is not a regular file");
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
var filename = (typeof opts.file.filename === "string" && opts.file.filename.length > 0)
|
|
1767
|
+
? opts.file.filename
|
|
1768
|
+
: nodePath.basename(filePath);
|
|
1769
|
+
var contentType = (typeof opts.file.contentType === "string" && opts.file.contentType.length > 0)
|
|
1770
|
+
? opts.file.contentType
|
|
1771
|
+
: "application/octet-stream";
|
|
1772
|
+
|
|
1773
|
+
// Reuse the existing multipart shorthand by passing { filePath } —
|
|
1774
|
+
// it produces a Readable body + sets Content-Length when sizes resolve.
|
|
1775
|
+
// _buildMultipartBody is internal; we route through request()'s
|
|
1776
|
+
// multipart shorthand so the same wire path applies.
|
|
1777
|
+
var fileSpec = {
|
|
1778
|
+
field: opts.file.fieldName,
|
|
1779
|
+
filePath: filePath,
|
|
1780
|
+
filename: filename,
|
|
1781
|
+
contentType: contentType,
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
var res;
|
|
1785
|
+
try {
|
|
1786
|
+
res = await request({
|
|
1787
|
+
method: "POST",
|
|
1788
|
+
url: opts.url,
|
|
1789
|
+
headers: opts.headers || {},
|
|
1790
|
+
multipart: { fields: opts.fields || {}, files: [fileSpec], streaming: true },
|
|
1791
|
+
timeoutMs: opts.timeoutMs,
|
|
1792
|
+
idleTimeoutMs: opts.idleTimeoutMs,
|
|
1793
|
+
signal: opts.signal,
|
|
1794
|
+
agent: opts.agent,
|
|
1795
|
+
allowedProtocols: opts.allowedProtocols,
|
|
1796
|
+
allowedHosts: opts.allowedHosts,
|
|
1797
|
+
allowInternal: opts.allowInternal,
|
|
1798
|
+
maxResponseBytes: opts.maxResponseBytes,
|
|
1799
|
+
audit: opts.audit,
|
|
1800
|
+
errorClass: HttpClientError,
|
|
1801
|
+
});
|
|
1802
|
+
} catch (e) {
|
|
1803
|
+
_emitAudit(opts, "system.httpclient.upload_stream.refused", "denied", {
|
|
1804
|
+
reason: "request-failed", message: e.message, code: e.code,
|
|
1805
|
+
});
|
|
1806
|
+
throw e;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
_emitAudit(opts, "system.httpclient.upload_stream.completed", "allowed", {
|
|
1810
|
+
statusCode: res.statusCode,
|
|
1811
|
+
fileBytes: st.size,
|
|
1812
|
+
fieldName: opts.file.fieldName,
|
|
1813
|
+
filename: filename,
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
return {
|
|
1817
|
+
statusCode: res.statusCode,
|
|
1818
|
+
response: res,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1398
1822
|
// ---- Test helpers ----
|
|
1399
1823
|
|
|
1400
1824
|
function _resetForTest() {
|
|
@@ -1424,10 +1848,13 @@ function _getCachedTransportKind(url) {
|
|
|
1424
1848
|
|
|
1425
1849
|
module.exports = {
|
|
1426
1850
|
request: request,
|
|
1851
|
+
downloadStream: downloadStream,
|
|
1852
|
+
uploadMultipartStream: uploadMultipartStream,
|
|
1427
1853
|
configurePool: configurePool,
|
|
1428
1854
|
DEFAULT_CONTROL_PLANE_CAP: DEFAULT_CONTROL_PLANE_CAP,
|
|
1429
1855
|
DEFAULT_GET_CAP: DEFAULT_GET_CAP,
|
|
1430
1856
|
DEFAULT_AGENT_OPTS: DEFAULT_AGENT_OPTS,
|
|
1857
|
+
ALLOWED_DOWNLOAD_HASH_ALGS: ALLOWED_DOWNLOAD_HASH_ALGS,
|
|
1431
1858
|
_resetForTest: _resetForTest,
|
|
1432
1859
|
_getCachedTransportCount: _getCachedTransportCount,
|
|
1433
1860
|
_getCachedTransportKind: _getCachedTransportKind,
|