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.
Files changed (120) hide show
  1. package/README.md +246 -9
  2. package/dist/browser/kinetex.esm.js +38 -22
  3. package/dist/browser/kinetex.js +2545 -550
  4. package/dist/browser/kinetex.min.js +38 -22
  5. package/dist/cjs/aws-sigv4.js +133 -19
  6. package/dist/cjs/cache.js +49 -7
  7. package/dist/cjs/circuit-breaker.js +45 -3
  8. package/dist/cjs/client.js +387 -104
  9. package/dist/cjs/cookie-parser.js +103 -5
  10. package/dist/cjs/cookie-store.js +125 -28
  11. package/dist/cjs/core.js +465 -66
  12. package/dist/cjs/dedup.js +49 -11
  13. package/dist/cjs/digest.js +160 -24
  14. package/dist/cjs/graphql.js +164 -24
  15. package/dist/cjs/headers.js +303 -45
  16. package/dist/cjs/interceptors.js +221 -7
  17. package/dist/cjs/lifecycle.js +89 -40
  18. package/dist/cjs/logging.js +168 -15
  19. package/dist/cjs/mod.js +3 -2
  20. package/dist/cjs/pagination.js +247 -22
  21. package/dist/cjs/progress.js +177 -27
  22. package/dist/cjs/proxy.js +412 -0
  23. package/dist/cjs/response.js +316 -47
  24. package/dist/cjs/socks5.js +131 -15
  25. package/dist/cjs/sse.js +173 -43
  26. package/dist/cjs/url.js +191 -45
  27. package/dist/cjs/utils.js +222 -48
  28. package/dist/cjs/ws.js +19 -10
  29. package/dist/esm/aws-sigv4.js +133 -19
  30. package/dist/esm/aws-sigv4.js.map +1 -1
  31. package/dist/esm/cache.js +49 -7
  32. package/dist/esm/cache.js.map +1 -1
  33. package/dist/esm/circuit-breaker.js +45 -3
  34. package/dist/esm/circuit-breaker.js.map +1 -1
  35. package/dist/esm/client.js +387 -104
  36. package/dist/esm/client.js.map +1 -1
  37. package/dist/esm/cookie-parser.js +103 -5
  38. package/dist/esm/cookie-parser.js.map +1 -1
  39. package/dist/esm/cookie-store.js +125 -28
  40. package/dist/esm/cookie-store.js.map +1 -1
  41. package/dist/esm/core.js +465 -66
  42. package/dist/esm/core.js.map +1 -1
  43. package/dist/esm/dedup.js +49 -11
  44. package/dist/esm/dedup.js.map +1 -1
  45. package/dist/esm/digest.js +160 -24
  46. package/dist/esm/digest.js.map +1 -1
  47. package/dist/esm/graphql.js +164 -24
  48. package/dist/esm/graphql.js.map +1 -1
  49. package/dist/esm/headers.js +303 -45
  50. package/dist/esm/headers.js.map +1 -1
  51. package/dist/esm/interceptors.js +221 -7
  52. package/dist/esm/interceptors.js.map +1 -1
  53. package/dist/esm/lifecycle.js +89 -40
  54. package/dist/esm/lifecycle.js.map +1 -1
  55. package/dist/esm/logging.js +168 -15
  56. package/dist/esm/logging.js.map +1 -1
  57. package/dist/esm/mod.js +3 -2
  58. package/dist/esm/mod.js.map +1 -1
  59. package/dist/esm/pagination.js +247 -22
  60. package/dist/esm/pagination.js.map +1 -1
  61. package/dist/esm/progress.js +177 -27
  62. package/dist/esm/progress.js.map +1 -1
  63. package/dist/esm/proxy.js +413 -0
  64. package/dist/esm/proxy.js.map +1 -0
  65. package/dist/esm/response.js +316 -47
  66. package/dist/esm/response.js.map +1 -1
  67. package/dist/esm/socks5.js +131 -15
  68. package/dist/esm/socks5.js.map +1 -1
  69. package/dist/esm/sse.js +173 -43
  70. package/dist/esm/sse.js.map +1 -1
  71. package/dist/esm/types.js.map +1 -1
  72. package/dist/esm/url.js +191 -45
  73. package/dist/esm/url.js.map +1 -1
  74. package/dist/esm/utils.js +222 -48
  75. package/dist/esm/utils.js.map +1 -1
  76. package/dist/esm/ws.js +19 -10
  77. package/dist/esm/ws.js.map +1 -1
  78. package/dist/types/aws-sigv4.d.ts.map +1 -1
  79. package/dist/types/cache.d.ts +19 -1
  80. package/dist/types/cache.d.ts.map +1 -1
  81. package/dist/types/circuit-breaker.d.ts +14 -1
  82. package/dist/types/circuit-breaker.d.ts.map +1 -1
  83. package/dist/types/client.d.ts +69 -11
  84. package/dist/types/client.d.ts.map +1 -1
  85. package/dist/types/cookie-parser.d.ts +0 -17
  86. package/dist/types/cookie-parser.d.ts.map +1 -1
  87. package/dist/types/cookie-store.d.ts.map +1 -1
  88. package/dist/types/core.d.ts +103 -25
  89. package/dist/types/core.d.ts.map +1 -1
  90. package/dist/types/dedup.d.ts.map +1 -1
  91. package/dist/types/digest.d.ts +17 -37
  92. package/dist/types/digest.d.ts.map +1 -1
  93. package/dist/types/graphql.d.ts.map +1 -1
  94. package/dist/types/headers.d.ts +45 -27
  95. package/dist/types/headers.d.ts.map +1 -1
  96. package/dist/types/interceptors.d.ts +102 -0
  97. package/dist/types/interceptors.d.ts.map +1 -1
  98. package/dist/types/lifecycle.d.ts +19 -2
  99. package/dist/types/lifecycle.d.ts.map +1 -1
  100. package/dist/types/logging.d.ts +22 -3
  101. package/dist/types/logging.d.ts.map +1 -1
  102. package/dist/types/mod.d.ts +5 -3
  103. package/dist/types/mod.d.ts.map +1 -1
  104. package/dist/types/pagination.d.ts +0 -25
  105. package/dist/types/pagination.d.ts.map +1 -1
  106. package/dist/types/progress.d.ts +1 -1
  107. package/dist/types/progress.d.ts.map +1 -1
  108. package/dist/types/proxy.d.ts +50 -0
  109. package/dist/types/proxy.d.ts.map +1 -0
  110. package/dist/types/response.d.ts +7 -1
  111. package/dist/types/response.d.ts.map +1 -1
  112. package/dist/types/socks5.d.ts.map +1 -1
  113. package/dist/types/sse.d.ts.map +1 -1
  114. package/dist/types/types.d.ts +114 -3
  115. package/dist/types/types.d.ts.map +1 -1
  116. package/dist/types/url.d.ts +0 -14
  117. package/dist/types/url.d.ts.map +1 -1
  118. package/dist/types/utils.d.ts.map +1 -1
  119. package/dist/types/ws.d.ts.map +1 -1
  120. package/package.json +1 -1
