@blamejs/core 0.4.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 +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- package/package.json +39 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP client primitive — Promise-returning, AbortSignal-aware,
|
|
4
|
+
* connection-pooled, streaming-capable, HTTP/2-capable.
|
|
5
|
+
*
|
|
6
|
+
* Built on node:http, node:https, and node:http2. Zero npm runtime
|
|
7
|
+
* dependency. Same caller surface for h1 and h2; the protocol version
|
|
8
|
+
* is negotiated per-origin via ALPN (h2 preferred, h1 fallback).
|
|
9
|
+
*
|
|
10
|
+
* Single entry point:
|
|
11
|
+
*
|
|
12
|
+
* await httpClient.request({
|
|
13
|
+
* method, // string, default GET
|
|
14
|
+
* url, // string or URL
|
|
15
|
+
* headers, // object, default {}
|
|
16
|
+
* body, // Buffer | string | Readable | undefined
|
|
17
|
+
* timeoutMs, // wall-clock cap (caller-chosen, no default)
|
|
18
|
+
* idleTimeoutMs, // zero-progress idle cap (default 30s)
|
|
19
|
+
* responseMode, // "buffer" (default) | "stream"
|
|
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
|
+
* signal, // AbortSignal — propagated to req/stream
|
|
24
|
+
* errorClass, // FrameworkError subclass
|
|
25
|
+
* observer, // optional (stage, info) => void hook
|
|
26
|
+
* agent, // override per-origin pool (h1 only)
|
|
27
|
+
* preferH2, // bool — for cleartext h2 (h2c). HTTPS origins
|
|
28
|
+
* // already attempt h2 via ALPN; this flag is
|
|
29
|
+
* // for HTTP origins (internal services, tests)
|
|
30
|
+
* // that explicitly speak h2c.
|
|
31
|
+
* })
|
|
32
|
+
* → { statusCode, headers, body }
|
|
33
|
+
*
|
|
34
|
+
* Protocol selection:
|
|
35
|
+
*
|
|
36
|
+
* - HTTPS origin: TLS handshake with ALPN ['h2', 'http/1.1']. If
|
|
37
|
+
* server picks 'h2', subsequent requests to that origin multiplex
|
|
38
|
+
* over the same h2 session. If server picks 'h1', the cached
|
|
39
|
+
* transport is an https.Agent with keepAlive.
|
|
40
|
+
*
|
|
41
|
+
* - HTTP origin without preferH2: h1 only.
|
|
42
|
+
* - HTTP origin with preferH2: h2c (cleartext h2). No ALPN — caller
|
|
43
|
+
* attests the server speaks h2c. Used by internal services and
|
|
44
|
+
* test fixtures (mock h2 server).
|
|
45
|
+
*
|
|
46
|
+
* Per-origin transport cache:
|
|
47
|
+
*
|
|
48
|
+
* key = "<protocol>//<hostname>:<port>"
|
|
49
|
+
* value = { kind: 'h1', lib, agent } | { kind: 'h2', session }
|
|
50
|
+
*
|
|
51
|
+
* While a transport is being negotiated (TLS handshake / h2 connect)
|
|
52
|
+
* the cache holds the in-flight Promise so concurrent calls to a
|
|
53
|
+
* new origin coalesce onto the same connection.
|
|
54
|
+
*
|
|
55
|
+
* Resiliency:
|
|
56
|
+
* - Wall-clock + idle timeouts (split — slow-progress vs zero-progress)
|
|
57
|
+
* - AbortSignal propagated to req.destroy / stream.close
|
|
58
|
+
* - TLS 1.3 minimum + PQC ecdhCurve preference
|
|
59
|
+
* - h2 session GOAWAY / error → cache eviction; next request reconnects
|
|
60
|
+
* - h2 stream cancellation via NGHTTP2_CANCEL on abort (clean, not destroy)
|
|
61
|
+
* - Request-body stream errors propagated to Promise rejection
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
var http = require("http");
|
|
65
|
+
var https = require("https");
|
|
66
|
+
var http2 = require("http2");
|
|
67
|
+
var { URL } = require("url");
|
|
68
|
+
var C = require("./constants");
|
|
69
|
+
var pqcAgent = require("./pqc-agent");
|
|
70
|
+
var safeAsync = require("./safe-async");
|
|
71
|
+
var safeBuffer = require("./safe-buffer");
|
|
72
|
+
var safeUrl = require("./safe-url");
|
|
73
|
+
var { FrameworkError } = require("./framework-error");
|
|
74
|
+
|
|
75
|
+
// Per-origin transport cache. Entry is either the resolved transport
|
|
76
|
+
// object or a pending Promise that resolves to one. The Promise form
|
|
77
|
+
// lets concurrent calls to a new origin coalesce on the same connect.
|
|
78
|
+
//
|
|
79
|
+
// Transport shapes the cache currently holds:
|
|
80
|
+
//
|
|
81
|
+
// { kind: "h1", lib, agent } — node:http(s) + keepAlive Agent
|
|
82
|
+
// { kind: "h2", session } — node:http2 ClientHttp2Session
|
|
83
|
+
//
|
|
84
|
+
// Reserved for the future (when node:http3 ships stable — currently
|
|
85
|
+
// behind --experimental-quic, no http3 module yet):
|
|
86
|
+
//
|
|
87
|
+
// { kind: "h3", session } — node:http3 ClientHttp3Session
|
|
88
|
+
//
|
|
89
|
+
// Adding the h3 case won't change the caller-facing surface: ALPN
|
|
90
|
+
// negotiation gains "h3" as the highest-preference protocol over QUIC,
|
|
91
|
+
// _getTransport branches on the resolved transport, and a new
|
|
92
|
+
// _requestH3 mirrors _requestH2's stream-based shape. h3's design
|
|
93
|
+
// gives 0-RTT first-class (vs. h1/h2 where 0-RTT is opaque under
|
|
94
|
+
// node's TLS layer — see TLS_SESSION_RESUMPTION_NOTES below).
|
|
95
|
+
var _transports = new Map();
|
|
96
|
+
|
|
97
|
+
// TLS session resumption notes — what we get for free vs. what's
|
|
98
|
+
// out of reach today:
|
|
99
|
+
//
|
|
100
|
+
// keepAlive Agent (h1) / long-lived ClientHttp2Session (h2) means
|
|
101
|
+
// the WARM-CONNECTION case is zero-handshake — better than 0-RTT.
|
|
102
|
+
// We pay the TLS handshake once per origin, then amortize.
|
|
103
|
+
//
|
|
104
|
+
// When a pool socket is recycled, node's tls layer caches session
|
|
105
|
+
// tickets and does 1-RTT resumption automatically. We don't
|
|
106
|
+
// expose 0-RTT (early_data) — node's https.Agent has no clean API
|
|
107
|
+
// for it, and 0-RTT is REPLAY-RISKY for non-idempotent requests
|
|
108
|
+
// (server can't distinguish original from replay until the
|
|
109
|
+
// handshake completes). Operators who need 0-RTT for a specific
|
|
110
|
+
// idempotent path can pass their own agent via opts.agent.
|
|
111
|
+
//
|
|
112
|
+
// QUIC/h3 changes this calculus: 0-RTT is a first-class feature
|
|
113
|
+
// built into the protocol, with replay protection at the QUIC
|
|
114
|
+
// layer. We'll plumb it through when h3 lands.
|
|
115
|
+
|
|
116
|
+
// Pool tuning for the HTTP-client transport cache. Keep-alive is
|
|
117
|
+
// shorter than the standalone pqc-agent default (1s vs 30s) because
|
|
118
|
+
// the cache layer manages its own warm-connection reuse and we want
|
|
119
|
+
// idle sockets reaped quickly between bursts. ecdhCurve / minVersion
|
|
120
|
+
// come from pqc-agent and cannot be set here — the framework's
|
|
121
|
+
// PQC-only TLS posture is one place, in lib/pqc-agent.js.
|
|
122
|
+
var HTTP_CLIENT_AGENT_OPTS = {
|
|
123
|
+
keepAlive: true,
|
|
124
|
+
keepAliveMsecs: 1000,
|
|
125
|
+
maxSockets: 16,
|
|
126
|
+
maxFreeSockets: 8,
|
|
127
|
+
scheduling: "lifo",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// h2 session connect options. Same TLS posture as h1 Agent.
|
|
131
|
+
var DEFAULT_H2_TLS_OPTS = {
|
|
132
|
+
ALPNProtocols: ["h2", "http/1.1"],
|
|
133
|
+
ecdhCurve: C.TLS_GROUP_CURVE_STR,
|
|
134
|
+
minVersion: "TLSv1.3",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
var DEFAULT_CONTROL_PLANE_CAP = C.BYTES.mib(16);
|
|
138
|
+
var DEFAULT_GET_CAP = C.BYTES.gib(1);
|
|
139
|
+
var DEFAULT_IDLE_TIMEOUT_MS = C.TIME.seconds(30);
|
|
140
|
+
|
|
141
|
+
// h2 session idle close. After this much idle time with no streams,
|
|
142
|
+
// close the session — long-running processes don't pin one TLS
|
|
143
|
+
// connection forever.
|
|
144
|
+
var H2_SESSION_IDLE_TIMEOUT_MS = C.TIME.minutes(5);
|
|
145
|
+
|
|
146
|
+
function _originKey(u) {
|
|
147
|
+
return u.protocol + "//" + u.hostname + ":" +
|
|
148
|
+
(u.port || (u.protocol === "https:" ? 443 : 80));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _makeH1Transport(u) {
|
|
152
|
+
var lib = u.protocol === "https:" ? https : http;
|
|
153
|
+
// HTTPS path goes through pqcAgent.create so the framework's PQC-only
|
|
154
|
+
// posture is enforced via the single primitive. Cleartext HTTP stays
|
|
155
|
+
// on http.Agent because there's no TLS posture to enforce.
|
|
156
|
+
var agent = u.protocol === "https:"
|
|
157
|
+
? pqcAgent.create(HTTP_CLIENT_AGENT_OPTS)
|
|
158
|
+
: new lib.Agent(HTTP_CLIENT_AGENT_OPTS);
|
|
159
|
+
return { kind: "h1", lib: lib, agent: agent };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Connect an h2 session to an HTTPS origin via ALPN. If the server picks
|
|
163
|
+
// http/1.1, fall back to an h1 transport for that origin.
|
|
164
|
+
function _connectHttpsWithAlpn(u) {
|
|
165
|
+
return new Promise(function (resolve, reject) {
|
|
166
|
+
var session = http2.connect(u.protocol + "//" + u.host, DEFAULT_H2_TLS_OPTS);
|
|
167
|
+
var settled = false;
|
|
168
|
+
function _done(t) { if (!settled) { settled = true; resolve(t); } }
|
|
169
|
+
function _fail(err) { if (!settled) { settled = true; reject(err); } }
|
|
170
|
+
|
|
171
|
+
session.once("connect", function () {
|
|
172
|
+
var alpn = session.alpnProtocol;
|
|
173
|
+
if (alpn === "h2") {
|
|
174
|
+
_wireH2Session(session, _originKey(u));
|
|
175
|
+
_done({ kind: "h2", session: session });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Server picked http/1.1 — close the h2 session, return h1 transport.
|
|
179
|
+
try { session.close(); } catch (_e) {}
|
|
180
|
+
_done(_makeH1Transport(u));
|
|
181
|
+
});
|
|
182
|
+
session.once("error", function (err) {
|
|
183
|
+
try { session.close(); } catch (_e) {}
|
|
184
|
+
_fail(err);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Connect an h2c session (cleartext h2). No ALPN, no fallback — caller
|
|
190
|
+
// has attested via preferH2 that the server speaks h2c.
|
|
191
|
+
function _connectH2c(u) {
|
|
192
|
+
return new Promise(function (resolve, reject) {
|
|
193
|
+
var session = http2.connect(u.protocol + "//" + u.host);
|
|
194
|
+
session.once("connect", function () {
|
|
195
|
+
_wireH2Session(session, _originKey(u));
|
|
196
|
+
resolve({ kind: "h2", session: session });
|
|
197
|
+
});
|
|
198
|
+
session.once("error", function (err) {
|
|
199
|
+
try { session.close(); } catch (_e) {}
|
|
200
|
+
reject(err);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Common h2 session wiring — idle close + cache eviction on error/close.
|
|
206
|
+
function _wireH2Session(session, key) {
|
|
207
|
+
session.setTimeout(H2_SESSION_IDLE_TIMEOUT_MS, function () {
|
|
208
|
+
try { session.close(); } catch (_e) {}
|
|
209
|
+
});
|
|
210
|
+
session.once("close", function () { _transports.delete(key); });
|
|
211
|
+
session.once("error", function () { _transports.delete(key); });
|
|
212
|
+
session.once("goaway", function () {
|
|
213
|
+
// Server signalling 'no new streams' — let in-flight finish, evict cache.
|
|
214
|
+
_transports.delete(key);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Async transport selection. Returns Promise<transport>.
|
|
219
|
+
function _getTransport(u, opts) {
|
|
220
|
+
var key = _originKey(u);
|
|
221
|
+
var cached = _transports.get(key);
|
|
222
|
+
if (cached) {
|
|
223
|
+
// Could be a resolved transport OR a pending Promise.
|
|
224
|
+
return Promise.resolve(cached);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var promise;
|
|
228
|
+
if (u.protocol === "https:") {
|
|
229
|
+
promise = _connectHttpsWithAlpn(u);
|
|
230
|
+
} else if (opts && opts.preferH2) {
|
|
231
|
+
promise = _connectH2c(u);
|
|
232
|
+
} else {
|
|
233
|
+
// HTTP without preferH2 → h1 only.
|
|
234
|
+
promise = Promise.resolve(_makeH1Transport(u));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Cache the in-flight Promise immediately so concurrent calls
|
|
238
|
+
// coalesce. On resolve, replace with the transport. On reject, evict.
|
|
239
|
+
_transports.set(key, promise);
|
|
240
|
+
promise.then(
|
|
241
|
+
function (t) { _transports.set(key, t); },
|
|
242
|
+
function (_err) { _transports.delete(key); }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return promise;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function _makeError(errorClass, code, message, permanent, statusCode) {
|
|
249
|
+
if (!errorClass) return new FrameworkError(message, code);
|
|
250
|
+
return new errorClass(code, message, permanent, statusCode);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _isPermanentStatus(statusCode) {
|
|
254
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
255
|
+
return statusCode !== 408 && statusCode !== 425 && statusCode !== 429;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// h2 sends headers as lowercased keys plus :method / :path / :scheme /
|
|
261
|
+
// :authority pseudo-headers. Convert from h1-shaped headers.
|
|
262
|
+
function _toH2Headers(method, u, headers) {
|
|
263
|
+
var h2Headers = Object.create(null);
|
|
264
|
+
h2Headers[":method"] = method;
|
|
265
|
+
h2Headers[":path"] = u.pathname + (u.search || "");
|
|
266
|
+
h2Headers[":scheme"] = u.protocol === "https:" ? "https" : "http";
|
|
267
|
+
h2Headers[":authority"] = u.host;
|
|
268
|
+
for (var k in headers) {
|
|
269
|
+
if (!Object.prototype.hasOwnProperty.call(headers, k)) continue;
|
|
270
|
+
var lk = k.toLowerCase();
|
|
271
|
+
// h2 forbids the connection-specific headers
|
|
272
|
+
if (lk === "connection" || lk === "host" ||
|
|
273
|
+
lk === "keep-alive" || lk === "transfer-encoding" ||
|
|
274
|
+
lk === "upgrade" || lk === "proxy-connection") continue;
|
|
275
|
+
h2Headers[lk] = headers[k];
|
|
276
|
+
}
|
|
277
|
+
return h2Headers;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function _fromH2Headers(h2Headers) {
|
|
281
|
+
// Strip pseudo-headers from the response — caller doesn't want them
|
|
282
|
+
// mixed with normal headers.
|
|
283
|
+
var out = {};
|
|
284
|
+
for (var k in h2Headers) {
|
|
285
|
+
if (!Object.prototype.hasOwnProperty.call(h2Headers, k)) continue;
|
|
286
|
+
if (k.charAt(0) === ":") continue;
|
|
287
|
+
out[k] = h2Headers[k];
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---- request() ----
|
|
293
|
+
|
|
294
|
+
function request(opts) {
|
|
295
|
+
if (!opts || !opts.url) {
|
|
296
|
+
return Promise.reject(_makeError(opts && opts.errorClass, "BAD_ARG", "url is required", true));
|
|
297
|
+
}
|
|
298
|
+
// Validate scheme + shape via url-safe. Default is HTTPS-only — the
|
|
299
|
+
// framework refuses to silently drop bytes on the wire as cleartext.
|
|
300
|
+
// Callers with cleartext endpoints (h2c, internal services, test
|
|
301
|
+
// fixtures) explicitly opt in via opts.allowedProtocols
|
|
302
|
+
// (safeUrl.ALLOW_HTTP_ALL accepts both http: and https:).
|
|
303
|
+
var u;
|
|
304
|
+
try {
|
|
305
|
+
u = safeUrl.parse(opts.url, {
|
|
306
|
+
allowedProtocols: opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
307
|
+
errorClass: opts.errorClass,
|
|
308
|
+
});
|
|
309
|
+
} catch (e) {
|
|
310
|
+
return Promise.reject(e);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Caller-supplied agent bypasses transport cache (h1 only).
|
|
314
|
+
if (opts.agent) {
|
|
315
|
+
return _requestH1({
|
|
316
|
+
kind: "h1",
|
|
317
|
+
lib: u.protocol === "https:" ? https : http,
|
|
318
|
+
agent: opts.agent,
|
|
319
|
+
}, u, opts);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return _getTransport(u, opts).then(function (transport) {
|
|
323
|
+
if (transport.kind === "h2") return _requestH2(transport, u, opts);
|
|
324
|
+
return _requestH1(transport, u, opts);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---- _requestH1: existing node:http(s) path ----
|
|
329
|
+
|
|
330
|
+
function _requestH1(transport, u, opts) {
|
|
331
|
+
return new Promise(function (resolve, reject) {
|
|
332
|
+
var method = (opts.method || "GET").toUpperCase();
|
|
333
|
+
var headers = Object.assign({}, opts.headers || {});
|
|
334
|
+
var responseMode = opts.responseMode || "buffer";
|
|
335
|
+
var maxResponseBytes = opts.maxResponseBytes ||
|
|
336
|
+
(method === "GET" ? DEFAULT_GET_CAP : DEFAULT_CONTROL_PLANE_CAP);
|
|
337
|
+
var observer = typeof opts.observer === "function" ? opts.observer : null;
|
|
338
|
+
var startedAt = Date.now();
|
|
339
|
+
|
|
340
|
+
var signal = safeAsync.withTimeoutSignal(opts.signal || null, opts.timeoutMs);
|
|
341
|
+
if (signal && signal.aborted) {
|
|
342
|
+
var r0 = signal.reason;
|
|
343
|
+
var code0 = (r0 && r0.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
|
|
344
|
+
return reject(_makeError(opts.errorClass, code0,
|
|
345
|
+
(r0 && r0.message) || "request aborted before start", false));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (Buffer.isBuffer(opts.body)) {
|
|
349
|
+
headers["Content-Length"] = opts.body.length;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
var reqOpts = {
|
|
353
|
+
method: method,
|
|
354
|
+
hostname: u.hostname,
|
|
355
|
+
port: u.port || (u.protocol === "https:" ? 443 : 80),
|
|
356
|
+
path: u.pathname + (u.search || ""),
|
|
357
|
+
headers: headers,
|
|
358
|
+
agent: transport.agent,
|
|
359
|
+
timeout: typeof opts.idleTimeoutMs === "number" ? opts.idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
if (observer) observer("request:start", { method: method, url: String(opts.url), protocol: "h1" });
|
|
363
|
+
|
|
364
|
+
var settled = false;
|
|
365
|
+
function _resolve(value) { if (!settled) { settled = true; resolve(value); } }
|
|
366
|
+
function _reject(err) { if (!settled) { settled = true; reject(err); } }
|
|
367
|
+
|
|
368
|
+
var req = transport.lib.request(reqOpts, function (res) {
|
|
369
|
+
if (observer) observer("response:headers", { statusCode: res.statusCode, headers: res.headers });
|
|
370
|
+
|
|
371
|
+
if (responseMode === "stream") {
|
|
372
|
+
if (res.statusCode >= 400) {
|
|
373
|
+
res.resume();
|
|
374
|
+
return _reject(_makeError(opts.errorClass, "HTTP_ERROR",
|
|
375
|
+
"HTTP " + res.statusCode + " " + (res.statusMessage || ""),
|
|
376
|
+
_isPermanentStatus(res.statusCode), res.statusCode));
|
|
377
|
+
}
|
|
378
|
+
return _resolve({ statusCode: res.statusCode, headers: res.headers, body: res });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
var collector = safeBuffer.boundedChunkCollector({ maxBytes: maxResponseBytes });
|
|
382
|
+
var capExceeded = false;
|
|
383
|
+
|
|
384
|
+
res.on("data", function (chunk) {
|
|
385
|
+
if (capExceeded) return;
|
|
386
|
+
try { collector.push(chunk); }
|
|
387
|
+
catch (_e) {
|
|
388
|
+
capExceeded = true;
|
|
389
|
+
req.destroy();
|
|
390
|
+
_reject(_makeError(opts.errorClass, "RESPONSE_TOO_LARGE",
|
|
391
|
+
"response body exceeds " + maxResponseBytes + " bytes", true));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
res.on("end", function () {
|
|
395
|
+
if (capExceeded) return;
|
|
396
|
+
var buf = collector.result();
|
|
397
|
+
if (observer) observer("response:end", {
|
|
398
|
+
statusCode: res.statusCode,
|
|
399
|
+
durationMs: Date.now() - startedAt,
|
|
400
|
+
bytes: buf.length,
|
|
401
|
+
});
|
|
402
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
403
|
+
_resolve({ statusCode: res.statusCode, headers: res.headers, body: buf });
|
|
404
|
+
} else {
|
|
405
|
+
var msg = "HTTP " + res.statusCode + ": " + buf.toString("utf8").slice(0, 500);
|
|
406
|
+
_reject(_makeError(opts.errorClass, "HTTP_ERROR", msg,
|
|
407
|
+
_isPermanentStatus(res.statusCode), res.statusCode));
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
res.on("error", function (e) {
|
|
411
|
+
if (capExceeded) return;
|
|
412
|
+
if (observer) observer("error", { phase: "response", message: e.message });
|
|
413
|
+
_reject(_makeError(opts.errorClass, e.code || "RES_ERROR", e.message, false));
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
req.on("timeout", function () {
|
|
418
|
+
req.destroy();
|
|
419
|
+
_reject(_makeError(opts.errorClass, "ETIMEDOUT",
|
|
420
|
+
"request idle timeout (no data for " + reqOpts.timeout + "ms)", false));
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
req.on("error", function (e) {
|
|
424
|
+
if (observer) observer("error", { phase: "request", message: e.message });
|
|
425
|
+
_reject(_makeError(opts.errorClass, e.code || "REQ_ERROR", e.message, false));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
if (signal) {
|
|
429
|
+
var onAbort = function () {
|
|
430
|
+
var r = signal.reason;
|
|
431
|
+
var code = (r && r.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
|
|
432
|
+
var msg = (r && r.message) || "request aborted";
|
|
433
|
+
try { req.destroy(r || new Error(msg)); } catch (_e) {}
|
|
434
|
+
_reject(_makeError(opts.errorClass, code, msg, false));
|
|
435
|
+
};
|
|
436
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (opts.body && typeof opts.body.pipe === "function") {
|
|
440
|
+
opts.body.on("error", function (e) {
|
|
441
|
+
try { req.destroy(); } catch (_) {}
|
|
442
|
+
_reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
|
|
443
|
+
"request body stream error: " + e.message, false));
|
|
444
|
+
});
|
|
445
|
+
opts.body.pipe(req);
|
|
446
|
+
} else if (Buffer.isBuffer(opts.body)) {
|
|
447
|
+
req.end(opts.body);
|
|
448
|
+
} else if (typeof opts.body === "string") {
|
|
449
|
+
req.end(Buffer.from(opts.body, "utf8"));
|
|
450
|
+
} else {
|
|
451
|
+
req.end();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---- _requestH2: node:http2 path ----
|
|
457
|
+
|
|
458
|
+
function _requestH2(transport, u, opts) {
|
|
459
|
+
return new Promise(function (resolve, reject) {
|
|
460
|
+
var method = (opts.method || "GET").toUpperCase();
|
|
461
|
+
var responseMode = opts.responseMode || "buffer";
|
|
462
|
+
var maxResponseBytes = opts.maxResponseBytes ||
|
|
463
|
+
(method === "GET" ? DEFAULT_GET_CAP : DEFAULT_CONTROL_PLANE_CAP);
|
|
464
|
+
var observer = typeof opts.observer === "function" ? opts.observer : null;
|
|
465
|
+
var startedAt = Date.now();
|
|
466
|
+
|
|
467
|
+
var signal = safeAsync.withTimeoutSignal(opts.signal || null, opts.timeoutMs);
|
|
468
|
+
if (signal && signal.aborted) {
|
|
469
|
+
var r0 = signal.reason;
|
|
470
|
+
var code0 = (r0 && r0.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
|
|
471
|
+
return reject(_makeError(opts.errorClass, code0,
|
|
472
|
+
(r0 && r0.message) || "request aborted before start", false));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
var headers = _toH2Headers(method, u, opts.headers || {});
|
|
476
|
+
if (Buffer.isBuffer(opts.body)) headers["content-length"] = String(opts.body.length);
|
|
477
|
+
|
|
478
|
+
if (observer) observer("request:start", { method: method, url: String(opts.url), protocol: "h2" });
|
|
479
|
+
|
|
480
|
+
var stream;
|
|
481
|
+
try {
|
|
482
|
+
stream = transport.session.request(headers, {
|
|
483
|
+
endStream: opts.body == null,
|
|
484
|
+
});
|
|
485
|
+
} catch (e) {
|
|
486
|
+
return reject(_makeError(opts.errorClass, e.code || "H2_REQUEST_ERROR", e.message, false));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
var settled = false;
|
|
490
|
+
function _resolve(v) { if (!settled) { settled = true; resolve(v); } }
|
|
491
|
+
function _reject(e) { if (!settled) { settled = true; reject(e); } }
|
|
492
|
+
|
|
493
|
+
// Idle timeout for the stream itself (zero-progress detector).
|
|
494
|
+
var idleMs = typeof opts.idleTimeoutMs === "number" ? opts.idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS;
|
|
495
|
+
stream.setTimeout(idleMs, function () {
|
|
496
|
+
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) {}
|
|
497
|
+
_reject(_makeError(opts.errorClass, "ETIMEDOUT",
|
|
498
|
+
"h2 stream idle timeout (no data for " + idleMs + "ms)", false));
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
stream.on("response", function (resHeaders) {
|
|
502
|
+
var statusCode = resHeaders[":status"];
|
|
503
|
+
var responseHeaders = _fromH2Headers(resHeaders);
|
|
504
|
+
|
|
505
|
+
if (observer) observer("response:headers", { statusCode: statusCode, headers: responseHeaders });
|
|
506
|
+
|
|
507
|
+
if (responseMode === "stream") {
|
|
508
|
+
if (statusCode >= 400) {
|
|
509
|
+
stream.resume();
|
|
510
|
+
return _reject(_makeError(opts.errorClass, "HTTP_ERROR",
|
|
511
|
+
"HTTP " + statusCode, _isPermanentStatus(statusCode), statusCode));
|
|
512
|
+
}
|
|
513
|
+
return _resolve({ statusCode: statusCode, headers: responseHeaders, body: stream });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
var collector = safeBuffer.boundedChunkCollector({ maxBytes: maxResponseBytes });
|
|
517
|
+
var capExceeded = false;
|
|
518
|
+
|
|
519
|
+
stream.on("data", function (chunk) {
|
|
520
|
+
if (capExceeded) return;
|
|
521
|
+
try { collector.push(chunk); }
|
|
522
|
+
catch (_e) {
|
|
523
|
+
capExceeded = true;
|
|
524
|
+
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e2) {}
|
|
525
|
+
_reject(_makeError(opts.errorClass, "RESPONSE_TOO_LARGE",
|
|
526
|
+
"response body exceeds " + maxResponseBytes + " bytes", true));
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
stream.on("end", function () {
|
|
530
|
+
if (capExceeded) return;
|
|
531
|
+
var buf = collector.result();
|
|
532
|
+
if (observer) observer("response:end", {
|
|
533
|
+
statusCode: statusCode,
|
|
534
|
+
durationMs: Date.now() - startedAt,
|
|
535
|
+
bytes: buf.length,
|
|
536
|
+
});
|
|
537
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
538
|
+
_resolve({ statusCode: statusCode, headers: responseHeaders, body: buf });
|
|
539
|
+
} else {
|
|
540
|
+
var msg = "HTTP " + statusCode + ": " + buf.toString("utf8").slice(0, 500);
|
|
541
|
+
_reject(_makeError(opts.errorClass, "HTTP_ERROR", msg,
|
|
542
|
+
_isPermanentStatus(statusCode), statusCode));
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
stream.on("error", function (e) {
|
|
548
|
+
if (observer) observer("error", { phase: "stream", message: e.message });
|
|
549
|
+
_reject(_makeError(opts.errorClass, e.code || "H2_STREAM_ERROR", e.message, false));
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (signal) {
|
|
553
|
+
var onAbort = function () {
|
|
554
|
+
var r = signal.reason;
|
|
555
|
+
var code = (r && r.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
|
|
556
|
+
var msg = (r && r.message) || "request aborted";
|
|
557
|
+
// NGHTTP2_CANCEL is the protocol-level "I gave up" signal —
|
|
558
|
+
// cleaner than destroying the stream.
|
|
559
|
+
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) {}
|
|
560
|
+
_reject(_makeError(opts.errorClass, code, msg, false));
|
|
561
|
+
};
|
|
562
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (opts.body && typeof opts.body.pipe === "function") {
|
|
566
|
+
opts.body.on("error", function (e) {
|
|
567
|
+
try { stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR); } catch (_) {}
|
|
568
|
+
_reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
|
|
569
|
+
"request body stream error: " + e.message, false));
|
|
570
|
+
});
|
|
571
|
+
opts.body.pipe(stream);
|
|
572
|
+
} else if (Buffer.isBuffer(opts.body)) {
|
|
573
|
+
stream.end(opts.body);
|
|
574
|
+
} else if (typeof opts.body === "string") {
|
|
575
|
+
stream.end(Buffer.from(opts.body, "utf8"));
|
|
576
|
+
}
|
|
577
|
+
// If body is null/undefined, endStream:true was set in session.request()
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ---- Test helpers ----
|
|
582
|
+
|
|
583
|
+
function _resetForTest() {
|
|
584
|
+
_transports.forEach(function (t) {
|
|
585
|
+
if (t && t.kind === "h1" && t.agent && typeof t.agent.destroy === "function") {
|
|
586
|
+
try { t.agent.destroy(); } catch (_e) {}
|
|
587
|
+
}
|
|
588
|
+
if (t && t.kind === "h2" && t.session) {
|
|
589
|
+
try { t.session.close(); } catch (_e) {}
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
_transports.clear();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function _getCachedTransportCount() {
|
|
596
|
+
return _transports.size;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Diagnostic — returns 'h1' | 'h2' | null for a given URL's cached transport.
|
|
600
|
+
function _getCachedTransportKind(url) {
|
|
601
|
+
var u = url instanceof URL ? url : new URL(url);
|
|
602
|
+
var t = _transports.get(_originKey(u));
|
|
603
|
+
if (!t) return null;
|
|
604
|
+
if (t.then) return "pending";
|
|
605
|
+
return t.kind;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
module.exports = {
|
|
609
|
+
request: request,
|
|
610
|
+
DEFAULT_CONTROL_PLANE_CAP: DEFAULT_CONTROL_PLANE_CAP,
|
|
611
|
+
DEFAULT_GET_CAP: DEFAULT_GET_CAP,
|
|
612
|
+
_resetForTest: _resetForTest,
|
|
613
|
+
_getCachedTransportCount: _getCachedTransportCount,
|
|
614
|
+
_getCachedTransportKind: _getCachedTransportKind,
|
|
615
|
+
};
|