diff --git a/package-lock.json b/package-lock.json index 435704e..5c582ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@sasjs/utils": "^3.5.6", "axios": "1.16.0", "axios-cookiejar-support": "5.0.5", - "form-data": "4.0.4", + "form-data": "4.0.6", "https": "1.0.0", "tough-cookie": "4.1.3" }, @@ -3556,22 +3556,6 @@ "tough-cookie": ">=4.0.0" } }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -6179,16 +6163,16 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -6616,7 +6600,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" diff --git a/package.json b/package.json index df9d846..36a9b37 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@sasjs/utils": "^3.5.6", "axios": "1.16.0", "axios-cookiejar-support": "5.0.5", - "form-data": "4.0.4", + "form-data": "4.0.6", "https": "1.0.0", "tough-cookie": "4.1.3" } diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index a1f5355d9..fc3df2e 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -375,7 +375,7 @@ export class AuthManager { * */ public async logOut() { - this.requestClient.clearCsrfTokens() + this.requestClient.resetInMemoryAuthState() return this.requestClient.get(this.logoutUrl, undefined).then(() => true) } diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index 82ba21b..6b61a02 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -28,6 +28,9 @@ import { import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError' import { inspect } from 'util' +const getLogger = () => + (typeof process !== 'undefined' && process.logger) || console + export class RequestClient implements HttpClient { private requests: SASjsRequest[] = [] private requestsLimit: number = 10 @@ -37,6 +40,7 @@ export class RequestClient implements HttpClient { protected csrfToken: CsrfToken = { headerName: '', value: '' } protected fileUploadCsrfToken: CsrfToken | undefined protected httpClient!: AxiosInstance + private isRecoveringFromNetworkError = false constructor( protected baseUrl: string, @@ -77,6 +81,25 @@ export class RequestClient implements HttpClient { localStorage.setItem('refreshToken', '') } + public resetInMemoryAuthState() { + this.clearCsrfTokens() + if (typeof localStorage !== 'undefined') { + this.clearLocalStorageTokens() + } + if (typeof document !== 'undefined') { + this.clearAllCookies() + } + } + + private clearAllCookies() { + const cookies = document.cookie.split(';') + for (const cookie of cookies) { + const name = cookie.split('=')[0].trim() + if (!name) continue + document.cookie = `${name}=; Max-Age=0; Path=/;` + } + } + public getBaseUrl() { return this.httpClient.defaults.baseURL || '' } @@ -354,6 +377,7 @@ export class RequestClient implements HttpClient { const csrfTokenKey = Object.keys(params).find((k) => k?.toLowerCase().includes('csrf') ) + if (csrfTokenKey) { this.csrfToken.value = params[csrfTokenKey] this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token' @@ -378,7 +402,7 @@ export class RequestClient implements HttpClient { }) .then((res) => res.data) .catch((error) => { - const logger = process.logger || console + const logger = getLogger() logger.error(error) }) } @@ -687,6 +711,45 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} throw new CertificateError(e.message) } + if ( + e.isAxiosError && + !response && + e.code === 'ERR_NETWORK' && + !this.isRecoveringFromNetworkError + ) { + // Opaque ERR_NETWORK usually means the server rejected stale credentials. + // Wipe in-memory auth state, re-establish session via GET /, + // then retry the original request. + this.resetInMemoryAuthState() + this.isRecoveringFromNetworkError = true + try { + // Re-establish session and CSRF cookie + const rootResponse = await this.httpClient + .get('/', { withXSRFToken: true }) + .catch((err) => err.response) + + if (rootResponse?.data) { + const cookie = + /