|
12 | 12 | const originalFetch = window.fetch; |
13 | 13 | const originalXHROpen = XMLHttpRequest.prototype.open; |
14 | 14 | const originalXHRSend = XMLHttpRequest.prototype.send; |
| 15 | + const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; |
| 16 | + |
| 17 | + // Duplicate of src/auth-header-utils.js — keep in sync. The canonical |
| 18 | + // module is imported by unit tests; this copy runs in page context where |
| 19 | + // classic scripts can't use ESM imports. |
| 20 | + function hasAuthorizationHeader (headers) { |
| 21 | + if (!headers) return false; |
| 22 | + if (typeof Headers !== 'undefined' && headers instanceof Headers) { |
| 23 | + return headers.has('authorization'); |
| 24 | + } |
| 25 | + if (Array.isArray(headers)) { |
| 26 | + return headers.some((entry) => |
| 27 | + Array.isArray(entry) && typeof entry[0] === 'string' && |
| 28 | + entry[0].toLowerCase() === 'authorization' |
| 29 | + ); |
| 30 | + } |
| 31 | + if (typeof headers === 'object') { |
| 32 | + for (const key of Object.keys(headers)) { |
| 33 | + if (key.toLowerCase() === 'authorization') return true; |
| 34 | + } |
| 35 | + } |
| 36 | + return false; |
| 37 | + } |
| 38 | + |
| 39 | + function fetchCallHasAuthorization (input, init) { |
| 40 | + if (hasAuthorizationHeader(init?.headers)) return true; |
| 41 | + if (typeof Request !== 'undefined' && input instanceof Request) { |
| 42 | + return hasAuthorizationHeader(input.headers); |
| 43 | + } |
| 44 | + return false; |
| 45 | + } |
| 46 | + |
| 47 | + function setAuthorizationOnOptions (options, value) { |
| 48 | + options.headers = options.headers || {}; |
| 49 | + if (typeof Headers !== 'undefined' && options.headers instanceof Headers) { |
| 50 | + options.headers.set('Authorization', value); |
| 51 | + } else if (Array.isArray(options.headers)) { |
| 52 | + const normalized = new Headers(options.headers); |
| 53 | + normalized.set('Authorization', value); |
| 54 | + options.headers = normalized; |
| 55 | + } else { |
| 56 | + options.headers['Authorization'] = value; |
| 57 | + } |
| 58 | + return options.headers; |
| 59 | + } |
| 60 | + |
| 61 | + function normalizeFetchCall (input, init) { |
| 62 | + if (typeof Request !== 'undefined' && input instanceof Request) { |
| 63 | + return { |
| 64 | + url: input.url, |
| 65 | + method: init?.method || input.method || 'GET', |
| 66 | + body: init?.body |
| 67 | + }; |
| 68 | + } |
| 69 | + return { |
| 70 | + url: typeof input === 'string' ? input : String(input), |
| 71 | + method: init?.method || 'GET', |
| 72 | + body: init?.body |
| 73 | + }; |
| 74 | + } |
15 | 75 |
|
16 | 76 | // Helper to get auth header (will be async, but we'll handle that) |
17 | 77 | let getAuthHeaderFn = null; |
|
24 | 84 | // Intercept fetch - MUST replace immediately to catch all calls |
25 | 85 | // This runs synchronously, so it catches fetch even if called immediately |
26 | 86 | window.fetch = function (url, options = {}) { |
27 | | - const urlString = typeof url === 'string' ? url : url.toString(); |
28 | | - const method = options?.method || 'GET'; |
| 87 | + // Normalize once — handles fetch(url, init) and fetch(new Request(...)) |
| 88 | + // so downstream signing sees the real URL/method, not "[object Request]". |
| 89 | + const { url: urlString, method, body } = normalizeFetchCall(url, options); |
29 | 90 | console.log('[Podkey] 🔍 fetch() intercepted:', urlString, method); |
30 | 91 |
|
| 92 | + // Respect an Authorization header the page already set — on either |
| 93 | + // options.headers or a Request input (e.g. Solid-OIDC DPoP). Overwriting |
| 94 | + // would re-identify the request as Podkey's NIP-98 and break the page's |
| 95 | + // own auth. If it fails with 401, the retry path below still injects |
| 96 | + // NIP-98. See issue #5. |
| 97 | + const pageSetAuth = fetchCallHasAuthorization(url, options); |
| 98 | + |
31 | 99 | // If we have the auth function, use it |
32 | | - if (authFunctionReady && getAuthHeaderFn) { |
| 100 | + if (authFunctionReady && getAuthHeaderFn && !pageSetAuth) { |
33 | 101 | console.log('[Podkey] ✅ Auth function ready, adding header...'); |
34 | 102 | const promise = (async () => { |
35 | 103 | try { |
36 | | - const authHeader = await getAuthHeaderFn(url, options.method || 'GET', options.body); |
| 104 | + const authHeader = await getAuthHeaderFn(urlString, method, body); |
37 | 105 | if (authHeader) { |
38 | 106 | options = options || {}; |
39 | | - options.headers = options.headers || {}; |
40 | | - if (options.headers instanceof Headers) { |
41 | | - options.headers.set('Authorization', authHeader); |
42 | | - console.log('[Podkey] ✅ Added NIP-98 auth header (Headers)'); |
43 | | - } else { |
44 | | - options.headers['Authorization'] = authHeader; |
45 | | - console.log('[Podkey] ✅ Added NIP-98 auth header (object)'); |
46 | | - } |
| 107 | + setAuthorizationOnOptions(options, authHeader); |
| 108 | + console.log('[Podkey] ✅ Added NIP-98 auth header'); |
47 | 109 | } else { |
48 | 110 | console.log('[Podkey] ⚠️ No auth header returned (will retry on 401)'); |
49 | 111 | } |
|
59 | 121 | console.log('[Podkey] 🔄 401 detected, retrying with auth...'); |
60 | 122 | return (async () => { |
61 | 123 | try { |
62 | | - const authHeader = await getAuthHeaderFn(url, options.method || 'GET', options.body); |
| 124 | + const authHeader = await getAuthHeaderFn(urlString, method, body); |
63 | 125 | if (authHeader) { |
64 | 126 | const retryOptions = { ...options }; |
65 | | - retryOptions.headers = retryOptions.headers || {}; |
66 | | - if (retryOptions.headers instanceof Headers) { |
67 | | - retryOptions.headers.set('Authorization', authHeader); |
68 | | - } else { |
69 | | - retryOptions.headers['Authorization'] = authHeader; |
70 | | - } |
| 127 | + setAuthorizationOnOptions(retryOptions, authHeader); |
71 | 128 | console.log('[Podkey] 🔄 Retrying with NIP-98 auth...'); |
72 | 129 | const retryResponse = await originalFetch.call(this, url, retryOptions); |
73 | 130 | if (retryResponse.status === 200 || retryResponse.status === 201) { |
|
102 | 159 | const checkAuth = () => { |
103 | 160 | if (authFunctionReady && getAuthHeaderFn) { |
104 | 161 | console.log('[Podkey] 🔄 Auth function now ready, retrying with NIP-98...'); |
105 | | - getAuthHeaderFn(url, method, options?.body).then(authHeader => { |
| 162 | + getAuthHeaderFn(urlString, method, body).then(authHeader => { |
106 | 163 | if (authHeader) { |
107 | 164 | const retryOptions = { ...options }; |
108 | | - retryOptions.headers = retryOptions.headers || {}; |
109 | | - if (retryOptions.headers instanceof Headers) { |
110 | | - retryOptions.headers.set('Authorization', authHeader); |
111 | | - } else { |
112 | | - retryOptions.headers['Authorization'] = authHeader; |
113 | | - } |
| 165 | + setAuthorizationOnOptions(retryOptions, authHeader); |
114 | 166 | originalFetch.call(this, url, retryOptions).then(resolve); |
115 | 167 | } else { |
116 | 168 | resolve(response); |
|
132 | 184 | XMLHttpRequest.prototype.open = function (method, url, ...args) { |
133 | 185 | this._podkeyMethod = method; |
134 | 186 | this._podkeyUrl = url; |
| 187 | + this._podkeyHasPageAuth = false; |
135 | 188 | return originalXHROpen.apply(this, [method, url, ...args]); |
136 | 189 | }; |
137 | 190 |
|
| 191 | + // Track a page-set Authorization on XHR so send() can respect it. |
| 192 | + XMLHttpRequest.prototype.setRequestHeader = function (name, value) { |
| 193 | + if (typeof name === 'string' && name.toLowerCase() === 'authorization') { |
| 194 | + this._podkeyHasPageAuth = true; |
| 195 | + } |
| 196 | + return originalXHRSetRequestHeader.apply(this, arguments); |
| 197 | + }; |
| 198 | + |
138 | 199 | XMLHttpRequest.prototype.send = function (body) { |
139 | | - if (getAuthHeaderFn && this._podkeyUrl) { |
| 200 | + if (getAuthHeaderFn && this._podkeyUrl && !this._podkeyHasPageAuth) { |
140 | 201 | (async () => { |
141 | 202 | try { |
142 | 203 | const authHeader = await getAuthHeaderFn(this._podkeyUrl, this._podkeyMethod, body); |
143 | 204 | if (authHeader) { |
144 | | - this.setRequestHeader('Authorization', authHeader); |
| 205 | + originalXHRSetRequestHeader.call(this, 'Authorization', authHeader); |
145 | 206 | } |
146 | 207 | } catch (e) { |
147 | 208 | console.error('[Podkey] Error in XHR interceptor:', e); |
|
0 commit comments