package/dist/cjs/dedup.js CHANGED
@@ -75,29 +75,54 @@ export class DedupMap {
75
75
  const effectiveSignal = signal ?? this._globalSignal;
76
76
  // Handle abort signal (F-2): if signal is already aborted, don't dedupe
77
77
  if (effectiveSignal?.aborted) {
78
+ // Still a real network call. It used to return before the counters were
79
+ // touched, so a client whose method filter excluded everything reported
80
+ // totalRequests 0 and a hitRate of 0 after any number of real requests
81
+ // — and `misses` is documented as "requests that triggered a real
82
+ // network call". Both bypasses below now count.
83
+ this._misses++;
78
84
  return factory();
79
85
  }
80
86
  if (!this.methods.has(method.toUpperCase())) {
87
+ this._misses++;
81
88
  return factory();
82
89
  }
83
- const key = this.keyFn(method, url, headers);
90
+ // The method is canonicalised before it reaches the key function. The
91
+ // methods filter has always been case-insensitive, but the key was built
92
+ // from the caller's spelling, so `execute("get", u)` and
93
+ // `execute("GET", u)` were two different keys and never coalesced — the
94
+ // one case a caller gets wrong most often, and one the library otherwise
95
+ // treats as equal everywhere.
96
+ const canonicalMethod = method.toUpperCase();
97
+ const key = this.keyFn(canonicalMethod, url, headers);
84
98
  const existing = this.inflight.get(key);
85
99
  if (existing) {
86
- // Within window: return cached result
87
- if (existing.resolvedAt !== null &&
88
- effectiveWindowMs > 0 &&
89
- Date.now() - existing.resolvedAt <= effectiveWindowMs) {
100
+ // In-flight: share the promise. This is coalescing, not caching, so it
101
+ // is not gated on the window.
102
+ if (existing.resolvedAt === null) {
90
103
  this._hits++;
91
- // Return the typed promise directly - preserves error type
92
104
  return existing.promise;
93
105
  }
94
- // In-flight: share the promise
95
- if (existing.resolvedAt === null) {
106
+ // Within window: return cached result
107
+ if (effectiveWindowMs > 0 && Date.now() - existing.resolvedAt <= effectiveWindowMs) {
96
108
  this._hits++;
109
+ // Return the typed promise directly - preserves error type
97
110
  return existing.promise;
98
111
  }
99
- // Expired window — fall through to new request
100
- this.inflight.delete(key);
112
+ if (effectiveWindowMs > 0) {
113
+ // This caller's own window has lapsed — drop the stale entry and lead.
114
+ this.inflight.delete(key);
115
+ }
116
+ else {
117
+ // This caller explicitly asked for no window, so it gets its own
118
+ // request. It used to fall through to `inflight.delete(key)` and lead
119
+ // as well, which destroyed a window some *other* caller had
120
+ // established and was still relying on: one `windowMs: 0` call was
121
+ // enough to empty the cache for everyone, and the next windowed caller
122
+ // had to go to the network for a result that was still in hand.
123
+ this._misses++;
124
+ return factory();
125
+ }
101
126
  }
102
127
  this._misses++;
103
128
  // Create entry and register it in the inflight map BEFORE starting the async work
@@ -132,6 +157,16 @@ export class DedupMap {
132
157
  })().then((result) => {
133
158
  entry.resolvedAt = Date.now();
134
159
  entry.result = result;
160
+ // This entry may have lost the key while the factory was running: an
161
+ // abort, a `clear()`, or an `invalidate()` all drop it, and a later
162
+ // caller for the same URL then becomes the leader. Registering a
163
+ // window timer here regardless re-inserted a timer for a key someone
164
+ // else now owns, and when it fired it deleted their still-in-flight
165
+ // entry — so the request after that started a third copy. The
166
+ // identity check is what makes "who owns this key" decidable.
167
+ if (this.inflight.get(key) !== entry) {
168
+ return result;
169
+ }
135
170
  if (effectiveWindowMs <= 0) {
136
171
  this.inflight.delete(key);
137
172
  }
@@ -155,7 +190,10 @@ export class DedupMap {
155
190
  }, (err) => {
156
191
  entry.resolvedAt = Date.now();
157
192
  entry.error = err;
158
- this.inflight.delete(key);
193
+ // Same ownership rule as the success path: an entry that has already
194
+ // been dropped must not delete the entry that replaced it.
195
+ if (this.inflight.get(key) === entry)
196
+ this.inflight.delete(key);
159
197
  const existingTimeout = this.timeouts.get(key);
160
198
  if (existingTimeout !== undefined) {
161
199
  clearTimeout(existingTimeout);
@@ -8,7 +8,7 @@
8
8
  * WWW-Authenticate challenge, compute the response hash, and format the
9
9
  * Authorization header in one call.
10
10
  */
11
- import { randomBytes } from "./utils.js";
11
+ import { randomBytes, uint8ArrayToBase64 } from "./utils.js";
12
12
  // ============================================================================
13
13
  // §1 MD5 — Pure JS (no Web Crypto dependency)
14
14
  // ============================================================================
@@ -133,16 +133,37 @@ function md5(input) {
133
133
  * console.log(challenge.realm); // "testrealm@host.com"
134
134
  * ```
135
135
  */
136
+ /** Decode a hex digest string into its raw bytes. */
137
+ function hexToBytes(hex) {
138
+ const out = new Uint8Array(hex.length / 2);
139
+ for (let i = 0; i < out.length; i++) {
140
+ out[i] = parseInt(hex.substr(i * 2, 2), 16);
141
+ }
142
+ return out;
143
+ }
144
+ /** Decode `quoted-pair` escapes (RFC 7230 §3.2.6) in a quoted-string body. */
145
+ function unescapeQuoted(value) {
146
+ return value.replace(/\\(.)/g, "$1");
147
+ }
136
148
  export function parseDigestChallenge(header) {
137
149
  const challenge = {
138
150
  algorithm: "MD5",
139
151
  };
140
152
  const cleaned = header.replace(/^Digest\s+/i, "").trim();
141
- const regex = /(\w+)\s*=\s*(?:"([^"]*)"|([^,\s]+))/g;
153
+ // A quoted-string may contain a `quoted-pair` (RFC 7230 §3.2.6): `"` and
154
+ // `\\` are literal characters, not delimiters. The previous `"([^"]*)"`
155
+ // stopped at the first quote regardless of a preceding backslash, so
156
+ // `realm="a\"b"` parsed as `a\` and left `b"` to be re-scanned as if it
157
+ // were another parameter — a realm containing a quote silently truncated
158
+ // the challenge, and `realm="x\", qop="auth-int"` did the same to `qop`.
159
+ // Round-tripping a header produced by `formatDigestAuth` was therefore
160
+ // lossy for exactly the values that need escaping.
161
+ const quotedString = '"((?:[^"\\\\]|\\\\.)*)"';
162
+ const regex = new RegExp(`(\\w+)\\s*=\\s*(?:${quotedString}|([^,\\s]+))`, "g");
142
163
  let match;
143
164
  while ((match = regex.exec(cleaned)) !== null) {
144
165
  const key = match[1].toLowerCase();
145
- const value = (match[2] ?? match[3]);
166
+ const value = match[2] !== undefined ? unescapeQuoted(match[2]) : match[3];
146
167
  switch (key) {
147
168
  case "realm":
148
169
  challenge.realm = value;
@@ -428,7 +449,11 @@ export async function computeDigestResponse(challenge, username, password, metho
428
449
  const realm = challenge.realm;
429
450
  const _cnonce = cnonce || randomBytes(5);
430
451
  const _nc = nc || "00000001";
431
- const algo = resolveHashAlgo(challenge.algorithm);
452
+ const resolved = resolveHashAlgo(challenge.algorithm);
453
+ if (resolved === null) {
454
+ throw new Error(`Unsupported digest algorithm: ${challenge.algorithm}`);
455
+ }
456
+ const { algo, sess } = resolved;
432
457
  async function hash(s) {
433
458
  if (algo === "MD5")
434
459
  return md5(s);
@@ -436,12 +461,44 @@ export async function computeDigestResponse(challenge, username, password, metho
436
461
  return sha512256Hex(s);
437
462
  return await sha256Hex(s);
438
463
  }
439
- const ha1 = await hash(`${username}:${realm}:${password}`);
464
+ // `-sess` binds the session (RFC 7616 §3.4.4/§3.4.5): the client nonce and
465
+ // the server nonce are folded into HA1, so the same credentials cannot be
466
+ // replayed under a different nonce. It requires `qop` to be present,
467
+ // because without one there is no cnonce to fold in.
468
+ // With `userhash=true`, RFC 7616 §3.4.4 substitutes the hashed username
469
+ // for the plaintext one everywhere it appears — including inside HA1.
470
+ const user = challenge.userhash
471
+ ? uint8ArrayToBase64(hexToBytes(await hash(`${username}:${realm}`)))
472
+ : username;
473
+ let ha1 = await hash(`${user}:${realm}:${password}`);
474
+ if (sess) {
475
+ if (!challenge.qop) {
476
+ throw new Error(`Digest algorithm ${challenge.algorithm} requires a qop parameter`);
477
+ }
478
+ ha1 = await hash(`${ha1}:${nonce}:${_cnonce}`);
479
+ }
440
480
  const ha2 = await hash(`${method}:${uri}`);
441
481
  let response;
442
482
  const qopList = qop.split(/\s*,\s*/).filter(Boolean);
443
- if (qopList.includes("auth") || qopList.includes("auth-int")) {
444
- response = await hash(`${ha1}:${nonce}:${_nc}:${_cnonce}:${qopList[0]}:${ha2}`);
483
+ if (qopList.length > 0) {
484
+ // RFC 7616 §3.4.3: with `qop=auth-int`, HA2 is
485
+ // H(method:uri:H(entity-body))
486
+ // and the entity-body hash is part of the response. This API is not given
487
+ // the body, so that hash cannot be produced — and the old code took
488
+ // `qopList[0]` and used it in the response while still hashing
489
+ // H(method:uri). A server offering `qop="auth-int,auth"`, which RFC 7616
490
+ // §3.4.4 explicitly says a client may see, was therefore answered with a
491
+ // `qop=auth-int` header carrying a response computed as if it were `auth`:
492
+ // a client that claims a protection it does not implement, and fails
493
+ // against every conforming server.
494
+ //
495
+ // So: prefer `auth`, which is the only one implementable here, and refuse
496
+ // rather than answer wrongly when only `auth-int` is offered.
497
+ if (!qopList.includes("auth")) {
498
+ throw new Error(`Digest challenge offers only unsupported qop values: ${qopList.join(", ")}. ` +
499
+ `"auth-int" requires the entity body, which this API does not receive.`);
500
+ }
501
+ response = await hash(`${ha1}:${nonce}:${_nc}:${_cnonce}:auth:${ha2}`);
445
502
  }
446
503
  else {
447
504
  response = await hash(`${ha1}:${nonce}:${ha2}`);
@@ -455,13 +512,64 @@ export async function computeDigestResponse(challenge, username, password, metho
455
512
  * @param algorithm - Algorithm string from the challenge (e.g. "MD5", "SHA-256")
456
513
  * @returns The resolved algorithm identifier
457
514
  */
515
+ /**
516
+ * Compute the `username*` value for a challenge offering `userhash=true`
517
+ * (RFC 7616 §3.4.4): `base64(H(username : realm))`.
518
+ *
519
+ * A server that sends `userhash=true` is telling the client *not* to send the
520
+ * username in the clear. The previous code parsed the flag, exposed it on the
521
+ * public {@link DigestChallenge} type with a JSDoc promising support, and then
522
+ * ignored it — sending a plain `username="admin"` back to a server that had
523
+ * explicitly asked for it to be hashed. That is a privacy regression, and the
524
+ * server rejects the request too, because HA1 was built from the unhashed name.
525
+ *
526
+ * @returns the base64 hashed username, or `null` when `userhash` is not offered
527
+ */
528
+ export async function computeUsernameStar(challenge, username) {
529
+ if (!challenge.userhash)
530
+ return null;
531
+ const resolved = resolveHashAlgo(challenge.algorithm);
532
+ if (resolved === null) {
533
+ throw new Error(`Unsupported digest algorithm: ${challenge.algorithm}`);
534
+ }
535
+ const { algo } = resolved;
536
+ const digest = algo === "MD5"
537
+ ? md5(`${username}:${challenge.realm}`)
538
+ : algo === "SHA-512-256"
539
+ ? sha512256Hex(`${username}:${challenge.realm}`)
540
+ : await sha256Hex(`${username}:${challenge.realm}`);
541
+ return uint8ArrayToBase64(hexToBytes(digest));
542
+ }
543
+ /**
544
+ * Resolve the challenge's `algorithm` field to a base hash plus the
545
+ * `-sess` session-binding flag (RFC 7616 §3.4.4/§3.4.5).
546
+ *
547
+ * Two separate defects lived here. First, "anything unrecognised is MD5" was
548
+ * a silent downgrade: `SCRAM-SHA-256` and `garbage` were answered with a
549
+ * 32-hex MD5 under a header naming a different algorithm, so the server's
550
+ * rejection blamed an algorithm the caller never chose. Second, the
551
+ * `-sess` forms stripped to `MD5SESS`/`SHA256SESS`, matched nothing, and then
552
+ * fell through the `hash` dispatch to its SHA-256 default — so `MD5-sess`
553
+ * was computed with SHA-256, and `SHA-256-sess` happened to work only by
554
+ * accident. An unrecognised algorithm is now an error.
555
+ *
556
+ * @returns the base algorithm and whether HA1 is session-bound, or `null`
557
+ * when the algorithm is not one we can compute
558
+ */
458
559
  function resolveHashAlgo(algorithm) {
459
- const upper = algorithm.toUpperCase().replace(/-/g, "");
460
- if (upper === "SHA256")
461
- return "SHA-256";
462
- if (upper === "SHA512256")
463
- return "SHA-512-256";
464
- return "MD5";
560
+ const trimmed = algorithm.trim();
561
+ const sess = /-sess$/i.test(trimmed);
562
+ const base = trimmed
563
+ .replace(/-sess$/i, "")
564
+ .toUpperCase()
565
+ .replace(/-/g, "");
566
+ if (base === "MD5")
567
+ return { algo: "MD5", sess };
568
+ if (base === "SHA256")
569
+ return { algo: "SHA-256", sess };
570
+ if (base === "SHA512256")
571
+ return { algo: "SHA-512-256", sess };
572
+ return null;
465
573
  }
466
574
  // ============================================================================
467
575
  // §5 DIGEST AUTHORIZATION HEADER FORMATTING
@@ -491,23 +599,49 @@ function resolveHashAlgo(algorithm) {
491
599
  * "/dir/index.html", "f2/wE", "00000001");
492
600
  * ```
493
601
  */
494
- export function formatDigestAuth(challenge, username, response, uri, cnonce, nc) {
602
+ /**
603
+ * Escape a value for a `quoted-string` (RFC 7230 §3.2.6): backslash and double
604
+ * quote are escaped, everything else passes through.
605
+ *
606
+ * A username or URI containing `"` previously terminated the quoted string
607
+ * early and everything after it was parsed as further auth-params — a
608
+ * username of `evil", qop=auth, x="` produced a header carrying an attacker-
609
+ * chosen `qop` and an extra parameter. `realm` and `opaque` come from the
610
+ * *server*, so a hostile or compromised one could inject into the client's own
611
+ * outgoing Authorization header.
612
+ */
613
+ function quoted(value) {
614
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
615
+ }
616
+ export function formatDigestAuth(challenge, username, response, uri, cnonce, nc, usernameStar) {
495
617
  const parts = [];
496
618
  const _cnonce = cnonce || randomBytes(5);
497
619
  const _nc = nc || "00000001";
498
- parts.push(`username="${username}"`);
499
- parts.push(`realm="${challenge.realm}"`);
500
- parts.push(`nonce="${challenge.nonce}"`);
501
- parts.push(`uri="${uri}"`);
502
- parts.push(`response="${response}"`);
620
+ if (challenge.userhash) {
621
+ if (usernameStar === undefined) {
622
+ throw new Error("challenge offers userhash=true: pass the value from computeUsernameStar() " +
623
+ "as `usernameStar` — sending a plaintext username would defeat it");
624
+ }
625
+ parts.push(`username*="${quoted(usernameStar)}"`);
626
+ }
627
+ else {
628
+ if (usernameStar !== undefined) {
629
+ throw new Error("usernameStar was given but the challenge does not offer userhash");
630
+ }
631
+ parts.push(`username="${quoted(username)}"`);
632
+ }
633
+ parts.push(`realm="${quoted(challenge.realm)}"`);
634
+ parts.push(`nonce="${quoted(challenge.nonce)}"`);
635
+ parts.push(`uri="${quoted(uri)}"`);
636
+ parts.push(`response="${quoted(response)}"`);
503
637
  if (challenge.opaque)
504
- parts.push(`opaque="${challenge.opaque}"`);
638
+ parts.push(`opaque="${quoted(challenge.opaque)}"`);
505
639
  if (challenge.algorithm && challenge.algorithm !== "MD5")
506
640
  parts.push(`algorithm=${challenge.algorithm}`);
507
641
  if (challenge.qop) {
508
- parts.push(`qop=${challenge.qop.split(/\s*,\s*/)[0]}`);
642
+ parts.push(`qop=auth`);
509
643
  parts.push(`nc=${_nc}`);
510
- parts.push(`cnonce="${_cnonce}"`);
644
+ parts.push(`cnonce="${quoted(_cnonce)}"`);
511
645
  }
512
646
  return `Digest ${parts.join(", ")}`;
513
647
  }
@@ -548,7 +682,8 @@ export async function createDigestAuthorization(wwwAuth, username, password, met
548
682
  const cnonce = randomBytes(5);
549
683
  const nc = "00000001";
550
684
  const response = await computeDigestResponse(challenge, username, password, method, uri, cnonce, nc);
551
- return formatDigestAuth(challenge, username, response, uri, cnonce, nc);
685
+ const usernameStar = await computeUsernameStar(challenge, username);
686
+ return formatDigestAuth(challenge, username, response, uri, cnonce, nc, usernameStar ?? undefined);
552
687
  }
553
688
  /**
554
689
  * Stateful Digest-auth authorizer.
@@ -573,6 +708,7 @@ export function createDigestAuthorizer() {
573
708
  counters.set(scope, next);
574
709
  const nc = next.toString(16).padStart(8, "0");
575
710
  const response = await computeDigestResponse(challenge, username, password, method, uri, cnonce, nc);
576
- return formatDigestAuth(challenge, username, response, uri, cnonce, nc);
711
+ const usernameStar = await computeUsernameStar(challenge, username);
712
+ return formatDigestAuth(challenge, username, response, uri, cnonce, nc, usernameStar ?? undefined);
577
713
  };
578
714
  }