mppx 0.3.12 → 0.3.13

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.
@@ -1 +1 @@
1
- {"version":3,"file":"Fetch.d.ts","sourceRoot":"","sources":["../../../src/client/internal/Fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,MAAM,iBAAiB,CAAA;AAC9C,OAAO,KAAK,KAAK,CAAC,MAAM,cAAc,CAAA;AAItC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,EACpE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAC3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAqCrB;AAED,6EAA6E;AAC7E,KAAK,aAAa,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,IAAI;KAC/D,CAAC,IAAI,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,GACtE,GAAG,SAAS,CAAC,CAAC,WAAW,GACvB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GACZ,SAAS,GACX,SAAS;CACd,CAAC,MAAM,CAAC,CAAA;AAET,MAAM,CAAC,OAAO,WAAW,IAAI,CAAC;IAC5B,KAAK,MAAM,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC,SAAS,EAAE,IAAI;QACvF,qEAAqE;QACrE,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;QAC/B,+BAA+B;QAC/B,OAAO,EAAE,OAAO,CAAA;QAChB,2EAA2E;QAC3E,WAAW,CAAC,EACR,CAAC,CACC,SAAS,EAAE,SAAS,CAAC,SAAS,EAC9B,OAAO,EAAE;YACP,gBAAgB,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;SACxE,KACE,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,GACjC,SAAS,CAAA;KACd,CAAA;IAED,KAAK,KAAK,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC,SAAS,EAAE,IAAI,CACtF,KAAK,EAAE,WAAW,GAAG,GAAG,EACxB,IAAI,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,KACxB,OAAO,CAAC,QAAQ,CAAC,CAAA;IAEtB,KAAK,WAAW,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC,SAAS,EAAE,IACxF,UAAU,CAAC,WAAW,GAAG;QACvB,+DAA+D;QAC/D,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;KACjC,CAAA;CACJ;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CAAC,KAAK,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,EACxE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,GAC/B,IAAI,CAGN;AAED,MAAM,CAAC,OAAO,WAAW,QAAQ,CAAC;IAChC,KAAK,MAAM,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC,SAAS,EAAE,IACnF,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,IAAI,IAAI,CAK9B"}
1
+ {"version":3,"file":"Fetch.d.ts","sourceRoot":"","sources":["../../../src/client/internal/Fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,MAAM,iBAAiB,CAAA;AAC9C,OAAO,KAAK,KAAK,CAAC,MAAM,cAAc,CAAA;AAatC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,EACpE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAC3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CA2CrB;AAED,6EAA6E;AAC7E,KAAK,aAAa,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,IAAI;KAC/D,CAAC,IAAI,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,GACtE,GAAG,SAAS,CAAC,CAAC,WAAW,GACvB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GACZ,SAAS,GACX,SAAS;CACd,CAAC,MAAM,CAAC,CAAA;AAET,MAAM,CAAC,OAAO,WAAW,IAAI,CAAC;IAC5B,KAAK,MAAM,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC,SAAS,EAAE,IAAI;QACvF,qEAAqE;QACrE,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;QAC/B,+BAA+B;QAC/B,OAAO,EAAE,OAAO,CAAA;QAChB,2EAA2E;QAC3E,WAAW,CAAC,EACR,CAAC,CACC,SAAS,EAAE,SAAS,CAAC,SAAS,EAC9B,OAAO,EAAE;YACP,gBAAgB,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;SACxE,KACE,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,GACjC,SAAS,CAAA;KACd,CAAA;IAED,KAAK,KAAK,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC,SAAS,EAAE,IAAI,CACtF,KAAK,EAAE,WAAW,GAAG,GAAG,EACxB,IAAI,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,KACxB,OAAO,CAAC,QAAQ,CAAC,CAAA;IAEtB,KAAK,WAAW,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC,SAAS,EAAE,IACxF,UAAU,CAAC,WAAW,GAAG;QACvB,+DAA+D;QAC/D,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;KACjC,CAAA;CACJ;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CAAC,KAAK,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,EACxE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,GAC/B,IAAI,CASN;AAED,MAAM,CAAC,OAAO,WAAW,QAAQ,CAAC;IAChC,KAAK,MAAM,CAAC,OAAO,SAAS,SAAS,MAAM,CAAC,SAAS,EAAE,GAAG,SAAS,MAAM,CAAC,SAAS,EAAE,IACnF,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,IAAI,IAAI,CAO9B"}
@@ -1,4 +1,8 @@
1
1
  import * as Challenge from '../../Challenge.js';
2
+ // We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
3
+ // even across multiple module instances/bundles. This lets restore() avoid clobbering
4
+ // an unrelated fetch installed by user code or another library.
5
+ const MPPX_FETCH_WRAPPER = Symbol.for('mppx.fetch.wrapper');
2
6
  let originalFetch;
3
7
  /**
4
8
  * Creates a fetch wrapper that automatically handles 402 Payment Required responses.
@@ -23,9 +27,12 @@ let originalFetch;
23
27
  */
24
28
  export function from(config) {
25
29
  const { fetch = globalThis.fetch, methods, onChallenge } = config;
26
- return async (input, init) => {
30
+ // Always operate on the true underlying fetch to avoid wrapper-on-wrapper stacking,
31
+ // which can duplicate retries and make restore semantics fragile.
32
+ const baseFetch = unwrapFetch(fetch);
33
+ const wrappedFetch = async (input, init) => {
27
34
  // Pass init through untouched to preserve object identity for non-402 responses.
28
- const response = await fetch(input, init);
35
+ const response = await baseFetch(input, init);
29
36
  if (response.status !== 402)
30
37
  return response;
31
38
  // Only extract context for payment handling after confirming 402.
@@ -41,14 +48,14 @@ export function from(config) {
41
48
  })
42
49
  : undefined;
43
50
  const credential = onChallengeCredential ?? (await resolveCredential(challenge, mi, context));
44
- return fetch(input, {
51
+ validateCredentialHeaderValue(credential);
52
+ return baseFetch(input, {
45
53
  ...fetchInit,
46
- headers: {
47
- ...normalizeHeaders(fetchInit.headers),
48
- Authorization: credential,
49
- },
54
+ headers: withAuthorizationHeader(fetchInit.headers, credential),
50
55
  });
51
56
  };
57
+ wrappedFetch[MPPX_FETCH_WRAPPER] = baseFetch;
58
+ return wrappedFetch;
52
59
  }
53
60
  /**
54
61
  * Replaces the global `fetch` with a payment-aware wrapper.
@@ -71,9 +78,14 @@ export function from(config) {
71
78
  * ```
72
79
  */
73
80
  export function polyfill(config) {
81
+ // Defensive guard for runtimes/tests where fetch might be non-configurable.
82
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'fetch');
83
+ if (!descriptor || (!descriptor.writable && !descriptor.set)) {
84
+ throw new Error('globalThis.fetch is not writable');
85
+ }
74
86
  if (!originalFetch)
75
87
  originalFetch = globalThis.fetch;
76
- globalThis.fetch = from(config);
88
+ globalThis.fetch = from({ ...config, fetch: globalThis.fetch });
77
89
  }
78
90
  /**
79
91
  * Restores the original `fetch` after calling `polyfill`.
@@ -90,7 +102,9 @@ export function polyfill(config) {
90
102
  * ```
91
103
  */
92
104
  export function restore() {
93
- if (originalFetch) {
105
+ // Only restore if the current fetch is still an mppx wrapper.
106
+ // If app code replaced fetch after polyfill(), we must not overwrite it.
107
+ if (originalFetch && isWrappedFetch(globalThis.fetch)) {
94
108
  globalThis.fetch = originalFetch;
95
109
  originalFetch = undefined;
96
110
  }
@@ -111,6 +125,38 @@ function normalizeHeaders(headers) {
111
125
  return headers;
112
126
  }
113
127
  /** @internal */
128
+ function withAuthorizationHeader(headers, credential) {
129
+ const normalized = normalizeHeaders(headers);
130
+ // Remove any existing Authorization header regardless of casing to avoid
131
+ // duplicate/conflicting credentials on retry.
132
+ for (const key of Object.keys(normalized)) {
133
+ if (key.toLowerCase() === 'authorization')
134
+ delete normalized[key];
135
+ }
136
+ normalized.Authorization = credential;
137
+ return normalized;
138
+ }
139
+ /** @internal */
140
+ function unwrapFetch(fetch) {
141
+ let current = fetch;
142
+ while (current[MPPX_FETCH_WRAPPER]) {
143
+ current = current[MPPX_FETCH_WRAPPER];
144
+ }
145
+ return current;
146
+ }
147
+ /** @internal */
148
+ function isWrappedFetch(fetch) {
149
+ return Boolean(fetch[MPPX_FETCH_WRAPPER]);
150
+ }
151
+ /** @internal */
152
+ function validateCredentialHeaderValue(credential) {
153
+ if (!credential.trim())
154
+ throw new Error('Credential header value must be non-empty');
155
+ if (credential.includes('\r') || credential.includes('\n')) {
156
+ throw new Error('Credential header value contains illegal newline characters');
157
+ }
158
+ }
159
+ /** @internal */
114
160
  async function resolveCredential(challenge, mi, context) {
115
161
  const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined;
116
162
  return mi.createCredential(parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge });
@@ -1 +1 @@
1
- {"version":3,"file":"Fetch.js","sourceRoot":"","sources":["../../../src/client/internal/Fetch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Fetch.js","sourceRoot":"","sources":["../../../src/client/internal/Fetch.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"constantTimeEqual.d.ts","sourceRoot":"","sources":["../../src/internal/constantTimeEqual.ts"],"names":[],"mappings":"AAEA,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAI/D"}
1
+ {"version":3,"file":"constantTimeEqual.d.ts","sourceRoot":"","sources":["../../src/internal/constantTimeEqual.ts"],"names":[],"mappings":"AAEA,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAM/D"}
@@ -1,8 +1,11 @@
1
- import { createHash, timingSafeEqual } from 'node:crypto';
1
+ import { Hash, Hex } from 'ox';
2
2
  /** Constant-time string comparison to prevent timing attacks. */
3
3
  export function constantTimeEqual(a, b) {
4
- const hashA = createHash('sha256').update(a).digest();
5
- const hashB = createHash('sha256').update(b).digest();
6
- return timingSafeEqual(hashA, hashB);
4
+ const hashA = Hash.sha256(Hex.fromString(a));
5
+ const hashB = Hash.sha256(Hex.fromString(b));
6
+ let result = 0;
7
+ for (let i = 0; i < hashA.length; i++)
8
+ result |= hashA.charCodeAt(i) ^ hashB.charCodeAt(i);
9
+ return result === 0;
7
10
  }
8
11
  //# sourceMappingURL=constantTimeEqual.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constantTimeEqual.js","sourceRoot":"","sources":["../../src/internal/constantTimeEqual.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEzD,iEAAiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,CAAS,EAAE,CAAS;IACpD,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;IACrD,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;IACrD,OAAO,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AACtC,CAAC"}
1
+ {"version":3,"file":"constantTimeEqual.js","sourceRoot":"","sources":["../../src/internal/constantTimeEqual.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,IAAI,CAAA;AAE9B,iEAAiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,CAAS,EAAE,CAAS;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5C,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAC1F,OAAO,MAAM,KAAK,CAAC,CAAA;AACrB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mppx",
3
3
  "type": "module",
4
- "version": "0.3.12",
4
+ "version": "0.3.13",
5
5
  "main": "./dist/index.js",
6
6
  "license": "MIT",
7
7
  "files": [
@@ -610,6 +610,34 @@ describe('Fetch.from: 402 retry headers normalization', () => {
610
610
  expect(retryHeaders.Accept).toBe('application/json')
611
611
  expect(retryHeaders.Authorization).toBe('credential')
612
612
  })
613
+
614
+ test('replaces existing authorization header case-insensitively', async () => {
615
+ let callCount = 0
616
+ const calls: { init: RequestInit | undefined }[] = []
617
+ const mockFetch: typeof globalThis.fetch = async (_input, init) => {
618
+ calls.push({ init })
619
+ callCount++
620
+ if (callCount === 1) return make402()
621
+ return new Response('OK', { status: 200 })
622
+ }
623
+
624
+ const fetch = Fetch.from({
625
+ fetch: mockFetch,
626
+ methods: [noopMethod],
627
+ })
628
+
629
+ await fetch('https://example.com/api', {
630
+ headers: { authorization: 'Bearer stale-token', 'X-Custom': 'value' },
631
+ })
632
+
633
+ const retryHeaders = (calls[1]!.init as Record<string, unknown>).headers as Record<
634
+ string,
635
+ string
636
+ >
637
+ expect(retryHeaders.authorization).toBeUndefined()
638
+ expect(retryHeaders.Authorization).toBe('credential')
639
+ expect(retryHeaders['X-Custom']).toBe('value')
640
+ })
613
641
  })
614
642
 
615
643
  describe('Fetch.from: input passthrough', () => {
@@ -722,4 +750,20 @@ describe('Fetch.polyfill / restore', () => {
722
750
  Fetch.restore()
723
751
  expect(globalThis.fetch).toBe(originalFetch)
724
752
  })
753
+
754
+ test('restore is a no-op when fetch was replaced externally after polyfill', () => {
755
+ const originalFetch = globalThis.fetch
756
+ const externalFetch = vi.fn(
757
+ async (_input: RequestInfo | URL, _init?: RequestInit) =>
758
+ new Response('external', { status: 200 }),
759
+ ) as unknown as typeof globalThis.fetch
760
+
761
+ Fetch.polyfill({ methods: [noopMethod] })
762
+ globalThis.fetch = externalFetch
763
+
764
+ Fetch.restore()
765
+ expect(globalThis.fetch).toBe(externalFetch)
766
+
767
+ globalThis.fetch = originalFetch
768
+ })
725
769
  })
@@ -2,6 +2,15 @@ import * as Challenge from '../../Challenge.js'
2
2
  import type * as Method from '../../Method.js'
3
3
  import type * as z from '../../zod.js'
4
4
 
5
+ // We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
6
+ // even across multiple module instances/bundles. This lets restore() avoid clobbering
7
+ // an unrelated fetch installed by user code or another library.
8
+ const MPPX_FETCH_WRAPPER = Symbol.for('mppx.fetch.wrapper')
9
+
10
+ type WrappedFetch = typeof globalThis.fetch & {
11
+ [MPPX_FETCH_WRAPPER]?: typeof globalThis.fetch
12
+ }
13
+
5
14
  let originalFetch: typeof globalThis.fetch | undefined
6
15
 
7
16
  /**
@@ -29,10 +38,13 @@ export function from<const methods extends readonly Method.AnyClient[]>(
29
38
  config: from.Config<methods>,
30
39
  ): from.Fetch<methods> {
31
40
  const { fetch = globalThis.fetch, methods, onChallenge } = config
41
+ // Always operate on the true underlying fetch to avoid wrapper-on-wrapper stacking,
42
+ // which can duplicate retries and make restore semantics fragile.
43
+ const baseFetch = unwrapFetch(fetch)
32
44
 
33
- return async (input, init) => {
45
+ const wrappedFetch = async (input: RequestInfo | URL, init?: from.RequestInit<methods>) => {
34
46
  // Pass init through untouched to preserve object identity for non-402 responses.
35
- const response = await fetch(input, init)
47
+ const response = await baseFetch(input, init)
36
48
 
37
49
  if (response.status !== 402) return response
38
50
 
@@ -55,15 +67,18 @@ export function from<const methods extends readonly Method.AnyClient[]>(
55
67
  })
56
68
  : undefined
57
69
  const credential = onChallengeCredential ?? (await resolveCredential(challenge, mi, context))
70
+ validateCredentialHeaderValue(credential)
58
71
 
59
- return fetch(input, {
72
+ return baseFetch(input, {
60
73
  ...fetchInit,
61
- headers: {
62
- ...normalizeHeaders(fetchInit.headers),
63
- Authorization: credential,
64
- },
74
+ headers: withAuthorizationHeader(fetchInit.headers, credential),
65
75
  })
66
76
  }
77
+
78
+ // Record the wrapped target so future polyfill() / restore() calls can detect origin
79
+ // and safely unwrap only mppx-installed wrappers.
80
+ ;(wrappedFetch as WrappedFetch)[MPPX_FETCH_WRAPPER] = baseFetch
81
+ return wrappedFetch as from.Fetch<methods>
67
82
  }
68
83
 
69
84
  /** Union of all context types from all methods that have context schemas. */
@@ -127,8 +142,14 @@ export declare namespace from {
127
142
  export function polyfill<const methods extends readonly Method.AnyClient[]>(
128
143
  config: polyfill.Config<methods>,
129
144
  ): void {
145
+ // Defensive guard for runtimes/tests where fetch might be non-configurable.
146
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'fetch')
147
+ if (!descriptor || (!descriptor.writable && !descriptor.set)) {
148
+ throw new Error('globalThis.fetch is not writable')
149
+ }
150
+
130
151
  if (!originalFetch) originalFetch = globalThis.fetch
131
- globalThis.fetch = from(config) as typeof globalThis.fetch
152
+ globalThis.fetch = from({ ...config, fetch: globalThis.fetch }) as typeof globalThis.fetch
132
153
  }
133
154
 
134
155
  export declare namespace polyfill {
@@ -151,7 +172,9 @@ export declare namespace polyfill {
151
172
  * ```
152
173
  */
153
174
  export function restore(): void {
154
- if (originalFetch) {
175
+ // Only restore if the current fetch is still an mppx wrapper.
176
+ // If app code replaced fetch after polyfill(), we must not overwrite it.
177
+ if (originalFetch && isWrappedFetch(globalThis.fetch)) {
155
178
  globalThis.fetch = originalFetch
156
179
  originalFetch = undefined
157
180
  }
@@ -171,6 +194,40 @@ function normalizeHeaders(headers: unknown): Record<string, string> {
171
194
  return headers as Record<string, string>
172
195
  }
173
196
 
197
+ /** @internal */
198
+ function withAuthorizationHeader(headers: unknown, credential: string): Record<string, string> {
199
+ const normalized = normalizeHeaders(headers)
200
+ // Remove any existing Authorization header regardless of casing to avoid
201
+ // duplicate/conflicting credentials on retry.
202
+ for (const key of Object.keys(normalized)) {
203
+ if (key.toLowerCase() === 'authorization') delete normalized[key]
204
+ }
205
+ normalized.Authorization = credential
206
+ return normalized
207
+ }
208
+
209
+ /** @internal */
210
+ function unwrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch {
211
+ let current = fetch as WrappedFetch
212
+ while (current[MPPX_FETCH_WRAPPER]) {
213
+ current = current[MPPX_FETCH_WRAPPER] as WrappedFetch
214
+ }
215
+ return current as typeof globalThis.fetch
216
+ }
217
+
218
+ /** @internal */
219
+ function isWrappedFetch(fetch: typeof globalThis.fetch): fetch is WrappedFetch {
220
+ return Boolean((fetch as WrappedFetch)[MPPX_FETCH_WRAPPER])
221
+ }
222
+
223
+ /** @internal */
224
+ function validateCredentialHeaderValue(credential: string): void {
225
+ if (!credential.trim()) throw new Error('Credential header value must be non-empty')
226
+ if (credential.includes('\r') || credential.includes('\n')) {
227
+ throw new Error('Credential header value contains illegal newline characters')
228
+ }
229
+ }
230
+
174
231
  /** @internal */
175
232
  async function resolveCredential(
176
233
  challenge: Challenge.Challenge,
@@ -1,8 +1,10 @@
1
- import { createHash, timingSafeEqual } from 'node:crypto'
1
+ import { Hash, Hex } from 'ox'
2
2
 
3
3
  /** Constant-time string comparison to prevent timing attacks. */
4
4
  export function constantTimeEqual(a: string, b: string): boolean {
5
- const hashA = createHash('sha256').update(a).digest()
6
- const hashB = createHash('sha256').update(b).digest()
7
- return timingSafeEqual(hashA, hashB)
5
+ const hashA = Hash.sha256(Hex.fromString(a))
6
+ const hashB = Hash.sha256(Hex.fromString(b))
7
+ let result = 0
8
+ for (let i = 0; i < hashA.length; i++) result |= hashA.charCodeAt(i) ^ hashB.charCodeAt(i)
9
+ return result === 0
8
10
  }