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/dist/cjs/graphql.js
CHANGED
|
@@ -30,6 +30,8 @@ import { ValidationError } from "./types.js";
|
|
|
30
30
|
// FIX (H6): untrusted GraphQL responses are parsed with raw JSON.parse below —
|
|
31
31
|
// sanitize prototype-pollution keys before they reach user code or merges.
|
|
32
32
|
import { sanitizeParsedJSON } from "./utils.js";
|
|
33
|
+
import { sleep } from "./utils.js";
|
|
34
|
+
import { sha256Hex } from "./utils.js";
|
|
33
35
|
// ============================================================================
|
|
34
36
|
// §2 ERRORS
|
|
35
37
|
// ============================================================================
|
|
@@ -82,9 +84,6 @@ function buildGraphQLError(graphQLErrors, req, res) {
|
|
|
82
84
|
return new GraphQLClientError(message, "EGRAPHQL", graphQLErrors, req, res);
|
|
83
85
|
}
|
|
84
86
|
// Utility function (B-10 fix - moved before usage)
|
|
85
|
-
function sleep(ms) {
|
|
86
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
-
}
|
|
88
87
|
function buildLinkChain(links, terminal) {
|
|
89
88
|
return links.reduceRight((next, link) => (op) => link(op, next), terminal);
|
|
90
89
|
}
|
|
@@ -121,14 +120,6 @@ export function extractOperationName(query) {
|
|
|
121
120
|
// ============================================================================
|
|
122
121
|
// §5 AUTOMATIC PERSISTED QUERIES (APQ)
|
|
123
122
|
// ============================================================================
|
|
124
|
-
async function sha256Hex(str) {
|
|
125
|
-
const encoded = new TextEncoder().encode(str);
|
|
126
|
-
const buf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
|
|
127
|
-
const hash = await crypto.subtle.digest("SHA-256", buf);
|
|
128
|
-
return Array.from(new Uint8Array(hash))
|
|
129
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
130
|
-
.join("");
|
|
131
|
-
}
|
|
132
123
|
/** Maximum APQ cache entries to prevent memory leaks */
|
|
133
124
|
const MAX_APQ_CACHE_ENTRIES = 1000;
|
|
134
125
|
/** APQ cache entry TTL in ms — entries older than this are evicted */
|
package/dist/cjs/headers.js
CHANGED
|
@@ -952,6 +952,16 @@ export function parseContentDisposition(value) {
|
|
|
952
952
|
* @param cd - Parsed disposition value
|
|
953
953
|
* @returns Formatted `Content-Disposition` header value
|
|
954
954
|
*/
|
|
955
|
+
/**
|
|
956
|
+
* Render a `Content-Disposition` header value.
|
|
957
|
+
*
|
|
958
|
+
* Takes the fields it actually reads rather than a whole
|
|
959
|
+
* `ContentDispositionValue`: `params` is never consulted, and `name` /
|
|
960
|
+
* `filename` are already optional in the sense that matters — a null or absent
|
|
961
|
+
* one is simply not emitted. Requiring callers to write `name: null,
|
|
962
|
+
* params: new Map()` to format `{ type: "inline" }` made the signature
|
|
963
|
+
* describe a parse result rather than what formatting needs.
|
|
964
|
+
*/
|
|
955
965
|
export function formatContentDisposition(cd) {
|
|
956
966
|
let out = cd.type;
|
|
957
967
|
if (cd.name)
|
|
@@ -1180,7 +1190,11 @@ export function parseAuthorization(value) {
|
|
|
1180
1190
|
continue;
|
|
1181
1191
|
const k = t.slice(0, eq).trim().toLowerCase();
|
|
1182
1192
|
let v = t.slice(eq + 1).trim();
|
|
1183
|
-
|
|
1193
|
+
// `length >= 2` so a lone `"` is left alone instead of becoming the
|
|
1194
|
+
// empty string: `'"'.slice(1, -1)` is `""`, so without the guard a
|
|
1195
|
+
// one-character value silently lost its only character. The identical
|
|
1196
|
+
// strip further down (stripIPPortAndBrackets) already had this guard.
|
|
1197
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
1184
1198
|
v = v.slice(1, -1);
|
|
1185
1199
|
params.set(k, v);
|
|
1186
1200
|
}
|
|
@@ -1463,6 +1477,15 @@ export function parseLinkHeader(value) {
|
|
|
1463
1477
|
* @param links - Parsed Link entries
|
|
1464
1478
|
* @returns Formatted `Link` header value
|
|
1465
1479
|
*/
|
|
1480
|
+
/**
|
|
1481
|
+
* Render an array of links as a `Link` header value.
|
|
1482
|
+
*
|
|
1483
|
+
* Only `uri` is required. `LinkValue` is the shape `parseLinkHeader` *returns*
|
|
1484
|
+
* — every parameter present, null when absent — and a caller building a header
|
|
1485
|
+
* by hand should not have to write `rel: null, type: null, hreflang: null,
|
|
1486
|
+
* title: null, media: null` to say `{ uri, rel }`. The body already guards
|
|
1487
|
+
* every one of them, so the parameter type now says what formatting needs.
|
|
1488
|
+
*/
|
|
1466
1489
|
export function formatLinkHeader(links) {
|
|
1467
1490
|
return links
|
|
1468
1491
|
.map((l) => {
|
package/dist/cjs/interceptors.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* - No dependencies, no runtime globals beyond Promise/Map/Set
|
|
29
29
|
*/
|
|
30
30
|
import { getAuthFingerprint } from "./cache.js";
|
|
31
|
+
import { perfNow, sleep, redactHeaders as redactHeaderValues } from "./utils.js";
|
|
31
32
|
import { KinetexError } from "./types.js";
|
|
32
33
|
import { parseHTTPDate } from "./headers.js";
|
|
33
34
|
// ============================================================================
|
|
@@ -607,13 +608,9 @@ let _reqIdSeq = 0;
|
|
|
607
608
|
export function createLoggingInterceptor(config = {}) {
|
|
608
609
|
const cfg = { ...LOG_DEFAULTS, ...config };
|
|
609
610
|
const redactSet = new Set(cfg.redactHeaders.map((h) => h.toLowerCase()));
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
out[k] = redactSet.has(k.toLowerCase()) ? "**REDACTED**" : v;
|
|
614
|
-
}
|
|
615
|
-
return out;
|
|
616
|
-
}
|
|
611
|
+
// The mask is "**REDACTED**" rather than the logger's "***" — that string is
|
|
612
|
+
// observable in this interceptor's own log output, so it is preserved here.
|
|
613
|
+
const redactHeaders = (headers) => redactHeaderValues(headers, redactSet, "**REDACTED**");
|
|
617
614
|
function makeEntry(type, ctx) {
|
|
618
615
|
const requestId = ctx.store.get(REQUEST_ID_KEY) ?? "unknown";
|
|
619
616
|
return {
|
|
@@ -1276,16 +1273,8 @@ export function createMetricsInterceptor() {
|
|
|
1276
1273
|
// ============================================================================
|
|
1277
1274
|
// §14 UTILITIES
|
|
1278
1275
|
// ============================================================================
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
// Fall back to Date.now() for edge runtimes that may not expose it
|
|
1282
|
-
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
1283
|
-
? performance.now()
|
|
1284
|
-
: Date.now();
|
|
1285
|
-
}
|
|
1286
|
-
function sleep(ms) {
|
|
1287
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1288
|
-
}
|
|
1276
|
+
/** High-resolution monotonic clock — the one `utils.ts` exports, under a local name. */
|
|
1277
|
+
const now = perfNow;
|
|
1289
1278
|
/** @internal Compute byte size of a request/response body. Exported for testing. */
|
|
1290
1279
|
export function computeBodySize(body) {
|
|
1291
1280
|
if (!body)
|
package/dist/cjs/lifecycle.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* - Hook timing + tracing
|
|
24
24
|
* - Typed event emitter
|
|
25
25
|
*/
|
|
26
|
+
import { perfNow, redactHeaders, abortError } from "./utils.js";
|
|
26
27
|
/**
|
|
27
28
|
* Lightweight typed event emitter for lifecycle events.
|
|
28
29
|
*
|
|
@@ -811,12 +812,7 @@ export function createLoggingHooks(options = {}) {
|
|
|
811
812
|
// cleartext by default, on a hook whose entire purpose is to log. The
|
|
812
813
|
// interceptors' own logging defaults already redact all four.
|
|
813
814
|
const redact = new Set((options.redactHeaders ?? ["authorization", "cookie", "set-cookie", "proxy-authorization"]).map((h) => h.toLowerCase()));
|
|
814
|
-
|
|
815
|
-
const out = {};
|
|
816
|
-
for (const [k, v] of Object.entries(h))
|
|
817
|
-
out[k] = redact.has(k.toLowerCase()) ? "***" : v;
|
|
818
|
-
return out;
|
|
819
|
-
}
|
|
815
|
+
const safeHeaders = (h) => redactHeaders(h, redact, "***");
|
|
820
816
|
return {
|
|
821
817
|
beforeRequest: (req) => {
|
|
822
818
|
log("→ request", {
|
|
@@ -893,7 +889,7 @@ export function createAbortHook() {
|
|
|
893
889
|
beforeRequest: (req, ctx) => {
|
|
894
890
|
if (req.signal?.aborted) {
|
|
895
891
|
ctx.meta.aborted = true;
|
|
896
|
-
throw
|
|
892
|
+
throw abortError("Request aborted before sending");
|
|
897
893
|
}
|
|
898
894
|
},
|
|
899
895
|
onCancel: (evt) => {
|
|
@@ -904,18 +900,6 @@ export function createAbortHook() {
|
|
|
904
900
|
// ============================================================================
|
|
905
901
|
// §9 UTILITIES
|
|
906
902
|
// ============================================================================
|
|
907
|
-
// Cross-runtime: safe AbortError creation (DOMException may not exist in all runtimes)
|
|
908
|
-
function createAbortError(message) {
|
|
909
|
-
if (typeof DOMException !== "undefined") {
|
|
910
|
-
return new DOMException(message, "AbortError");
|
|
911
|
-
}
|
|
912
|
-
const err = new Error(message);
|
|
913
|
-
err.name = "AbortError";
|
|
914
|
-
return err;
|
|
915
|
-
}
|
|
916
|
-
function perfNow() {
|
|
917
|
-
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
918
|
-
}
|
|
919
903
|
/**
|
|
920
904
|
* Create a new HookContext for the given request.
|
|
921
905
|
*
|
package/dist/cjs/logging.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
// FIX (H6): logged bodies are untrusted — parsed JSON is stripped of
|
|
29
29
|
// prototype-pollution keys before redaction writes (see sanitizeParsedJSON).
|
|
30
|
-
import { sanitizeParsedJSON } from "./utils.js";
|
|
30
|
+
import { sanitizeParsedJSON, perfNow, redactHeaders, safeStringify as stringifySafely, } from "./utils.js";
|
|
31
31
|
// Cross-runtime: safely detect Node.js process.env
|
|
32
32
|
const getNodeEnv = () => {
|
|
33
33
|
try {
|
|
@@ -462,11 +462,7 @@ export class Redactor {
|
|
|
462
462
|
}
|
|
463
463
|
/** Redact sensitive header values, replacing them with "***". */
|
|
464
464
|
redactHeaders(headers) {
|
|
465
|
-
|
|
466
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
467
|
-
out[k] = this.headerSet.has(k.toLowerCase()) ? "***" : v;
|
|
468
|
-
}
|
|
469
|
-
return out;
|
|
465
|
+
return redactHeaders(headers, this.headerSet, "***");
|
|
470
466
|
}
|
|
471
467
|
/**
|
|
472
468
|
* Redact sensitive query parameters from a URL.
|
|
@@ -516,9 +512,11 @@ export class Redactor {
|
|
|
516
512
|
}
|
|
517
513
|
if (!this.paramSet.has(decoded.toLowerCase()))
|
|
518
514
|
return pair;
|
|
519
|
-
// A valueless parameter (`?token`) is redacted to `token=***`
|
|
520
|
-
//
|
|
521
|
-
|
|
515
|
+
// A valueless parameter (`?token`) is redacted to `token=***` rather than
|
|
516
|
+
// left as the bare name, so the redaction stays visible. The value, when
|
|
517
|
+
// there is one, is discarded either way — which is why this used to be
|
|
518
|
+
// written as a ternary whose two arms were the same string.
|
|
519
|
+
return `${name}=***`;
|
|
522
520
|
});
|
|
523
521
|
return `${base}?${params.join("&")}${fragment}`;
|
|
524
522
|
}
|
|
@@ -957,22 +955,11 @@ function serializeError(err) {
|
|
|
957
955
|
return { name: "Error", message: String(err) };
|
|
958
956
|
}
|
|
959
957
|
/** `JSON.stringify` that never throws — falls back for circular values. */
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (seen.has(v))
|
|
966
|
-
return "[circular]";
|
|
967
|
-
seen.add(v);
|
|
968
|
-
}
|
|
969
|
-
return v;
|
|
970
|
-
}) ?? String(value));
|
|
971
|
-
}
|
|
972
|
-
catch {
|
|
973
|
-
return String(value);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
958
|
+
// The marker is lowercase here and capitalised in the shared helper. Both are
|
|
959
|
+
// asserted by tests and both appear in log output, so this converges on one
|
|
960
|
+
// implementation without converging on one string.
|
|
961
|
+
const CIRCULAR_MARKER = "[circular]";
|
|
962
|
+
const safeStringify = (value) => stringifySafely(value, CIRCULAR_MARKER);
|
|
976
963
|
/**
|
|
977
964
|
* Truncate a string to at most `maxBytes` UTF-8 bytes, never splitting a
|
|
978
965
|
* character, and report how many bytes were actually removed.
|
|
@@ -993,10 +980,6 @@ function truncateToBytes(str, maxBytes) {
|
|
|
993
980
|
}
|
|
994
981
|
return { text: str.slice(0, end), removed: total - used };
|
|
995
982
|
}
|
|
996
|
-
/** Cross-runtime high-resolution timer. Falls back to Date.now() when performance.now() is unavailable. */
|
|
997
|
-
function perfNow() {
|
|
998
|
-
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
999
|
-
}
|
|
1000
983
|
// ============================================================================
|
|
1001
984
|
// §10 FACTORY HELPERS
|
|
1002
985
|
// ============================================================================
|
package/dist/cjs/progress.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* - Async iterator interface
|
|
21
21
|
* - Readable progress formatting (human-readable bytes, speed)
|
|
22
22
|
*/
|
|
23
|
+
import { perfNow } from "./utils.js";
|
|
23
24
|
// ============================================================================
|
|
24
25
|
// §2 PROGRESS TRACKER
|
|
25
26
|
// ============================================================================
|
|
@@ -926,6 +927,3 @@ export function throttleProgress(fn, hz = 10) {
|
|
|
926
927
|
// ============================================================================
|
|
927
928
|
// §10 UTILITIES
|
|
928
929
|
// ============================================================================
|
|
929
|
-
function perfNow() {
|
|
930
|
-
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
931
|
-
}
|
package/dist/cjs/proxy.js
CHANGED
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
* @module
|
|
19
19
|
*/
|
|
20
20
|
import { KinetexError } from "./types.js";
|
|
21
|
+
/**
|
|
22
|
+
* True when `host` is an IPv4/IPv6 literal rather than a DNS name.
|
|
23
|
+
*
|
|
24
|
+
* A textual shape check rather than `node:net.isIP`, because this is a
|
|
25
|
+
* synchronous guard and the module only reaches for `node:net` through a
|
|
26
|
+
* dynamic import. Being imprecise here is harmless: the worst case is that a
|
|
27
|
+
* name the resolver dislikes is sent as SNI, and the TLS stack rejects it.
|
|
28
|
+
*/
|
|
29
|
+
function isIpLiteral(host) {
|
|
30
|
+
const h = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
31
|
+
return h.includes(":") || /^\d+(\.\d+)*$/.test(h);
|
|
32
|
+
}
|
|
21
33
|
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
|
|
22
34
|
/** Maximum CONNECT response head we will buffer before giving up. */
|
|
23
35
|
const MAX_HEAD_BYTES = 64 * 1024;
|
|
@@ -305,9 +317,14 @@ export async function connectThroughProxy(proxy, target, options = {}) {
|
|
|
305
317
|
clearTimeout(tunnelTimer);
|
|
306
318
|
options.signal?.removeEventListener("abort", onTunnelAbort);
|
|
307
319
|
}
|
|
320
|
+
// RFC 6066 §3 forbids IP literals in SNI, and Node 26 rejects the
|
|
321
|
+
// handshake outright when one is sent; Node 22 and 24 ignored it. Omit
|
|
322
|
+
// `servername` for IP targets so certificate verification falls back to
|
|
323
|
+
// the `host`/`authorized` check, which still covers the certificate.
|
|
324
|
+
const sni = options.servername ?? target.hostname;
|
|
308
325
|
const secure = tls.connect({
|
|
309
326
|
socket: baseSocket,
|
|
310
|
-
servername:
|
|
327
|
+
...(sni !== undefined && !isIpLiteral(sni) ? { servername: sni } : {}),
|
|
311
328
|
...(options.ca !== undefined ? { ca: options.ca } : {}),
|
|
312
329
|
},
|
|
313
330
|
// `rejectUnauthorized` is left at its default (true), so a peer that
|
|
@@ -372,7 +389,7 @@ async function openTlsSocket(tls, opts, timeoutMs, signal, errMeta, what) {
|
|
|
372
389
|
const socket = tls.connect({
|
|
373
390
|
host: opts.host,
|
|
374
391
|
port: opts.port,
|
|
375
|
-
servername: opts.servername,
|
|
392
|
+
...(isIpLiteral(opts.servername) ? {} : { servername: opts.servername }),
|
|
376
393
|
...(opts.ca !== undefined ? { ca: opts.ca } : {}),
|
|
377
394
|
}, () => {
|
|
378
395
|
cleanup();
|