@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.
Files changed (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. 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
+ };