kinetex 1.2.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.
Files changed (126) hide show
  1. package/README.md +1164 -453
  2. package/dist/browser/kinetex.esm.js +38 -22
  3. package/dist/browser/kinetex.js +3127 -715
  4. package/dist/browser/kinetex.min.js +38 -22
  5. package/dist/cjs/aws-sigv4.js +137 -20
  6. package/dist/cjs/cache.js +101 -21
  7. package/dist/cjs/circuit-breaker.js +69 -7
  8. package/dist/cjs/client.js +838 -191
  9. package/dist/cjs/cookie-parser.js +110 -9
  10. package/dist/cjs/cookie-store.js +141 -36
  11. package/dist/cjs/core.js +501 -63
  12. package/dist/cjs/dedup.js +58 -18
  13. package/dist/cjs/digest.js +185 -23
  14. package/dist/cjs/graphql.js +164 -24
  15. package/dist/cjs/headers.js +362 -48
  16. package/dist/cjs/interceptors.js +285 -29
  17. package/dist/cjs/lifecycle.js +89 -40
  18. package/dist/cjs/logging.js +169 -16
  19. package/dist/cjs/mod.js +3 -2
  20. package/dist/cjs/pagination.js +261 -28
  21. package/dist/cjs/progress.js +282 -52
  22. package/dist/cjs/proxy.js +412 -0
  23. package/dist/cjs/response.js +316 -47
  24. package/dist/cjs/socks5.js +167 -36
  25. package/dist/cjs/sse.js +201 -34
  26. package/dist/cjs/url.js +191 -45
  27. package/dist/cjs/utils.js +222 -48
  28. package/dist/cjs/worker.js +6 -6
  29. package/dist/cjs/ws.js +32 -16
  30. package/dist/esm/aws-sigv4.js +137 -20
  31. package/dist/esm/aws-sigv4.js.map +1 -1
  32. package/dist/esm/cache.js +101 -21
  33. package/dist/esm/cache.js.map +1 -1
  34. package/dist/esm/circuit-breaker.js +69 -7
  35. package/dist/esm/circuit-breaker.js.map +1 -1
  36. package/dist/esm/client.js +838 -191
  37. package/dist/esm/client.js.map +1 -1
  38. package/dist/esm/cookie-parser.js +110 -9
  39. package/dist/esm/cookie-parser.js.map +1 -1
  40. package/dist/esm/cookie-store.js +141 -36
  41. package/dist/esm/cookie-store.js.map +1 -1
  42. package/dist/esm/core.js +501 -63
  43. package/dist/esm/core.js.map +1 -1
  44. package/dist/esm/dedup.js +58 -18
  45. package/dist/esm/dedup.js.map +1 -1
  46. package/dist/esm/digest.js +185 -23
  47. package/dist/esm/digest.js.map +1 -1
  48. package/dist/esm/graphql.js +164 -24
  49. package/dist/esm/graphql.js.map +1 -1
  50. package/dist/esm/headers.js +362 -48
  51. package/dist/esm/headers.js.map +1 -1
  52. package/dist/esm/interceptors.js +285 -29
  53. package/dist/esm/interceptors.js.map +1 -1
  54. package/dist/esm/lifecycle.js +89 -40
  55. package/dist/esm/lifecycle.js.map +1 -1
  56. package/dist/esm/logging.js +169 -16
  57. package/dist/esm/logging.js.map +1 -1
  58. package/dist/esm/mod.js +3 -2
  59. package/dist/esm/mod.js.map +1 -1
  60. package/dist/esm/pagination.js +261 -28
  61. package/dist/esm/pagination.js.map +1 -1
  62. package/dist/esm/progress.js +282 -52
  63. package/dist/esm/progress.js.map +1 -1
  64. package/dist/esm/proxy.js +413 -0
  65. package/dist/esm/proxy.js.map +1 -0
  66. package/dist/esm/response.js +316 -47
  67. package/dist/esm/response.js.map +1 -1
  68. package/dist/esm/socks5.js +167 -36
  69. package/dist/esm/socks5.js.map +1 -1
  70. package/dist/esm/sse.js +201 -34
  71. package/dist/esm/sse.js.map +1 -1
  72. package/dist/esm/types.js.map +1 -1
  73. package/dist/esm/url.js +191 -45
  74. package/dist/esm/url.js.map +1 -1
  75. package/dist/esm/utils.js +222 -48
  76. package/dist/esm/utils.js.map +1 -1
  77. package/dist/esm/worker.js +6 -6
  78. package/dist/esm/worker.js.map +1 -1
  79. package/dist/esm/ws.js +32 -16
  80. package/dist/esm/ws.js.map +1 -1
  81. package/dist/types/aws-sigv4.d.ts.map +1 -1
  82. package/dist/types/cache.d.ts +27 -2
  83. package/dist/types/cache.d.ts.map +1 -1
  84. package/dist/types/circuit-breaker.d.ts +14 -1
  85. package/dist/types/circuit-breaker.d.ts.map +1 -1
  86. package/dist/types/client.d.ts +98 -23
  87. package/dist/types/client.d.ts.map +1 -1
  88. package/dist/types/cookie-parser.d.ts +0 -17
  89. package/dist/types/cookie-parser.d.ts.map +1 -1
  90. package/dist/types/cookie-store.d.ts.map +1 -1
  91. package/dist/types/core.d.ts +109 -25
  92. package/dist/types/core.d.ts.map +1 -1
  93. package/dist/types/dedup.d.ts +0 -7
  94. package/dist/types/dedup.d.ts.map +1 -1
  95. package/dist/types/digest.d.ts +31 -37
  96. package/dist/types/digest.d.ts.map +1 -1
  97. package/dist/types/graphql.d.ts.map +1 -1
  98. package/dist/types/headers.d.ts +62 -29
  99. package/dist/types/headers.d.ts.map +1 -1
  100. package/dist/types/interceptors.d.ts +102 -0
  101. package/dist/types/interceptors.d.ts.map +1 -1
  102. package/dist/types/lifecycle.d.ts +19 -2
  103. package/dist/types/lifecycle.d.ts.map +1 -1
  104. package/dist/types/logging.d.ts +23 -4
  105. package/dist/types/logging.d.ts.map +1 -1
  106. package/dist/types/mod.d.ts +5 -3
  107. package/dist/types/mod.d.ts.map +1 -1
  108. package/dist/types/pagination.d.ts +0 -25
  109. package/dist/types/pagination.d.ts.map +1 -1
  110. package/dist/types/progress.d.ts +1 -1
  111. package/dist/types/progress.d.ts.map +1 -1
  112. package/dist/types/proxy.d.ts +50 -0
  113. package/dist/types/proxy.d.ts.map +1 -0
  114. package/dist/types/response.d.ts +7 -1
  115. package/dist/types/response.d.ts.map +1 -1
  116. package/dist/types/socks5.d.ts.map +1 -1
  117. package/dist/types/sse.d.ts.map +1 -1
  118. package/dist/types/types.d.ts +139 -5
  119. package/dist/types/types.d.ts.map +1 -1
  120. package/dist/types/url.d.ts +0 -14
  121. package/dist/types/url.d.ts.map +1 -1
  122. package/dist/types/utils.d.ts.map +1 -1
  123. package/dist/types/worker.d.ts +6 -6
  124. package/dist/types/worker.d.ts.map +1 -1
  125. package/dist/types/ws.d.ts.map +1 -1
  126. package/package.json +2 -2
