kinetex 1.4.0 → 1.5.0
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/README.md +170 -46
- package/dist/browser/kinetex.esm.js +29 -29
- package/dist/browser/kinetex.js +1143 -791
- package/dist/browser/kinetex.min.js +29 -29
- package/dist/cjs/aws-sigv4.js +7 -14
- package/dist/cjs/cache.js +9 -0
- package/dist/cjs/circuit-breaker.js +8 -1
- package/dist/cjs/client.js +350 -95
- package/dist/cjs/cookie-parser.js +28 -1
- package/dist/cjs/core.js +227 -5
- package/dist/cjs/dedup.js +93 -10
- package/dist/cjs/digest.js +36 -15
- package/dist/cjs/graphql.js +2 -11
- package/dist/cjs/headers.js +24 -1
- package/dist/cjs/interceptors.js +6 -17
- package/dist/cjs/lifecycle.js +3 -19
- package/dist/cjs/logging.js +12 -29
- package/dist/cjs/progress.js +1 -3
- package/dist/cjs/proxy.js +19 -2
- package/dist/cjs/response.js +202 -75
- package/dist/cjs/socks5.js +5 -0
- package/dist/cjs/utils.js +115 -27
- package/dist/cjs/ws.js +43 -2
- package/dist/esm/aws-sigv4.js +7 -14
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +9 -0
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +8 -1
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +350 -95
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +28 -1
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/core.js +227 -5
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +93 -10
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +36 -15
- package/dist/esm/digest.js.map +1 -1
- package/dist/esm/graphql.js +2 -11
- package/dist/esm/graphql.js.map +1 -1
- package/dist/esm/headers.js +24 -1
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +6 -17
- package/dist/esm/interceptors.js.map +1 -1
- package/dist/esm/lifecycle.js +3 -19
- package/dist/esm/lifecycle.js.map +1 -1
- package/dist/esm/logging.js +12 -29
- package/dist/esm/logging.js.map +1 -1
- package/dist/esm/progress.js +1 -3
- package/dist/esm/progress.js.map +1 -1
- package/dist/esm/proxy.js +19 -2
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/response.js +202 -75
- package/dist/esm/response.js.map +1 -1
- package/dist/esm/socks5.js +5 -0
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/url.js.map +1 -1
- package/dist/esm/utils.js +115 -27
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/ws.js +43 -2
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/circuit-breaker.d.ts +1 -1
- package/dist/types/circuit-breaker.d.ts.map +1 -1
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/cookie-parser.d.ts.map +1 -1
- package/dist/types/core.d.ts +113 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts +38 -3
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts.map +1 -1
- package/dist/types/graphql.d.ts.map +1 -1
- package/dist/types/headers.d.ts +23 -4
- package/dist/types/headers.d.ts.map +1 -1
- package/dist/types/interceptors.d.ts +24 -38
- package/dist/types/interceptors.d.ts.map +1 -1
- package/dist/types/lifecycle.d.ts.map +1 -1
- package/dist/types/logging.d.ts.map +1 -1
- package/dist/types/progress.d.ts.map +1 -1
- package/dist/types/proxy.d.ts.map +1 -1
- package/dist/types/response.d.ts +42 -1
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/socks5.d.ts.map +1 -1
- package/dist/types/types.d.ts +105 -23
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/url.d.ts +0 -11
- package/dist/types/url.d.ts.map +1 -1
- package/dist/types/utils.d.ts +69 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/ws.d.ts +0 -1
- package/dist/types/ws.d.ts.map +1 -1
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -23,13 +23,14 @@ const users = await api.get<User[]>("/users");
|
|
|
23
23
|
const post = await api.POST("/posts").withJSON({ title: "Hello" }).bearer("token").json<Post>();
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Works in **Node.js
|
|
26
|
+
Works in **Node.js 22+** (LTS lines: 22, 24, 26), **Deno**, **Bun**, **browsers**, **Cloudflare Workers**, **Vercel Edge**, and all **WinterCG-compliant** runtimes.
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
30
|
## Table of Contents
|
|
31
31
|
|
|
32
32
|
- [Installation](#installation)
|
|
33
|
+
- [Migrating from 1.4.x](#migrating-from-14x)
|
|
33
34
|
- [Quick Start](#quick-start)
|
|
34
35
|
- [Client Configuration](#client-configuration)
|
|
35
36
|
- [Basic HTTP Methods](#basic-http-methods)
|
|
@@ -61,6 +62,7 @@ Works in **Node.js 18+**, **Deno**, **Bun**, **browsers**, **Cloudflare Workers*
|
|
|
61
62
|
- [HAR Recording](#har-recording)
|
|
62
63
|
- [OpenTelemetry Tracing](#opentelemetry-tracing)
|
|
63
64
|
- [Pipeline Trace](#pipeline-trace)
|
|
65
|
+
- [Private networks, DNS and TLS](#private-networks-dns-and-tls)
|
|
64
66
|
- [Transport Layer](#transport-layer)
|
|
65
67
|
- [Runtime Detection](#runtime-detection)
|
|
66
68
|
- [Child Clients](#child-clients)
|
|
@@ -102,6 +104,37 @@ import { kinetex } from "jsr:@kinetexjs/kinetex";
|
|
|
102
104
|
|
|
103
105
|
---
|
|
104
106
|
|
|
107
|
+
## Migrating from 1.4.x
|
|
108
|
+
|
|
109
|
+
1.5.0 contains three breaking changes. Each has a one-line opt-out, so none of them requires you to change anything unless the new default breaks you.
|
|
110
|
+
|
|
111
|
+
**`maxResponseSize` now defaults to 100 MB** instead of unlimited. The body is read in full before you see any of it, so an unbounded default let a single response exhaust the heap. If you legitimately receive responses over 100 MB:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
kinetex({ maxResponseSize: 0 }); // 0 has always meant "no limit"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**`totalTimeoutMs` now defaults to the effective `timeout`.** `timeout` bounds one attempt, and the backoff between attempts was not bounded by anything — `timeout: 1_000` with `maxRetries: 5` could hold the caller for roughly 37 seconds. The whole-request ceiling now defaults to `timeout`, so if you were relying on retries outliving `timeout`, raise it or remove it:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
kinetex({ timeout: 1_000, totalTimeoutMs: 0 }); // unbounded retries, as in 1.4.x
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**`URLBuilderOptions` is removed.** It was exported from `kinetex/url` but no constructor, factory or method accepted it, so it could never have been passed anywhere. If you imported the name as a type, drop it — `from()` takes a base URL directly, and `sortParams` is a real method.
|
|
124
|
+
|
|
125
|
+
Behaviour changes that are not breaking, but will change what you observe:
|
|
126
|
+
|
|
127
|
+
- A request carrying an `Idempotency-Key` is now retried whatever its method. An unkeyed POST is still attempted exactly once.
|
|
128
|
+
- `Cache-Control: private` responses are no longer cached. This cache is a `Map` in your process, shared across every request it makes, so storing a per-user response served it to whoever asked next. `honorCacheControl: false` opts out.
|
|
129
|
+
- `SameSite=Lax` and `SameSite=Strict` cookies are now sent. The client stated no SameSite context, which by design sends only `Unset` and `None`, so most real sessions were going out without their cookie.
|
|
130
|
+
- A 307/308 redirect carrying a stream or `Blob` body is now refused with a clear `EVALIDATION` rather than failing later as an opaque `ENETWORK`. Those methods preserve the body, and a one-shot body is spent after the first attempt. 301/302/303 are unaffected — they downgrade to GET.
|
|
131
|
+
- A `WebSocket.send()` on a socket that is not OPEN, with `bufferMessages: false`, now reports through `onError` instead of dropping the message silently.
|
|
132
|
+
- A caller-supplied `fetch` is exempt from the pre-flight DNS screen and owns its own resolution. The lexical screen still applies to the initial URL and every redirect hop.
|
|
133
|
+
|
|
134
|
+
See [`CHANGELOG.md`](./CHANGELOG.md) for the full list, including the defects this release fixes.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
105
138
|
## Quick Start
|
|
106
139
|
|
|
107
140
|
```ts
|
|
@@ -155,8 +188,10 @@ const client = kinetex({
|
|
|
155
188
|
// (see "Redirects" under Transport Layer) — the transport is never
|
|
156
189
|
// asked to follow, whatever this is set to
|
|
157
190
|
httpsOnly: false, // Reject non-HTTPS URLs
|
|
158
|
-
maxResponseSize: 10_000_000, // Response body size limit (0 = no limit)
|
|
191
|
+
maxResponseSize: 10_000_000, // Response body size limit (default: 100 MB; 0 = no limit)
|
|
159
192
|
maxRequestSize: 10_000_000, // Request body size limit (0 = no limit)
|
|
193
|
+
ca: [readFileSync("./corp-root.pem", "utf8")], // Extra CAs to trust (Node transports)
|
|
194
|
+
allowPrivateNetwork: false, // Permit private/loopback destinations
|
|
160
195
|
strictHeaders: false, // Throw on invalid headers vs warn+drop
|
|
161
196
|
onPipelineTrace: (step) => console.log(step), // Pipeline observability callback
|
|
162
197
|
onSWRError: (err, req) => log(err), // Background SWR revalidation error callback
|
|
@@ -382,7 +417,7 @@ interface SendOptions<T = unknown> {
|
|
|
382
417
|
maxRedirects?: number; // Max redirect hops (default: 20; 0 disables following)
|
|
383
418
|
httpVersion?: HTTPVersion; // Preferred HTTP version
|
|
384
419
|
maxRequestSize?: number; // Request size limit (bytes)
|
|
385
|
-
maxResponseSize?: number; // Response size limit (bytes)
|
|
420
|
+
maxResponseSize?: number; // Response size limit (bytes, default 104857600; 0 = unlimited)
|
|
386
421
|
parseResponse?: (raw: Uint8Array, headers: Record<string, string>, url: string) => T | Promise<T>;
|
|
387
422
|
parseFailure?: (raw: Uint8Array, error: Error) => void;
|
|
388
423
|
onSuccess?: (res: KinetexResponse<T>) => void;
|
|
@@ -470,12 +505,16 @@ client.GET("/public").noAuth().json();
|
|
|
470
505
|
kinetex({
|
|
471
506
|
retry: {
|
|
472
507
|
maxRetries: 3, // Max attempts (default: 3)
|
|
508
|
+
totalTimeoutMs: 20_000, // Ceiling on the WHOLE request: attempts + backoff (default: = timeout)
|
|
473
509
|
baseDelayMs: 300, // Exponential back-off base (default: 300)
|
|
474
510
|
maxDelayMs: 30_000, // Max delay cap (default: 30_000)
|
|
475
511
|
jitter: 0.3, // Random jitter factor 0-1 (default: 0.3)
|
|
476
512
|
statuses: [408, 429, 500, 502, 503, 504], // Status codes that trigger retry
|
|
477
513
|
onNetworkError: true, // Retry on network errors (default: true)
|
|
478
514
|
onTimeout: false, // Retry on timeouts (default: false)
|
|
515
|
+
// Only these methods are retried. A request carrying an `Idempotency-Key`
|
|
516
|
+
// is retried whatever its method — the key is the caller's declaration
|
|
517
|
+
// that repeating it is safe. See "Idempotency Keys" below.
|
|
479
518
|
methods: ["GET", "HEAD", "PUT", "DELETE", "OPTIONS", "TRACE"],
|
|
480
519
|
shouldRetry: (ctx) => ctx.attempt < 3 && ctx.response?.status === 503, // Custom predicate
|
|
481
520
|
onRetry: (ctx, delayMs) => console.log(`Retry ${ctx.attempt} in ${delayMs}ms`),
|
|
@@ -483,6 +522,38 @@ kinetex({
|
|
|
483
522
|
});
|
|
484
523
|
```
|
|
485
524
|
|
|
525
|
+
`timeout` applies to a single attempt; `totalTimeoutMs` bounds the whole logical
|
|
526
|
+
request, backoff included, and defaults to `timeout`. Set it to `0` to remove
|
|
527
|
+
the ceiling. When the next backoff would exceed the budget the retry is skipped
|
|
528
|
+
rather than slept through, and the original response or error is what the caller
|
|
529
|
+
receives — the budget never substitutes an `ETIMEOUT` for a real cause.
|
|
530
|
+
|
|
531
|
+
### Idempotency Keys
|
|
532
|
+
|
|
533
|
+
A `POST` is not retried by default, because replaying an unkeyed write can
|
|
534
|
+
double-charge someone. A request that carries an `Idempotency-Key` **is** retried
|
|
535
|
+
whatever its method: the header exists precisely to declare the request
|
|
536
|
+
replayable, and the server collapses the duplicates.
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
import { generateIdempotencyKey, isValidIdempotencyKey } from "kinetex";
|
|
540
|
+
|
|
541
|
+
// Keys are RFC 9562 v4 UUIDs from the runtime CSPRNG.
|
|
542
|
+
const key = generateIdempotencyKey();
|
|
543
|
+
isValidIdempotencyKey(key); // true
|
|
544
|
+
|
|
545
|
+
// Any method, retried on transient failures, same key every attempt —
|
|
546
|
+
// a fresh key per attempt would defeat the mechanism.
|
|
547
|
+
await client.post("/charges", body, { headers: { "idempotency-key": key } });
|
|
548
|
+
|
|
549
|
+
// Or fluently:
|
|
550
|
+
await client.POST("/charges").withJSON(body).idempotencyKey().json();
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
An unkeyed `POST` is still attempted exactly once. Validation is 1–255 visible
|
|
554
|
+
ASCII, so CR/LF/NUL cannot be smuggled into the headers, and a runtime without
|
|
555
|
+
`crypto.getRandomValues` throws rather than falling back to a guessable value.
|
|
556
|
+
|
|
486
557
|
### RetryContext
|
|
487
558
|
|
|
488
559
|
The `ctx` parameter in `shouldRetry` and `onRetry` is of type `RetryContext`:
|
|
@@ -935,7 +1006,7 @@ interface HookOptions {
|
|
|
935
1006
|
|
|
936
1007
|
## Request Deduplication
|
|
937
1008
|
|
|
938
|
-
Coalesces identical concurrent GET/HEAD requests into a single network call. The dedup key includes a SHA-256 fingerprint of every
|
|
1009
|
+
Coalesces identical concurrent GET/HEAD requests into a single network call. The dedup key includes a SHA-256 fingerprint of **every request header**, so two callers are only ever coalesced when they would have received the same response — different `Authorization`, `Cookie`, API key _or_ a tenant/session header each get their own in-flight slot. Per-request tracing headers (`traceparent`, `tracestate`, `baggage`, `x-request-id`, `x-correlation-id`, `x-trace-id`) are excluded, since every request carries its own and including them would disable coalescing entirely:
|
|
939
1010
|
|
|
940
1011
|
```ts
|
|
941
1012
|
import { CREDENTIAL_HEADERS } from "kinetex/cache";
|
|
@@ -1082,7 +1153,7 @@ const client = kinetex({
|
|
|
1082
1153
|
// maxBodySizeBytes: 5 * 1024 * 1024, // skip caching larger bodies
|
|
1083
1154
|
// defaultTtlMs: 60_000, // when the response has no Cache-Control (max 1 year)
|
|
1084
1155
|
// maxAbsoluteAgeMs: 7 * 24 * 60 * 60 * 1000, // hard cap regardless of Cache-Control
|
|
1085
|
-
// honorCacheControl: true, // respect no-store / no-cache
|
|
1156
|
+
// honorCacheControl: true, // respect no-store / no-cache / private
|
|
1086
1157
|
// cacheMethods: ["GET", "HEAD"],
|
|
1087
1158
|
// cacheStatuses: [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501],
|
|
1088
1159
|
storage: new MemoryStorageAdapter(), // omit for plain in-memory
|
|
@@ -1687,7 +1758,7 @@ const ws = new WSClient({
|
|
|
1687
1758
|
lowWaterMark: 16384,
|
|
1688
1759
|
maxSendRate: 0, // 0 = unlimited
|
|
1689
1760
|
keepRooms: true,
|
|
1690
|
-
bufferMessages: true,
|
|
1761
|
+
bufferMessages: true, // queue sends made while reconnecting; see below
|
|
1691
1762
|
maxBufferSize: 1000,
|
|
1692
1763
|
rooms: ["prices"],
|
|
1693
1764
|
signal: controller.signal,
|
|
@@ -1750,6 +1821,11 @@ ws.leave("prices");
|
|
|
1750
1821
|
ws.rooms; // readonly WSSubscribedRoom[]
|
|
1751
1822
|
|
|
1752
1823
|
// Backpressure
|
|
1824
|
+
// With `bufferMessages: true` (default) a send made while the socket is not
|
|
1825
|
+
// OPEN is queued, up to `maxBufferSize`, oldest evicted first. With it false
|
|
1826
|
+
// there is nowhere to put that message: it is dropped, and the drop is reported
|
|
1827
|
+
// through `onError` — `send()` returns void, so silence would make a lost
|
|
1828
|
+
// message indistinguishable from a delivered one.
|
|
1753
1829
|
ws.backpressure; // { bufferedBytes, highWaterMark, lowWaterMark, isBackpressured, ... }
|
|
1754
1830
|
await ws.drain(30_000); // Wait until outbound buffer is flushed
|
|
1755
1831
|
await ws.drainAndClose(30_000); // Drain, then close gracefully
|
|
@@ -2381,7 +2457,7 @@ HAR logs are routinely exported and shared, so entries are redacted before they
|
|
|
2381
2457
|
- **URLs** — sensitive query parameters (`api_key`, `access_token`, `signature`, `password`, `code`, `sas`, …) and the fragment are masked, in both `request.url` and `request.queryString[]`. Non-sensitive parameters and the rest of the URL are preserved so the log stays useful.
|
|
2382
2458
|
- **`Location`** — the redirect target is passed through the same URL redaction.
|
|
2383
2459
|
- **Bodies** — response text is recorded only for `json`/`xml`/`text/plain`/`javascript` content types and truncated to 8 KiB; HTML and binary bodies are never recorded.
|
|
2384
|
-
- **Request bodies** — recorded as `request.postData` (`{ mimeType, text }`) under the same policy and the same limit, so a HAR viewer shows what was actually sent. A `ReadableStream` or `FormData` body is omitted rather than buffered, since reading it would consume it; `request.bodySize` is `-1` for those, as it always was.
|
|
2460
|
+
- **Request bodies** — recorded as `request.postData` (`{ mimeType, text }`) under the same policy and the same limit, so a HAR viewer shows what was actually sent. A `ReadableStream` or `FormData` body is omitted rather than buffered, since reading it would consume it; `request.bodySize` is `-1` for those, as it always was. For a `json` body the same sensitive names used for headers and query parameters (`password`, `token`, `api_key`, `secret`, …) are replaced with `***REDACTED***` at any nesting depth, since a JSON body is where credentials most often travel and HAR files get shared. Non-JSON bodies keep their text, but the literal values of this request's own credentials — the credential header, the part after its scheme, and sensitive query parameters — are still replaced wherever they appear, which catches a server echoing a token back in an error message.
|
|
2385
2461
|
|
|
2386
2462
|
```ts
|
|
2387
2463
|
// ?api_key=SUPERSECRET&page=2 → https://api.example.com/v1/items?api_key=***REDACTED***&page=2
|
|
@@ -2465,6 +2541,61 @@ Pipeline stages in order:
|
|
|
2465
2541
|
|
|
2466
2542
|
---
|
|
2467
2543
|
|
|
2544
|
+
## Private networks, DNS and TLS
|
|
2545
|
+
|
|
2546
|
+
```ts
|
|
2547
|
+
const client = kinetex({
|
|
2548
|
+
baseURL: "https://internal.svc",
|
|
2549
|
+
|
|
2550
|
+
// Trust a private CA alongside the system store. Without this, a service mesh
|
|
2551
|
+
// or enterprise proxy whose certificate a corporate CA issued fails TLS.
|
|
2552
|
+
// Node's transports only — `fetch` exposes no CA option, so a custom `fetch`
|
|
2553
|
+
// or `dispatcher` setup has to carry its own.
|
|
2554
|
+
ca: [readFileSync("./corp-root.pem", "utf8")],
|
|
2555
|
+
|
|
2556
|
+
// Reach localhost, a service mesh or a local dev server. Off by default.
|
|
2557
|
+
allowPrivateNetwork: true,
|
|
2558
|
+
});
|
|
2559
|
+
```
|
|
2560
|
+
|
|
2561
|
+
### How the screen works
|
|
2562
|
+
|
|
2563
|
+
A URL is a name, not an address, so screening the text you typed does not
|
|
2564
|
+
describe what the socket will do. Three layers close that gap:
|
|
2565
|
+
|
|
2566
|
+
1. **Lexical.** `isSafeURL` rejects a literal loopback, private, link-local or
|
|
2567
|
+
otherwise blocked address, on the initial URL and on every redirect hop.
|
|
2568
|
+
2. **Resolution, pinned (Node).** `NodeHTTP2Transport` and the HTTP/1.1 path
|
|
2569
|
+
resolve once and hand the _checked_ address to the socket
|
|
2570
|
+
(`createPinnedLookup`). A name answering with both a public and a private
|
|
2571
|
+
address keeps only the public one; one answering with only private space
|
|
2572
|
+
fails with `EBLOCKEDHOST` rather than connecting.
|
|
2573
|
+
3. **Resolution, pre-flight (`FetchTransport`).** `fetch` exposes no resolver
|
|
2574
|
+
hook — undici owns the connection — so `FetchTransport` resolves before the
|
|
2575
|
+
call and refuses a host whose answers are all blocked. This layer applies
|
|
2576
|
+
only when kinetex is using the runtime's own `fetch`. Supply your own via
|
|
2577
|
+
the `fetch` option and you own resolution end to end: kinetex screens every
|
|
2578
|
+
URL lexically, but it cannot observe which address your dispatcher dials, so
|
|
2579
|
+
checking the system resolver's answer would describe a lookup that never
|
|
2580
|
+
happens. Treat `fetch` the way you would treat a custom `dispatcher` — a
|
|
2581
|
+
decision you have taken responsibility for.
|
|
2582
|
+
|
|
2583
|
+
The second layer closes rebinding completely, because the address that passed
|
|
2584
|
+
the check is the address that is dialled. The third cannot: it is a check
|
|
2585
|
+
followed by a separate resolution inside `fetch`, so a resolver that answers
|
|
2586
|
+
public on the first lookup and private on the second is not caught here. On
|
|
2587
|
+
Node you get the pinned path; on Deno, Bun and edge runtimes this is the
|
|
2588
|
+
residual exposure, and it is worth a runtime-level DNS policy where one is
|
|
2589
|
+
available.
|
|
2590
|
+
|
|
2591
|
+
### What the opt-out really does
|
|
2592
|
+
|
|
2593
|
+
`allowPrivateNetwork: true` is not a narrower exception — it turns the screen
|
|
2594
|
+
off for the initial URL _and_ for every redirect hop, and it is the reason a
|
|
2595
|
+
redirect to `http://127.0.0.1:9/` is followed rather than refused. Set it only
|
|
2596
|
+
where the URLs you request are not influenced by anyone else: local
|
|
2597
|
+
development, a service mesh you control, and the test suite.
|
|
2598
|
+
|
|
2468
2599
|
## Transport Layer
|
|
2469
2600
|
|
|
2470
2601
|
### Redirects
|
|
@@ -2490,7 +2621,10 @@ Every hop is subject to:
|
|
|
2490
2621
|
- **Loop detection.** A target already visited in this chain is refused with
|
|
2491
2622
|
`RedirectError` (`EREDIRECT`, "Redirect loop detected").
|
|
2492
2623
|
- **Method downgrade.** Per RFC 7231, 301/302/303 downgrade to `GET` and drop
|
|
2493
|
-
the body; 307/308 preserve both.
|
|
2624
|
+
the body; 307/308 preserve both. A 307/308 therefore needs the body a second
|
|
2625
|
+
time, and a `ReadableStream`/`Blob` body is spent after the first attempt — so
|
|
2626
|
+
that hop is refused with `EVALIDATION` rather than silently sent empty. Buffer
|
|
2627
|
+
the body first, or disable redirects for that request.
|
|
2494
2628
|
- **Credentials.** `Authorization`, `apikey` headers, `Cookie` and any
|
|
2495
2629
|
declared auth are dropped when a hop changes origin, and kept when it does
|
|
2496
2630
|
not. Intermediate `Set-Cookie` headers are captured by the jar, so cookies
|
|
@@ -2804,14 +2938,7 @@ import {
|
|
|
2804
2938
|
buildDataURL,
|
|
2805
2939
|
diffURLs,
|
|
2806
2940
|
} from "kinetex/url";
|
|
2807
|
-
import type {
|
|
2808
|
-
ParsedURL,
|
|
2809
|
-
URLBuilderOptions,
|
|
2810
|
-
URLPattern,
|
|
2811
|
-
URLPatternMatch,
|
|
2812
|
-
URLDiff,
|
|
2813
|
-
DataURLParts,
|
|
2814
|
-
} from "kinetex/url";
|
|
2941
|
+
import type { ParsedURL, URLPattern, URLPatternMatch, URLDiff, DataURLParts } from "kinetex/url";
|
|
2815
2942
|
|
|
2816
2943
|
// URL Builder (fluent, immutable — every method returns a new builder)
|
|
2817
2944
|
const builder = URLBuilder.from("https://api.example.com")
|
|
@@ -3226,8 +3353,12 @@ isSafeURL("http://169.254.169.254/latest/meta-data/"); // false
|
|
|
3226
3353
|
isSafeURL("http://[::ffff:127.0.0.1]/"); // false
|
|
3227
3354
|
|
|
3228
3355
|
// Applied to the initial URL AND to every redirect hop, so a public host cannot
|
|
3229
|
-
// bounce a request into the private network.
|
|
3230
|
-
//
|
|
3356
|
+
// bounce a request into the private network.
|
|
3357
|
+
//
|
|
3358
|
+
// These helpers are literal-address checks. Resolution is handled separately by
|
|
3359
|
+
// the transports: the Node ones resolve once and pin the checked address to the
|
|
3360
|
+
// socket, and `FetchTransport` resolves before the call and refuses a host whose
|
|
3361
|
+
// answers are all blocked. See "Private networks and DNS" below.
|
|
3231
3362
|
sanitizeURL("javascript:alert(1)"); // null — invalid or an SSRF risk
|
|
3232
3363
|
sanitizeURL("https://user:pass@api.example.com/x"); // "https://api.example.com/x" (credentials stripped)
|
|
3233
3364
|
|
|
@@ -3489,14 +3620,7 @@ import type { Socks5ProxyConfig, Socks5Tunnel, Socks5Target, TcpConnector } from
|
|
|
3489
3620
|
import type { FetchTransportOptions } from "kinetex/core";
|
|
3490
3621
|
import type { OTelTracer, OTelSpan } from "kinetex";
|
|
3491
3622
|
import type { SafeJSONParseOptions, SafeJSONParseResult, ErrorContext } from "kinetex";
|
|
3492
|
-
import type {
|
|
3493
|
-
ParsedURL,
|
|
3494
|
-
URLBuilderOptions,
|
|
3495
|
-
URLPattern,
|
|
3496
|
-
URLPatternMatch,
|
|
3497
|
-
URLDiff,
|
|
3498
|
-
DataURLParts,
|
|
3499
|
-
} from "kinetex/url";
|
|
3623
|
+
import type { ParsedURL, URLPattern, URLPatternMatch, URLDiff, DataURLParts } from "kinetex/url";
|
|
3500
3624
|
```
|
|
3501
3625
|
|
|
3502
3626
|
---
|
|
@@ -3567,26 +3691,26 @@ import { kinetex } from "kinetex/browser";
|
|
|
3567
3691
|
|
|
3568
3692
|
## Runtime Compatibility
|
|
3569
3693
|
|
|
3570
|
-
| Feature | Node
|
|
3571
|
-
| ----------------------------------------- |
|
|
3572
|
-
| HTTP/1.1 fetch | ✓
|
|
3573
|
-
| HTTP/2 (fetch, via Alt-Svc/runtime hints) | ✓*
|
|
3574
|
-
| HTTP/2 (NodeHTTP2Transport) |
|
|
3575
|
-
| HTTP/3 (detection via Alt-Svc) | ✓*
|
|
3576
|
-
| WebSocket (WSClient) |
|
|
3577
|
-
| SOCKS5 proxy | ✓
|
|
3578
|
-
| Blob | ✓
|
|
3579
|
-
| DOMException | ✓
|
|
3580
|
-
| Buffer | ✓
|
|
3581
|
-
| crypto.subtle | ✓
|
|
3582
|
-
| ReadableStream | ✓
|
|
3583
|
-
| URL pattern matching | ✓
|
|
3584
|
-
| Brotli decompression | ✓
|
|
3585
|
-
| Gzip/deflate decompression | ✓
|
|
3586
|
-
|
|
3587
|
-
\* HTTP/2+ detection is best-effort and internal. kinetex's own `detectHTTPVersion()` helper (not exported) reports HTTP/2 only when the runtime exposes protocol evidence — a response `httpVersion`/`protocol` property, or an `Alt-Svc` header — and otherwise reports `HTTP/1.1`. The version on every `KinetexResponse` comes from that heuristic, so it is not a guarantee. This is accurate for
|
|
3588
|
-
|
|
3589
|
-
¹ WSClient requires a native `WebSocket` constructor
|
|
3694
|
+
| Feature | Node 22+ | Deno | Bun | Browser | CF Workers | Vercel Edge |
|
|
3695
|
+
| ----------------------------------------- | -------- | ---- | ------------- | ------------- | ------------- | ------------- |
|
|
3696
|
+
| HTTP/1.1 fetch | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
3697
|
+
| HTTP/2 (fetch, via Alt-Svc/runtime hints) | ✓* | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
3698
|
+
| HTTP/2 (NodeHTTP2Transport) | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
3699
|
+
| HTTP/3 (detection via Alt-Svc) | ✓* | ✓* | ✓* | experimental | ✓* | ✓* |
|
|
3700
|
+
| WebSocket (WSClient) | ✓ | ✓ | ✓ | ✓ | partial² | ✗³ |
|
|
3701
|
+
| SOCKS5 proxy | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
|
|
3702
|
+
| Blob | ✓ | ✓ | ✓ | ✓ | ✓ | guarded | guarded |
|
|
3703
|
+
| DOMException | ✓ | ✓ | ✓ | ✓ | ✓ | guarded | guarded |
|
|
3704
|
+
| Buffer | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
|
|
3705
|
+
| crypto.subtle | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
3706
|
+
| ReadableStream | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
3707
|
+
| URL pattern matching | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
3708
|
+
| Brotli decompression | ✓ | ✓ | ✗ passthrough | ✗ passthrough | ✗ passthrough | ✗ passthrough | ✗ passthrough |
|
|
3709
|
+
| Gzip/deflate decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
3710
|
+
|
|
3711
|
+
\* HTTP/2+ detection is best-effort and internal. kinetex's own `detectHTTPVersion()` helper (not exported) reports HTTP/2 only when the runtime exposes protocol evidence — a response `httpVersion`/`protocol` property, or an `Alt-Svc` header — and otherwise reports `HTTP/1.1`. The version on every `KinetexResponse` comes from that heuristic, so it is not a guarantee. This is accurate for any runtime whose `fetch` does not negotiate h2 by default; use `NodeHTTP2Transport` (Node 22+) when you need guaranteed HTTP/2.
|
|
3712
|
+
|
|
3713
|
+
¹ WSClient requires a native `WebSocket` constructor, which every supported Node.js version (22+) provides.
|
|
3590
3714
|
|
|
3591
3715
|
² Cloudflare Workers exposes a `WebSocket` constructor, but outbound client connections depend on runtime support.
|
|
3592
3716
|
|