kinetex 1.3.0 → 1.4.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 +246 -9
- package/dist/browser/kinetex.esm.js +38 -22
- package/dist/browser/kinetex.js +2545 -550
- package/dist/browser/kinetex.min.js +38 -22
- package/dist/cjs/aws-sigv4.js +133 -19
- package/dist/cjs/cache.js +49 -7
- package/dist/cjs/circuit-breaker.js +45 -3
- package/dist/cjs/client.js +387 -104
- package/dist/cjs/cookie-parser.js +103 -5
- package/dist/cjs/cookie-store.js +125 -28
- package/dist/cjs/core.js +465 -66
- package/dist/cjs/dedup.js +49 -11
- package/dist/cjs/digest.js +160 -24
- package/dist/cjs/graphql.js +164 -24
- package/dist/cjs/headers.js +303 -45
- package/dist/cjs/interceptors.js +221 -7
- package/dist/cjs/lifecycle.js +89 -40
- package/dist/cjs/logging.js +168 -15
- package/dist/cjs/mod.js +3 -2
- package/dist/cjs/pagination.js +247 -22
- package/dist/cjs/progress.js +177 -27
- package/dist/cjs/proxy.js +412 -0
- package/dist/cjs/response.js +316 -47
- package/dist/cjs/socks5.js +131 -15
- package/dist/cjs/sse.js +173 -43
- package/dist/cjs/url.js +191 -45
- package/dist/cjs/utils.js +222 -48
- package/dist/cjs/ws.js +19 -10
- package/dist/esm/aws-sigv4.js +133 -19
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +49 -7
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +45 -3
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +387 -104
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +103 -5
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/cookie-store.js +125 -28
- package/dist/esm/cookie-store.js.map +1 -1
- package/dist/esm/core.js +465 -66
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +49 -11
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +160 -24
- package/dist/esm/digest.js.map +1 -1
- package/dist/esm/graphql.js +164 -24
- package/dist/esm/graphql.js.map +1 -1
- package/dist/esm/headers.js +303 -45
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +221 -7
- package/dist/esm/interceptors.js.map +1 -1
- package/dist/esm/lifecycle.js +89 -40
- package/dist/esm/lifecycle.js.map +1 -1
- package/dist/esm/logging.js +168 -15
- package/dist/esm/logging.js.map +1 -1
- package/dist/esm/mod.js +3 -2
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/pagination.js +247 -22
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/progress.js +177 -27
- package/dist/esm/progress.js.map +1 -1
- package/dist/esm/proxy.js +413 -0
- package/dist/esm/proxy.js.map +1 -0
- package/dist/esm/response.js +316 -47
- package/dist/esm/response.js.map +1 -1
- package/dist/esm/socks5.js +131 -15
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/sse.js +173 -43
- package/dist/esm/sse.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/url.js +191 -45
- package/dist/esm/url.js.map +1 -1
- package/dist/esm/utils.js +222 -48
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/ws.js +19 -10
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts +19 -1
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/circuit-breaker.d.ts +14 -1
- package/dist/types/circuit-breaker.d.ts.map +1 -1
- package/dist/types/client.d.ts +69 -11
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/cookie-parser.d.ts +0 -17
- package/dist/types/cookie-parser.d.ts.map +1 -1
- package/dist/types/cookie-store.d.ts.map +1 -1
- package/dist/types/core.d.ts +103 -25
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts +17 -37
- package/dist/types/digest.d.ts.map +1 -1
- package/dist/types/graphql.d.ts.map +1 -1
- package/dist/types/headers.d.ts +45 -27
- package/dist/types/headers.d.ts.map +1 -1
- package/dist/types/interceptors.d.ts +102 -0
- package/dist/types/interceptors.d.ts.map +1 -1
- package/dist/types/lifecycle.d.ts +19 -2
- package/dist/types/lifecycle.d.ts.map +1 -1
- package/dist/types/logging.d.ts +22 -3
- package/dist/types/logging.d.ts.map +1 -1
- package/dist/types/mod.d.ts +5 -3
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/pagination.d.ts +0 -25
- package/dist/types/pagination.d.ts.map +1 -1
- package/dist/types/progress.d.ts +1 -1
- package/dist/types/progress.d.ts.map +1 -1
- package/dist/types/proxy.d.ts +50 -0
- package/dist/types/proxy.d.ts.map +1 -0
- package/dist/types/response.d.ts +7 -1
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/socks5.d.ts.map +1 -1
- package/dist/types/sse.d.ts.map +1 -1
- package/dist/types/types.d.ts +114 -3
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/url.d.ts +0 -14
- package/dist/types/url.d.ts.map +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/ws.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cjs/headers.js
CHANGED
|
@@ -95,6 +95,8 @@ export const HeaderName = {
|
|
|
95
95
|
TE: "te",
|
|
96
96
|
/** HTTP header name for Expect */
|
|
97
97
|
Expect: "expect",
|
|
98
|
+
/** HTTP header name for Idempotency-Key */
|
|
99
|
+
IdempotencyKey: "idempotency-key",
|
|
98
100
|
/** HTTP header name for Max-Forwards */
|
|
99
101
|
MaxForwards: "max-forwards",
|
|
100
102
|
// ── Negotiation ───────────────────────────────────────────────────────────
|
|
@@ -726,9 +728,77 @@ _a = HttpHeaders;
|
|
|
726
728
|
* // Map(2) { "charset" => "utf-8", "boundary" => "something" }
|
|
727
729
|
* ```
|
|
728
730
|
*/
|
|
731
|
+
/**
|
|
732
|
+
* Split a parameter list on `;`, ignoring separators inside a quoted-string
|
|
733
|
+
* and honouring `quoted-pair` (RFC 7230 §3.2.6).
|
|
734
|
+
*
|
|
735
|
+
* `String.prototype.split(";")` treated every semicolon as a separator, so a
|
|
736
|
+
* value containing one was shredded into extra parameters:
|
|
737
|
+
*
|
|
738
|
+
* `boundary="a;b"` → boundary = `"a`, then a stray `b"`
|
|
739
|
+
* `filename="report; final.pdf"` → filename = `"report`, then `final.pdf"`
|
|
740
|
+
*
|
|
741
|
+
* The second is a `Content-Disposition` with a perfectly ordinary filename —
|
|
742
|
+
* semicolons are legal in filenames, and the multipart spec's own example
|
|
743
|
+
* uses one. The first appears in every `multipart/form-data` boundary. Both
|
|
744
|
+
* were parsed by `parseParams`, which is also what `parseContentType`,
|
|
745
|
+
* `parseContentDisposition`, `parseCacheControl` and `parseServerTiming` read
|
|
746
|
+
* their parameters through.
|
|
747
|
+
*/
|
|
748
|
+
function splitParamList(paramStr) {
|
|
749
|
+
const out = [];
|
|
750
|
+
let cur = "";
|
|
751
|
+
let inQuotes = false;
|
|
752
|
+
for (let i = 0; i < paramStr.length; i++) {
|
|
753
|
+
const c = paramStr[i];
|
|
754
|
+
if (inQuotes) {
|
|
755
|
+
// A backslash escapes the next character, including a closing quote.
|
|
756
|
+
if (c === "\\" && i + 1 < paramStr.length) {
|
|
757
|
+
cur += c + paramStr[++i];
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (c === '"')
|
|
761
|
+
inQuotes = false;
|
|
762
|
+
cur += c;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (c === '"') {
|
|
766
|
+
inQuotes = true;
|
|
767
|
+
cur += c;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (c === ";") {
|
|
771
|
+
out.push(cur);
|
|
772
|
+
cur = "";
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
cur += c;
|
|
776
|
+
}
|
|
777
|
+
out.push(cur);
|
|
778
|
+
return out;
|
|
779
|
+
}
|
|
780
|
+
/** Remove surrounding quotes from a parameter value and decode quoted-pairs. */
|
|
781
|
+
function unquoteParam(v) {
|
|
782
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
|
|
783
|
+
return v.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
784
|
+
}
|
|
785
|
+
return v;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Wrap a parameter value in a `quoted-string`, escaping `\` and `"`.
|
|
789
|
+
*
|
|
790
|
+
* Every formatter in this module interpolated parameter values raw, so a
|
|
791
|
+
* value containing a double quote terminated its own string and the rest was
|
|
792
|
+
* read as further parameters: `formatLinkHeader` with `title: 'a "q" b'`
|
|
793
|
+
* emitted `title="a "q" b"`, and an unknown param `x` with value `v"w"`
|
|
794
|
+
* emitted `x="v"w""`. The same class of bug as an unescaped digest auth-param.
|
|
795
|
+
*/
|
|
796
|
+
function quoteParam(v) {
|
|
797
|
+
return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
798
|
+
}
|
|
729
799
|
export function parseParams(paramStr) {
|
|
730
800
|
const map = new Map();
|
|
731
|
-
for (const part of paramStr
|
|
801
|
+
for (const part of splitParamList(paramStr)) {
|
|
732
802
|
const t = part.trim();
|
|
733
803
|
if (!t)
|
|
734
804
|
continue;
|
|
@@ -738,11 +808,7 @@ export function parseParams(paramStr) {
|
|
|
738
808
|
}
|
|
739
809
|
else {
|
|
740
810
|
const k = t.slice(0, eq).trim().toLowerCase();
|
|
741
|
-
|
|
742
|
-
// Strip optional surrounding quotes
|
|
743
|
-
if (v.startsWith('"') && v.endsWith('"'))
|
|
744
|
-
v = v.slice(1, -1);
|
|
745
|
-
map.set(k, v);
|
|
811
|
+
map.set(k, unquoteParam(t.slice(eq + 1).trim()));
|
|
746
812
|
}
|
|
747
813
|
}
|
|
748
814
|
return map;
|
|
@@ -762,8 +828,17 @@ function parseQualityList(header) {
|
|
|
762
828
|
continue;
|
|
763
829
|
const k = seg.slice(0, eq).trim().toLowerCase();
|
|
764
830
|
const v = seg.slice(eq + 1).trim();
|
|
765
|
-
if (k === "q")
|
|
766
|
-
|
|
831
|
+
if (k === "q") {
|
|
832
|
+
// RFC 9110 §12.4.2 bounds a qvalue to 0..1 with at most 3 decimals.
|
|
833
|
+
// `parseFloat(v) || 0` made a *malformed* q read as 0 — "not
|
|
834
|
+
// acceptable" — so `Accept: text/html;q=abc` silently stopped
|
|
835
|
+
// matching text/html, and `q=1.5` or `q=-1` flowed out of here as
|
|
836
|
+
// out-of-range weights. A q that is not a number at all is treated
|
|
837
|
+
// as absent, which is what RFC 9110 §12.4.2 says it means.
|
|
838
|
+
const n = parseFloat(v);
|
|
839
|
+
if (Number.isFinite(n))
|
|
840
|
+
quality = Math.min(1, Math.max(0, n));
|
|
841
|
+
}
|
|
767
842
|
else
|
|
768
843
|
params.set(k, v);
|
|
769
844
|
}
|
|
@@ -880,9 +955,9 @@ export function parseContentDisposition(value) {
|
|
|
880
955
|
export function formatContentDisposition(cd) {
|
|
881
956
|
let out = cd.type;
|
|
882
957
|
if (cd.name)
|
|
883
|
-
out += `; name
|
|
958
|
+
out += `; name=${quoteParam(cd.name)}`;
|
|
884
959
|
if (cd.filename) {
|
|
885
|
-
out += `; filename
|
|
960
|
+
out += `; filename=${quoteParam(cd.filename)}`;
|
|
886
961
|
// Also emit RFC 5987 encoded form
|
|
887
962
|
const encoded = encodeURIComponent(cd.filename);
|
|
888
963
|
if (encoded !== cd.filename) {
|
|
@@ -1230,20 +1305,54 @@ export function parseContentLanguage(value) {
|
|
|
1230
1305
|
* @returns Best matching content type, or `null` if none match
|
|
1231
1306
|
*/
|
|
1232
1307
|
export function negotiateContentType(acceptHeader, available) {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1308
|
+
if (available.length === 0)
|
|
1309
|
+
return null;
|
|
1310
|
+
// RFC 9110 §12.5.1: "A request without any Accept header field implies that
|
|
1311
|
+
// the user agent will accept any media type in response." An absent or empty
|
|
1312
|
+
// Accept therefore means *everything*, not nothing — the old loop over an
|
|
1313
|
+
// empty list returned null, so a client that simply did not care got no
|
|
1314
|
+
// match at all.
|
|
1315
|
+
if (!acceptHeader.trim())
|
|
1316
|
+
return available[0];
|
|
1317
|
+
/** The `type/subtype` of an entry, lowercased, with parameters dropped. */
|
|
1318
|
+
const essence = (v) => v.split(";")[0].trim().toLowerCase();
|
|
1319
|
+
const ranges = parseAccept(acceptHeader)
|
|
1320
|
+
.map((qv) => ({ essence: essence(qv.value), quality: qv.quality }))
|
|
1321
|
+
.filter((r) => r.essence !== "");
|
|
1322
|
+
// 0 = no match, 1 = type/*, 2 = exact type/subtype. RFC 9110 §12.5.1 gives
|
|
1323
|
+
// precedence to the *most specific* matching range, so a type named with
|
|
1324
|
+
// `q=0` stays refused even when a wildcard would otherwise admit it.
|
|
1325
|
+
const specificity = (range, want) => {
|
|
1326
|
+
if (range === want)
|
|
1327
|
+
return 2;
|
|
1328
|
+
if (range.endsWith("/*") && want.startsWith(range.slice(0, range.length - 1)))
|
|
1329
|
+
return 1;
|
|
1330
|
+
if (range === "*/*")
|
|
1331
|
+
return 0;
|
|
1332
|
+
return -1;
|
|
1333
|
+
};
|
|
1334
|
+
let best = null;
|
|
1335
|
+
for (let i = 0; i < available.length; i++) {
|
|
1336
|
+
const want = essence(available[i]);
|
|
1337
|
+
let quality = null;
|
|
1338
|
+
let spec = -1;
|
|
1339
|
+
for (const range of ranges) {
|
|
1340
|
+
const m = specificity(range.essence, want);
|
|
1341
|
+
if (m < 0)
|
|
1342
|
+
continue;
|
|
1343
|
+
if (m > spec) {
|
|
1344
|
+
spec = m;
|
|
1345
|
+
quality = range.quality;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
// Unmatched, or matched at q=0 — the client will not take it.
|
|
1349
|
+
if (quality === null || quality <= 0)
|
|
1350
|
+
continue;
|
|
1351
|
+
if (best === null || quality > best.quality || (quality === best.quality && spec > best.spec)) {
|
|
1352
|
+
best = { index: i, quality, spec };
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return best === null ? null : available[best.index];
|
|
1247
1356
|
}
|
|
1248
1357
|
/**
|
|
1249
1358
|
* Parse a Range header value (RFC 7233 §3.1).
|
|
@@ -1264,14 +1373,33 @@ export function parseRange(value) {
|
|
|
1264
1373
|
const dash = t.indexOf("-");
|
|
1265
1374
|
if (dash === -1)
|
|
1266
1375
|
return null;
|
|
1267
|
-
const
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1376
|
+
const startStr = t.slice(0, dash).trim();
|
|
1377
|
+
const endStr = t.slice(dash + 1).trim();
|
|
1378
|
+
const start = startStr === "" ? null : parseInt(startStr, 10);
|
|
1379
|
+
const end = endStr === "" ? null : parseInt(endStr, 10);
|
|
1380
|
+
// `bytes=abc-def` used to yield `{ start: NaN, end: NaN }` — `parseInt`
|
|
1381
|
+
// returns NaN and nothing filtered it, so a caller arithmetic-ing on the
|
|
1382
|
+
// bounds got NaN rather than a rejection.
|
|
1383
|
+
if (start !== null && !Number.isFinite(start))
|
|
1384
|
+
return null;
|
|
1385
|
+
if (end !== null && !Number.isFinite(end))
|
|
1386
|
+
return null;
|
|
1387
|
+
// RFC 9110 §14.1.1: byte-range-spec is `first-pos "-" [last-pos]` or
|
|
1388
|
+
// `"-" suffix-length`, with first-pos <= last-pos and both non-negative.
|
|
1389
|
+
if (start === null && end === null)
|
|
1390
|
+
return null; // neither form given
|
|
1391
|
+
if (start !== null && start < 0)
|
|
1392
|
+
return null;
|
|
1393
|
+
if (end !== null && end < 0)
|
|
1394
|
+
return null;
|
|
1395
|
+
if (start !== null && end !== null && start > end)
|
|
1396
|
+
return null;
|
|
1397
|
+
return { start, end };
|
|
1273
1398
|
})
|
|
1274
1399
|
.filter((r) => r !== null);
|
|
1400
|
+
// `Range: bytes=` and a list of nothing but invalid specs are not ranges.
|
|
1401
|
+
if (ranges.length === 0)
|
|
1402
|
+
return null;
|
|
1275
1403
|
return { unit, ranges };
|
|
1276
1404
|
}
|
|
1277
1405
|
/**
|
|
@@ -1340,20 +1468,20 @@ export function formatLinkHeader(links) {
|
|
|
1340
1468
|
.map((l) => {
|
|
1341
1469
|
let s = `<${l.uri}>`;
|
|
1342
1470
|
if (l.rel)
|
|
1343
|
-
s += `; rel
|
|
1471
|
+
s += `; rel=${quoteParam(l.rel)}`;
|
|
1344
1472
|
if (l.type)
|
|
1345
|
-
s += `; type
|
|
1473
|
+
s += `; type=${quoteParam(l.type)}`;
|
|
1346
1474
|
if (l.hreflang)
|
|
1347
|
-
s += `; hreflang
|
|
1475
|
+
s += `; hreflang=${quoteParam(l.hreflang)}`;
|
|
1348
1476
|
if (l.title)
|
|
1349
|
-
s += `; title
|
|
1477
|
+
s += `; title=${quoteParam(l.title)}`;
|
|
1350
1478
|
if (l.media)
|
|
1351
|
-
s += `; media
|
|
1479
|
+
s += `; media=${quoteParam(l.media)}`;
|
|
1352
1480
|
const paramEntries = l.params instanceof Map ? [...l.params.entries()] : Object.entries(l.params ?? {});
|
|
1353
1481
|
for (const [k, v] of paramEntries) {
|
|
1354
1482
|
if (["rel", "type", "hreflang", "title", "media"].includes(k))
|
|
1355
1483
|
continue;
|
|
1356
|
-
s += `; ${k}
|
|
1484
|
+
s += `; ${k}=${quoteParam(String(v))}`;
|
|
1357
1485
|
}
|
|
1358
1486
|
return s;
|
|
1359
1487
|
})
|
|
@@ -1415,18 +1543,61 @@ export function normalizeForwardedHeaders(headers) {
|
|
|
1415
1543
|
* @param options - Trust configuration
|
|
1416
1544
|
* @returns The selected client IP, or `null` if none present
|
|
1417
1545
|
*/
|
|
1546
|
+
/** A dotted-quad IPv4 literal, or a syntactically plausible IPv6 literal. */
|
|
1547
|
+
function isIPLiteral(v) {
|
|
1548
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(v)) {
|
|
1549
|
+
return v.split(".").every((o) => Number(o) <= 255 && (o === "0" || !o.startsWith("0")));
|
|
1550
|
+
}
|
|
1551
|
+
if (!v.includes(":"))
|
|
1552
|
+
return false;
|
|
1553
|
+
// An IPv6 literal is hex groups, colons, and at most one `::`. At most one
|
|
1554
|
+
// `::` is what rejects prose like "not:an:ip:address:here" while accepting
|
|
1555
|
+
// the compressed and IPv4-mapped forms real proxies emit.
|
|
1556
|
+
if (!/^[0-9a-f:.]+$/i.test(v))
|
|
1557
|
+
return false;
|
|
1558
|
+
return (v.match(/::/g) ?? []).length <= 1;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* RFC 7239 permits an obfuscated identifier in place of an address, and
|
|
1562
|
+
* `unknown` is the de-facto placeholder that nginx, HAProxy and several CDNs
|
|
1563
|
+
* emit when they cannot determine the client. Neither is an IP address.
|
|
1564
|
+
*/
|
|
1565
|
+
function usableClientAddress(raw) {
|
|
1566
|
+
if (!raw)
|
|
1567
|
+
return null;
|
|
1568
|
+
const v = stripIPPortAndBrackets(raw);
|
|
1569
|
+
if (!v)
|
|
1570
|
+
return null;
|
|
1571
|
+
if (/^_/.test(v))
|
|
1572
|
+
return v; // RFC 7239 obfuscated identifier
|
|
1573
|
+
return isIPLiteral(v) ? v : null;
|
|
1574
|
+
}
|
|
1418
1575
|
export function getClientIP(headers, options = {}) {
|
|
1419
1576
|
const trustedHops = Math.max(0, Math.trunc(options.trustedHops ?? 0));
|
|
1420
1577
|
const fwd = normalizeForwardedHeaders(headers);
|
|
1421
1578
|
if (fwd.for.length > 0) {
|
|
1422
|
-
const list = fwd.for;
|
|
1423
1579
|
// Right-most entry is the closest hop. trustedHops = 1 → the entry the
|
|
1424
1580
|
// nearest trusted proxy appended; index = length - trustedHops.
|
|
1425
|
-
const index = trustedHops === 0 ? 0 :
|
|
1426
|
-
|
|
1427
|
-
|
|
1581
|
+
const index = trustedHops === 0 ? 0 : fwd.for.length - trustedHops;
|
|
1582
|
+
// Walk outwards from the chosen hop, skipping placeholders. Returning
|
|
1583
|
+
// `unknown` as a client address, or any non-address the header happened
|
|
1584
|
+
// to contain, meant a caller rate-limiting, logging or geo-locating on
|
|
1585
|
+
// the literal string "unknown" or "garbage".
|
|
1586
|
+
for (let i = index; i >= 0; i--) {
|
|
1587
|
+
const found = usableClientAddress(fwd.for[i]);
|
|
1588
|
+
if (found)
|
|
1589
|
+
return found;
|
|
1590
|
+
}
|
|
1591
|
+
for (const entry of fwd.for) {
|
|
1592
|
+
const found = usableClientAddress(entry);
|
|
1593
|
+
if (found)
|
|
1594
|
+
return found;
|
|
1595
|
+
}
|
|
1596
|
+
return null;
|
|
1428
1597
|
}
|
|
1429
|
-
|
|
1598
|
+
// The old fallback returned `stripIPPortAndBrackets("")` — the empty string —
|
|
1599
|
+
// for an empty `x-real-ip`, breaking the documented `string | null` contract.
|
|
1600
|
+
return usableClientAddress(headers.get(HeaderName.XRealIP));
|
|
1430
1601
|
}
|
|
1431
1602
|
/**
|
|
1432
1603
|
* Normalize a Forwarded/XFF entry to a bare host: strip `for="…"` quoting,
|
|
@@ -1447,6 +1618,30 @@ function stripIPPortAndBrackets(value) {
|
|
|
1447
1618
|
return m[1];
|
|
1448
1619
|
return v;
|
|
1449
1620
|
}
|
|
1621
|
+
/** RFC 7231 §7.1.1.1 IMF-fixdate, e.g. `Sun, 06 Nov 1994 08:49:37 GMT`. */
|
|
1622
|
+
const IMF_FIXDATE_RE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/;
|
|
1623
|
+
/** RFC 7231 §7.1.1.1 obsolete RFC 850 date, e.g. `Sunday, 06-Nov-94 08:49:37 GMT`. */
|
|
1624
|
+
const RFC850_DATE_RE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2,4} \d{2}:\d{2}:\d{2} GMT$/;
|
|
1625
|
+
/** RFC 7231 §7.1.1.1 obsolete asctime date, e.g. `Sun Nov 6 08:49:37 1994`. */
|
|
1626
|
+
const ASCTIME_DATE_RE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ \d]\d \d{2}:\d{2}:\d{2} \d{4}$/;
|
|
1627
|
+
/**
|
|
1628
|
+
* Parse a string that must be an HTTP-date in one of the three formats
|
|
1629
|
+
* RFC 7231 §7.1.1.1 allows a recipient to accept.
|
|
1630
|
+
*
|
|
1631
|
+
* Unlike `Date.parse`, this never invents a date out of a bare number, a
|
|
1632
|
+
* signed value or a floating-point one.
|
|
1633
|
+
*
|
|
1634
|
+
* @param value - Candidate HTTP-date (already trimmed)
|
|
1635
|
+
* @returns Epoch milliseconds, or `null` when the value is not an HTTP-date
|
|
1636
|
+
*/
|
|
1637
|
+
export function parseHTTPDate(value) {
|
|
1638
|
+
const t = value.trim();
|
|
1639
|
+
if (!IMF_FIXDATE_RE.test(t) && !RFC850_DATE_RE.test(t) && !ASCTIME_DATE_RE.test(t)) {
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
const ms = Date.parse(t);
|
|
1643
|
+
return isNaN(ms) ? null : ms;
|
|
1644
|
+
}
|
|
1450
1645
|
/**
|
|
1451
1646
|
* Parse a Retry-After header which may be either delta-seconds or an
|
|
1452
1647
|
* HTTP-date.
|
|
@@ -1459,9 +1654,12 @@ export function parseRetryAfter(value) {
|
|
|
1459
1654
|
// Delta-seconds: pure integer
|
|
1460
1655
|
if (/^\d+$/.test(t))
|
|
1461
1656
|
return { date: null, delay: parseInt(t, 10) };
|
|
1462
|
-
// HTTP-date
|
|
1463
|
-
|
|
1464
|
-
|
|
1657
|
+
// HTTP-date. Date.parse() is far more permissive than RFC 7231 §7.1.1.1 and
|
|
1658
|
+
// happily reads "-5" as 2001-05-01, "1.5" as 2001-01-05 and "+5" as
|
|
1659
|
+
// 2001-05-01, so a malformed header was silently turned into a *delay*.
|
|
1660
|
+
// Gate on the three legal HTTP-date shapes before parsing.
|
|
1661
|
+
const ms = parseHTTPDate(t);
|
|
1662
|
+
if (ms !== null)
|
|
1465
1663
|
return { date: new Date(ms), delay: null };
|
|
1466
1664
|
return { date: null, delay: null };
|
|
1467
1665
|
}
|
|
@@ -1627,7 +1825,7 @@ export function formatServerTiming(entries) {
|
|
|
1627
1825
|
if (e.duration !== null)
|
|
1628
1826
|
s += `;dur=${e.duration}`;
|
|
1629
1827
|
if (e.description !== null)
|
|
1630
|
-
s += `;desc
|
|
1828
|
+
s += `;desc=${quoteParam(e.description)}`;
|
|
1631
1829
|
return s;
|
|
1632
1830
|
})
|
|
1633
1831
|
.join(", ");
|
|
@@ -2300,6 +2498,66 @@ export function createHeaders(init) {
|
|
|
2300
2498
|
export function createRequestHeaders(init) {
|
|
2301
2499
|
return new RichHeaders(init, "request");
|
|
2302
2500
|
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Generate a random Idempotency-Key value.
|
|
2503
|
+
*
|
|
2504
|
+
* Uses `crypto.getRandomValues`, available in every runtime kinetex targets
|
|
2505
|
+
* (Node 18+, Deno, Bun, browsers, Workers) — no dependency, and not
|
|
2506
|
+
* `Math.random`.
|
|
2507
|
+
*
|
|
2508
|
+
* Format is a RFC 9562 v4 UUID: 122 random bits, hyphenated, with the
|
|
2509
|
+
* version and variant bits set. Servers that require an opaque string are
|
|
2510
|
+
* equally satisfied; UUID is the form Stripe and most others document.
|
|
2511
|
+
*
|
|
2512
|
+
* @returns A fresh key, e.g. `"3f2a9c1e-7b4d-4a8f-9c2e-1d5b6f7a8c90"`.
|
|
2513
|
+
* @throws {Error} If the runtime exposes no CSPRNG.
|
|
2514
|
+
*
|
|
2515
|
+
* @example
|
|
2516
|
+
* ```ts
|
|
2517
|
+
* client.post("/charges", body, { headers: { "idempotency-key": generateIdempotencyKey() } });
|
|
2518
|
+
* ```
|
|
2519
|
+
*/
|
|
2520
|
+
export function generateIdempotencyKey() {
|
|
2521
|
+
if (typeof crypto === "undefined" || typeof crypto.getRandomValues !== "function") {
|
|
2522
|
+
throw new Error("generateIdempotencyKey: no CSPRNG available in this runtime — crypto.getRandomValues is required");
|
|
2523
|
+
}
|
|
2524
|
+
const bytes = new Uint8Array(16);
|
|
2525
|
+
crypto.getRandomValues(bytes);
|
|
2526
|
+
// RFC 9562 §5.4: set the version (4) and variant (10xx) bits.
|
|
2527
|
+
// `noUncheckedIndexedAccess` makes these reads `number | undefined`; the
|
|
2528
|
+
// buffer is a fixed 16 bytes so they are always present.
|
|
2529
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
|
2530
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
2531
|
+
const hex = [];
|
|
2532
|
+
for (const b of bytes)
|
|
2533
|
+
hex.push(b.toString(16).padStart(2, "0"));
|
|
2534
|
+
return [
|
|
2535
|
+
hex.slice(0, 4).join(""),
|
|
2536
|
+
hex.slice(4, 6).join(""),
|
|
2537
|
+
hex.slice(6, 8).join(""),
|
|
2538
|
+
hex.slice(8, 10).join(""),
|
|
2539
|
+
hex.slice(10, 16).join(""),
|
|
2540
|
+
].join("-");
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Validate an Idempotency-Key value.
|
|
2544
|
+
*
|
|
2545
|
+
* The header is opaque to the client, but it travels in a request header, so
|
|
2546
|
+
* it must not be able to smuggle CR/LF into the request line or headers.
|
|
2547
|
+
* Servers commonly cap the length (Stripe: 255 chars).
|
|
2548
|
+
*
|
|
2549
|
+
* @param key - Candidate value.
|
|
2550
|
+
* @param maxLength - Maximum accepted length. Default: 255.
|
|
2551
|
+
* @returns True when the value is safe to send.
|
|
2552
|
+
*/
|
|
2553
|
+
export function isValidIdempotencyKey(key, maxLength = 255) {
|
|
2554
|
+
if (typeof key !== "string")
|
|
2555
|
+
return false;
|
|
2556
|
+
if (key.length === 0 || key.length > maxLength)
|
|
2557
|
+
return false;
|
|
2558
|
+
// Visible ASCII only: rejects CR, LF, NUL, and any non-ASCII byte.
|
|
2559
|
+
return /^[\x21-\x7e]+$/.test(key);
|
|
2560
|
+
}
|
|
2303
2561
|
/**
|
|
2304
2562
|
* Create a new RichHeaders instance with "response" guard.
|
|
2305
2563
|
* Response guard forbids Set-Cookie headers.
|