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.
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +55 -9
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/constantTimeEqual.d.ts.map +1 -1
- package/dist/internal/constantTimeEqual.js +7 -4
- package/dist/internal/constantTimeEqual.js.map +1 -1
- package/package.json +1 -1
- package/src/client/internal/Fetch.test.ts +44 -0
- package/src/client/internal/Fetch.ts +66 -9
- package/src/internal/constantTimeEqual.ts +6 -4
|
@@ -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;
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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":"AAAA,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAI/C,IAAI,aAAkD,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,IAAI,CAClB,MAA4B;IAE5B,MAAM,EAAE,KAAK,GAAG,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CAAA;
|
|
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,
|
|
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 {
|
|
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 =
|
|
5
|
-
const hashB =
|
|
6
|
-
|
|
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,
|
|
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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 =
|
|
6
|
-
const hashB =
|
|
7
|
-
|
|
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
|
}
|