package/dist/cjs/url.js CHANGED
@@ -21,6 +21,9 @@
21
21
  * - Blob URL detection
22
22
  * - URL diff
23
23
  */
24
+ // The only import in a module that otherwise has none: base64-encoding a
25
+ // UTF-8 payload is not something to reimplement per file.
26
+ import { uint8ArrayToBase64 } from "./utils.js";
24
27
  // ============================================================================
25
28
  // §1 TYPES
26
29
  // ============================================================================
@@ -190,21 +193,44 @@ export function parseQuery(qs) {
190
193
  const result = {};
191
194
  if (!str)
192
195
  return result;
196
+ // `result` is a plain object, so `result["__proto__"]` reads
197
+ // `Object.prototype` rather than "nothing here yet". A query string of
198
+ // `__proto__=x&__proto__=y` therefore took the "already have a value"
199
+ // branch and assigned an *array* through the inherited `__proto__` setter,
200
+ // which re-prototypes the object being returned to the caller. The read is
201
+ // now an own-property check, and the write for that one key is a define,
202
+ // so it lands as data instead of going through a setter.
203
+ const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
204
+ const put = (k, v) => {
205
+ if (k === "__proto__") {
206
+ Object.defineProperty(result, k, {
207
+ value: v,
208
+ writable: true,
209
+ enumerable: true,
210
+ configurable: true,
211
+ });
212
+ }
213
+ else {
214
+ result[k] = v;
215
+ }
216
+ };
193
217
  for (const pair of str.split("&")) {
194
218
  if (!pair)
195
219
  continue;
196
220
  const eq = pair.indexOf("=");
197
221
  const key = percentDecode(eq === -1 ? pair : pair.slice(0, eq));
198
222
  const val = eq === -1 ? "" : percentDecode(pair.slice(eq + 1));
199
- const existing = result[key];
200
- if (existing === undefined) {
201
- result[key] = val;
202
- }
203
- else if (Array.isArray(existing)) {
204
- existing.push(val);
223
+ if (!own(result, key)) {
224
+ put(key, val);
205
225
  }
206
226
  else {
207
- result[key] = [existing, val];
227
+ const existing = result[key];
228
+ if (Array.isArray(existing)) {
229
+ existing.push(val);
230
+ }
231
+ else {
232
+ put(key, [existing, val]);
233
+ }
208
234
  }
209
235
  }
210
236
  return result;
@@ -800,25 +826,38 @@ function getIfEmpty(op) {
800
826
  * "/files/**" → matches "/files/a/b/c", wildcards: ["a","b","c"]
801
827
  * "/items/:id(\\d+)" → matches "/items/99" only if id is numeric
802
828
  */
829
+ function escapeRegexLiteral(text) {
830
+ return text.replace(/[.+^${}()|[\]\\]/g, "\\$&");
831
+ }
803
832
  export function compilePattern(pattern) {
804
- const paramNames = [];
805
- const wildcardCount = { n: 0 };
806
- const regexStr = pattern
807
- .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex chars (except /)
808
- .replace(/\\\*/g, "*") // restore our * wildcards
809
- .replace(/\*\*/g, () => {
810
- // wildcard: matches any path segments (non-greedy due to anchors)
811
- return "(.+?)";
812
- })
813
- .replace(/\*(?!\*)/g, () => {
814
- // single wildcard
815
- wildcardCount.n++;
816
- return "([^/]+)";
817
- })
818
- .replace(/:([A-Za-z_][A-Za-z0-9_]*)(?:\(([^)]+)\))?/g, (_, name, constraint) => {
819
- paramNames.push(name);
820
- return `(${constraint ?? "[^/]+"})`;
821
- });
833
+ const captures = [];
834
+ // One tokenising pass, so that a constraint is recognised as a constraint.
835
+ // The old order escaped the whole pattern first, which turned the `(` of
836
+ // `:id(\d+)` into `\(`, after which the parameter form could never match
837
+ // and the documented constrained syntax was dead: `/items/:id(\d+)` matched
838
+ // `/items/anything`.
839
+ const TOKEN = /:([A-Za-z_][A-Za-z0-9_]*)(?:\(([^)]+)\))?|\*\*|\*/g;
840
+ let regexStr = "";
841
+ let last = 0;
842
+ for (const m of pattern.matchAll(TOKEN)) {
843
+ regexStr += escapeRegexLiteral(pattern.slice(last, m.index));
844
+ last = m.index + m[0].length;
845
+ if (m[1] !== undefined) {
846
+ // The constraint is a regex and is deliberately NOT escaped; the
847
+ // literal text around it is.
848
+ captures.push({ kind: "param", name: m[1] });
849
+ regexStr += `(${m[2] ?? "[^/]+"})`;
850
+ }
851
+ else if (m[0] === "**") {
852
+ captures.push({ kind: "wildcard", greedy: true });
853
+ regexStr += "(.+?)";
854
+ }
855
+ else {
856
+ captures.push({ kind: "wildcard", greedy: false });
857
+ regexStr += "([^/]+)";
858
+ }
859
+ }
860
+ regexStr += escapeRegexLiteral(pattern.slice(last));
822
861
  const regex = new RegExp(`^${regexStr}\\/?$`);
823
862
  return {
824
863
  match(url) {
@@ -829,13 +868,28 @@ export function compilePattern(pattern) {
829
868
  const params = {};
830
869
  const wildcards = [];
831
870
  const groups = {};
832
- let captureIdx = 1;
833
- for (const name of paramNames) {
834
- params[name] = percentDecode(m[captureIdx++] ?? "");
835
- }
836
- while (captureIdx <= m.length - 1) {
837
- const val = m[captureIdx++] ?? "";
838
- wildcards.push(...val.split("/").filter(Boolean).map(percentDecode));
871
+ for (let i = 0; i < captures.length; i++) {
872
+ const capture = captures[i];
873
+ const raw = m[i + 1] ?? "";
874
+ if (capture.kind === "param") {
875
+ const value = percentDecode(raw);
876
+ params[capture.name] = value;
877
+ groups[capture.name] = value;
878
+ continue;
879
+ }
880
+ // A greedy `**` spans segments, so it yields one entry per segment;
881
+ // a single `*` is one segment by definition.
882
+ if (capture.greedy) {
883
+ for (const part of raw.split("/").filter(Boolean)) {
884
+ wildcards.push(percentDecode(part));
885
+ }
886
+ }
887
+ else {
888
+ wildcards.push(percentDecode(raw));
889
+ }
890
+ // Unnamed captures are keyed by their 1-based index, so `groups` is
891
+ // a complete record of the match.
892
+ groups[String(i + 1)] = percentDecode(raw);
839
893
  }
840
894
  return { params, wildcards, groups };
841
895
  },
@@ -978,6 +1032,16 @@ export function relativeURL(url, base) {
978
1032
  return null;
979
1033
  if (!u.pathname.startsWith(b.pathname))
980
1034
  return null;
1035
+ // A string prefix is not containment. With the base `/posts`, the path
1036
+ // `/posts-admin/secret` starts with it, so the function reported the
1037
+ // relative URL `-admin/secret` — a value the caller then resolves against
1038
+ // `/posts` to reach a sibling resource it was never scoped to. The base
1039
+ // has to end on a segment boundary, unless it already ends in a slash.
1040
+ if (u.pathname.length > b.pathname.length &&
1041
+ !b.pathname.endsWith("/") &&
1042
+ u.pathname[b.pathname.length] !== "/") {
1043
+ return null;
1044
+ }
981
1045
  return u.pathname.slice(b.pathname.length) + u.search + u.hash;
982
1046
  }
983
1047
  catch {
@@ -1084,13 +1148,37 @@ export function isLocalhost(url) {
1084
1148
  * @returns Parsed DataURLParts, or null if not a valid data URL.
1085
1149
  */
1086
1150
  export function parseDataURL(url) {
1087
- const m = url.match(/^data:([^;,]+)?(;base64)?,(.*)$/s);
1088
- if (!m)
1151
+ // RFC 2397: `data:[<mediatype>][;base64],<data>`, where `<mediatype>` is a
1152
+ // type/subtype followed by any number of `;parameter` pairs. The old regex
1153
+ // stopped the media type at the first `;` and then demanded a comma, so
1154
+ // `data:text/plain;charset=utf-8,hi` — an entirely ordinary data URL — did
1155
+ // not parse at all.
1156
+ if (!url.startsWith("data:"))
1157
+ return null;
1158
+ const comma = url.indexOf(",");
1159
+ if (comma === -1)
1089
1160
  return null;
1161
+ let head = url.slice(5, comma);
1162
+ const data = url.slice(comma + 1);
1163
+ // `;base64` is only an encoding marker when it is the LAST parameter, and
1164
+ // it is the only parameter that is not part of the media type.
1165
+ const semi = head.lastIndexOf(";");
1166
+ let isBase64 = false;
1167
+ if (semi !== -1 && head.slice(semi + 1).toLowerCase() === "base64") {
1168
+ isBase64 = true;
1169
+ head = head.slice(0, semi);
1170
+ }
1171
+ // A URL that carries parameters but no type (`data:;charset=utf-8,hi`) has
1172
+ // an omitted media type, which RFC 2397 defaults to text/plain. A type is
1173
+ // `type/subtype`, so a head with no slash in it is parameters only --
1174
+ // keeping it as the media type would report the string "charset=utf-8" as
1175
+ // a MIME type. The parameters are kept either way, so nothing is lost.
1176
+ head = head.replace(/^;+/, "");
1177
+ const mediaType = head.includes("/") ? head : head === "" ? "text/plain" : `text/plain;${head}`;
1090
1178
  return {
1091
- mediaType: m[1] ?? "text/plain",
1092
- isBase64: !!m[2],
1093
- data: m[3] ?? "",
1179
+ mediaType,
1180
+ isBase64,
1181
+ data,
1094
1182
  };
1095
1183
  }
1096
1184
  /**
@@ -1102,16 +1190,25 @@ export function parseDataURL(url) {
1102
1190
  * @returns A data: URL string.
1103
1191
  */
1104
1192
  export function buildDataURL(data, mediaType, base64 = true) {
1193
+ // `,` and `;` are the data URL's own delimiters. A media type carrying one
1194
+ // is not a media type -- `text/plain;base64` written here produces
1195
+ // `data:text/plain;base64,x`, which every parser then reads as a BASE64
1196
+ // payload whether or not the caller asked for that.
1197
+ if (/[,;]/.test(mediaType)) {
1198
+ throw new URLValidationError(`Invalid media type for a data URL: ${JSON.stringify(mediaType)} (it must not contain "," or ";")`);
1199
+ }
1105
1200
  if (typeof data === "string") {
1106
1201
  if (base64) {
1107
- return `data:${mediaType};base64,${btoa(data)}`;
1202
+ // `btoa` is Latin-1 only and throws InvalidCharacterError above U+00FF,
1203
+ // so "café" -- exactly the payload base64 exists for -- could not be
1204
+ // encoded at all. Encode the UTF-8 bytes instead, the same way the
1205
+ // Uint8Array path already did.
1206
+ return `data:${mediaType};base64,${uint8ArrayToBase64(new TextEncoder().encode(data))}`;
1108
1207
  }
1109
1208
  return `data:${mediaType},${encodeURIComponent(data)}`;
1110
1209
  }
1111
1210
  // Uint8Array → base64 (chunked to avoid memory issues with large data)
1112
- // Use TextDecoder for efficient Uint8Array to string conversion
1113
- const binary = new TextDecoder("iso-8859-1").decode(data);
1114
- return `data:${mediaType};base64,${btoa(binary)}`;
1211
+ return `data:${mediaType};base64,${uint8ArrayToBase64(data)}`;
1115
1212
  }
1116
1213
  /**
1117
1214
  * Diff two URLs — returns changed components and query param differences.
@@ -1204,7 +1301,16 @@ export function withoutTrailingSlash(url) {
1204
1301
  * @returns URL without hash fragment.
1205
1302
  */
1206
1303
  export function stripHash(url) {
1207
- return URLBuilder.from(url).removeHash().toString();
1304
+ try {
1305
+ return URLBuilder.from(url).removeHash().toString();
1306
+ }
1307
+ catch {
1308
+ // Same contract as `stripQuery` below: a URL we cannot parse is returned
1309
+ // unchanged rather than turned into an exception. The two are used
1310
+ // together on the same input, and only one of them being total meant a
1311
+ // malformed URL was safe to strip a query from and fatal to strip a hash.
1312
+ return url;
1313
+ }
1208
1314
  }
1209
1315
  /**
1210
1316
  * Strip all query params from a URL.
@@ -1263,7 +1369,47 @@ export function urlFilename(url) {
1263
1369
  * @returns URL with sensitive param values replaced with "REDACTED".
1264
1370
  */
1265
1371
  export function redactURL(url, ...sensitiveParams) {
1266
- return URLBuilder.from(url)
1267
- .redactParams(...sensitiveParams)
1268
- .toString();
1372
+ try {
1373
+ return URLBuilder.from(url)
1374
+ .redactParams(...sensitiveParams)
1375
+ .toString();
1376
+ }
1377
+ catch {
1378
+ // This function exists to be called from a logger. `URLBuilder.from`
1379
+ // throws on anything `new URL()` rejects — a relative path, a truncated
1380
+ // string from a bad config — so the one helper that must never take a
1381
+ // process down on malformed input was the one that threw. The parameters
1382
+ // are masked on the raw string instead, and only if there is a query to
1383
+ // mask them in.
1384
+ return redactUnparsedURL(url, sensitiveParams);
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Redact sensitive parameters in a string that is not a parseable URL.
1389
+ *
1390
+ * Splits on the delimiters rather than pattern-matching the key, so a key
1391
+ * containing regex metacharacters is matched literally.
1392
+ */
1393
+ function redactUnparsedURL(url, sensitiveParams) {
1394
+ if (sensitiveParams.length === 0)
1395
+ return url;
1396
+ const hashAt = url.indexOf("#");
1397
+ const beforeHash = hashAt === -1 ? url : url.slice(0, hashAt);
1398
+ const hash = hashAt === -1 ? "" : url.slice(hashAt);
1399
+ const q = beforeHash.indexOf("?");
1400
+ if (q === -1)
1401
+ return url;
1402
+ const path = beforeHash.slice(0, q);
1403
+ const pairs = beforeHash
1404
+ .slice(q + 1)
1405
+ .split("&")
1406
+ .map((pair) => {
1407
+ const eq = pair.indexOf("=");
1408
+ const key = eq === -1 ? pair : pair.slice(0, eq);
1409
+ if (sensitiveParams.includes(key) || sensitiveParams.includes(percentDecode(key))) {
1410
+ return `${key}=REDACTED`;
1411
+ }
1412
+ return pair;
1413
+ });
1414
+ return `${path}?${pairs.join("&")}${hash}`;
1269
1415
  }