@webority/mobile-core 0.0.23
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/LICENSE +21 -0
- package/README.md +39 -0
- package/android/build.gradle +65 -0
- package/android/src/newarch/java/com/webority/mobilecore/smsretriever/AppSignatureHelper.kt +57 -0
- package/android/src/newarch/java/com/webority/mobilecore/smsretriever/SmsRetrieverModule.kt +20 -0
- package/android/src/newarch/java/com/webority/mobilecore/smsretriever/SmsRetrieverModuleImpl.kt +146 -0
- package/android/src/newarch/java/com/webority/mobilecore/smsretriever/SmsRetrieverPackage.kt +33 -0
- package/lib/commonjs/api/baseUrl.js +34 -0
- package/lib/commonjs/api/curl.js +109 -0
- package/lib/commonjs/api/publicClient.js +73 -0
- package/lib/commonjs/api/retry.js +75 -0
- package/lib/commonjs/api/tokenRefresh.js +205 -0
- package/lib/commonjs/api/userClient.js +255 -0
- package/lib/commonjs/auth/authStore.js +232 -0
- package/lib/commonjs/auth/index.js +44 -0
- package/lib/commonjs/auth/jwt.js +112 -0
- package/lib/commonjs/config/index.js +66 -0
- package/lib/commonjs/deepLink/index.js +286 -0
- package/lib/commonjs/download/adapters/expoFs.js +69 -0
- package/lib/commonjs/download/adapters/shared.js +13 -0
- package/lib/commonjs/download/index.js +250 -0
- package/lib/commonjs/formatters/currency.js +69 -0
- package/lib/commonjs/formatters/date.js +168 -0
- package/lib/commonjs/formatters/dateMath.js +310 -0
- package/lib/commonjs/formatters/index.js +246 -0
- package/lib/commonjs/formatters/initials.js +32 -0
- package/lib/commonjs/formatters/number.js +88 -0
- package/lib/commonjs/formatters/phone.js +369 -0
- package/lib/commonjs/formatters/windowsToIanaTable.js +164 -0
- package/lib/commonjs/geolocation/adapters/expoLocation.js +126 -0
- package/lib/commonjs/geolocation/index.js +355 -0
- package/lib/commonjs/index.js +630 -0
- package/lib/commonjs/initSDK.js +66 -0
- package/lib/commonjs/initUserAuth.js +37 -0
- package/lib/commonjs/logger/index.js +291 -0
- package/lib/commonjs/network/index.js +31 -0
- package/lib/commonjs/network/networkStatus.js +133 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/permissions/calendar.js +14 -0
- package/lib/commonjs/permissions/camera.js +14 -0
- package/lib/commonjs/permissions/contacts.js +10 -0
- package/lib/commonjs/permissions/index.js +133 -0
- package/lib/commonjs/permissions/location.js +14 -0
- package/lib/commonjs/permissions/mediaLibrary.js +10 -0
- package/lib/commonjs/permissions/notifications.js +10 -0
- package/lib/commonjs/permissions/photos.js +14 -0
- package/lib/commonjs/phone/countries.js +265 -0
- package/lib/commonjs/share/index.js +70 -0
- package/lib/commonjs/smsRetriever/index.js +91 -0
- package/lib/commonjs/specs/NativeSmsRetriever.js +15 -0
- package/lib/commonjs/sqlite/adapters/expo.js +53 -0
- package/lib/commonjs/sqlite/index.js +604 -0
- package/lib/commonjs/storage/index.js +214 -0
- package/lib/commonjs/types/globals.d.js +6 -0
- package/lib/commonjs/utils/imageCompression.js +389 -0
- package/lib/commonjs/utils/index.js +19 -0
- package/lib/commonjs/validators/index.js +227 -0
- package/lib/commonjs/versionCheck/index.js +419 -0
- package/lib/module/api/baseUrl.js +30 -0
- package/lib/module/api/curl.js +104 -0
- package/lib/module/api/publicClient.js +69 -0
- package/lib/module/api/retry.js +69 -0
- package/lib/module/api/tokenRefresh.js +192 -0
- package/lib/module/api/userClient.js +233 -0
- package/lib/module/auth/authStore.js +224 -0
- package/lib/module/auth/index.js +5 -0
- package/lib/module/auth/jwt.js +106 -0
- package/lib/module/config/index.js +58 -0
- package/lib/module/deepLink/index.js +280 -0
- package/lib/module/download/adapters/expoFs.js +64 -0
- package/lib/module/download/adapters/shared.js +8 -0
- package/lib/module/download/index.js +242 -0
- package/lib/module/formatters/currency.js +64 -0
- package/lib/module/formatters/date.js +160 -0
- package/lib/module/formatters/dateMath.js +294 -0
- package/lib/module/formatters/index.js +9 -0
- package/lib/module/formatters/initials.js +27 -0
- package/lib/module/formatters/number.js +81 -0
- package/lib/module/formatters/phone.js +302 -0
- package/lib/module/formatters/windowsToIanaTable.js +160 -0
- package/lib/module/geolocation/adapters/expoLocation.js +121 -0
- package/lib/module/geolocation/index.js +347 -0
- package/lib/module/index.js +54 -0
- package/lib/module/initSDK.js +61 -0
- package/lib/module/initUserAuth.js +33 -0
- package/lib/module/logger/index.js +285 -0
- package/lib/module/network/index.js +4 -0
- package/lib/module/network/networkStatus.js +124 -0
- package/lib/module/package.json +1 -0
- package/lib/module/permissions/calendar.js +15 -0
- package/lib/module/permissions/camera.js +15 -0
- package/lib/module/permissions/contacts.js +11 -0
- package/lib/module/permissions/index.js +128 -0
- package/lib/module/permissions/location.js +15 -0
- package/lib/module/permissions/mediaLibrary.js +11 -0
- package/lib/module/permissions/notifications.js +11 -0
- package/lib/module/permissions/photos.js +15 -0
- package/lib/module/phone/countries.js +261 -0
- package/lib/module/share/index.js +64 -0
- package/lib/module/smsRetriever/index.js +86 -0
- package/lib/module/specs/NativeSmsRetriever.js +13 -0
- package/lib/module/sqlite/adapters/expo.js +48 -0
- package/lib/module/sqlite/index.js +594 -0
- package/lib/module/storage/index.js +208 -0
- package/lib/module/types/globals.d.js +10 -0
- package/lib/module/utils/imageCompression.js +383 -0
- package/lib/module/utils/index.js +4 -0
- package/lib/module/validators/index.js +205 -0
- package/lib/module/versionCheck/index.js +413 -0
- package/lib/typescript/commonjs/api/baseUrl.d.ts +20 -0
- package/lib/typescript/commonjs/api/curl.d.ts +18 -0
- package/lib/typescript/commonjs/api/publicClient.d.ts +11 -0
- package/lib/typescript/commonjs/api/retry.d.ts +27 -0
- package/lib/typescript/commonjs/api/tokenRefresh.d.ts +43 -0
- package/lib/typescript/commonjs/api/userClient.d.ts +37 -0
- package/lib/typescript/commonjs/auth/authStore.d.ts +79 -0
- package/lib/typescript/commonjs/auth/index.d.ts +5 -0
- package/lib/typescript/commonjs/auth/jwt.d.ts +31 -0
- package/lib/typescript/commonjs/config/index.d.ts +55 -0
- package/lib/typescript/commonjs/deepLink/index.d.ts +54 -0
- package/lib/typescript/commonjs/download/adapters/expoFs.d.ts +38 -0
- package/lib/typescript/commonjs/download/adapters/shared.d.ts +3 -0
- package/lib/typescript/commonjs/download/index.d.ts +86 -0
- package/lib/typescript/commonjs/formatters/currency.d.ts +24 -0
- package/lib/typescript/commonjs/formatters/date.d.ts +22 -0
- package/lib/typescript/commonjs/formatters/dateMath.d.ts +88 -0
- package/lib/typescript/commonjs/formatters/index.d.ts +10 -0
- package/lib/typescript/commonjs/formatters/initials.d.ts +11 -0
- package/lib/typescript/commonjs/formatters/number.d.ts +15 -0
- package/lib/typescript/commonjs/formatters/phone.d.ts +133 -0
- package/lib/typescript/commonjs/formatters/windowsToIanaTable.d.ts +18 -0
- package/lib/typescript/commonjs/geolocation/adapters/expoLocation.d.ts +42 -0
- package/lib/typescript/commonjs/geolocation/index.d.ts +90 -0
- package/lib/typescript/commonjs/index.d.ts +54 -0
- package/lib/typescript/commonjs/initSDK.d.ts +72 -0
- package/lib/typescript/commonjs/initUserAuth.d.ts +41 -0
- package/lib/typescript/commonjs/logger/index.d.ts +81 -0
- package/lib/typescript/commonjs/network/index.d.ts +3 -0
- package/lib/typescript/commonjs/network/networkStatus.d.ts +43 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/permissions/calendar.d.ts +2 -0
- package/lib/typescript/commonjs/permissions/camera.d.ts +2 -0
- package/lib/typescript/commonjs/permissions/contacts.d.ts +2 -0
- package/lib/typescript/commonjs/permissions/index.d.ts +48 -0
- package/lib/typescript/commonjs/permissions/location.d.ts +2 -0
- package/lib/typescript/commonjs/permissions/mediaLibrary.d.ts +2 -0
- package/lib/typescript/commonjs/permissions/notifications.d.ts +2 -0
- package/lib/typescript/commonjs/permissions/photos.d.ts +2 -0
- package/lib/typescript/commonjs/phone/countries.d.ts +13 -0
- package/lib/typescript/commonjs/share/index.d.ts +29 -0
- package/lib/typescript/commonjs/smsRetriever/index.d.ts +30 -0
- package/lib/typescript/commonjs/specs/NativeSmsRetriever.d.ts +15 -0
- package/lib/typescript/commonjs/sqlite/adapters/expo.d.ts +30 -0
- package/lib/typescript/commonjs/sqlite/index.d.ts +113 -0
- package/lib/typescript/commonjs/storage/index.d.ts +64 -0
- package/lib/typescript/commonjs/utils/imageCompression.d.ts +152 -0
- package/lib/typescript/commonjs/utils/index.d.ts +3 -0
- package/lib/typescript/commonjs/validators/index.d.ts +51 -0
- package/lib/typescript/commonjs/versionCheck/index.d.ts +80 -0
- package/lib/typescript/module/api/baseUrl.d.ts +20 -0
- package/lib/typescript/module/api/curl.d.ts +18 -0
- package/lib/typescript/module/api/publicClient.d.ts +11 -0
- package/lib/typescript/module/api/retry.d.ts +27 -0
- package/lib/typescript/module/api/tokenRefresh.d.ts +43 -0
- package/lib/typescript/module/api/userClient.d.ts +37 -0
- package/lib/typescript/module/auth/authStore.d.ts +79 -0
- package/lib/typescript/module/auth/index.d.ts +5 -0
- package/lib/typescript/module/auth/jwt.d.ts +31 -0
- package/lib/typescript/module/config/index.d.ts +55 -0
- package/lib/typescript/module/deepLink/index.d.ts +54 -0
- package/lib/typescript/module/download/adapters/expoFs.d.ts +38 -0
- package/lib/typescript/module/download/adapters/shared.d.ts +3 -0
- package/lib/typescript/module/download/index.d.ts +86 -0
- package/lib/typescript/module/formatters/currency.d.ts +24 -0
- package/lib/typescript/module/formatters/date.d.ts +22 -0
- package/lib/typescript/module/formatters/dateMath.d.ts +88 -0
- package/lib/typescript/module/formatters/index.d.ts +10 -0
- package/lib/typescript/module/formatters/initials.d.ts +11 -0
- package/lib/typescript/module/formatters/number.d.ts +15 -0
- package/lib/typescript/module/formatters/phone.d.ts +133 -0
- package/lib/typescript/module/formatters/windowsToIanaTable.d.ts +18 -0
- package/lib/typescript/module/geolocation/adapters/expoLocation.d.ts +42 -0
- package/lib/typescript/module/geolocation/index.d.ts +90 -0
- package/lib/typescript/module/index.d.ts +54 -0
- package/lib/typescript/module/initSDK.d.ts +72 -0
- package/lib/typescript/module/initUserAuth.d.ts +41 -0
- package/lib/typescript/module/logger/index.d.ts +81 -0
- package/lib/typescript/module/network/index.d.ts +3 -0
- package/lib/typescript/module/network/networkStatus.d.ts +43 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/permissions/calendar.d.ts +2 -0
- package/lib/typescript/module/permissions/camera.d.ts +2 -0
- package/lib/typescript/module/permissions/contacts.d.ts +2 -0
- package/lib/typescript/module/permissions/index.d.ts +48 -0
- package/lib/typescript/module/permissions/location.d.ts +2 -0
- package/lib/typescript/module/permissions/mediaLibrary.d.ts +2 -0
- package/lib/typescript/module/permissions/notifications.d.ts +2 -0
- package/lib/typescript/module/permissions/photos.d.ts +2 -0
- package/lib/typescript/module/phone/countries.d.ts +13 -0
- package/lib/typescript/module/share/index.d.ts +29 -0
- package/lib/typescript/module/smsRetriever/index.d.ts +30 -0
- package/lib/typescript/module/specs/NativeSmsRetriever.d.ts +15 -0
- package/lib/typescript/module/sqlite/adapters/expo.d.ts +30 -0
- package/lib/typescript/module/sqlite/index.d.ts +113 -0
- package/lib/typescript/module/storage/index.d.ts +64 -0
- package/lib/typescript/module/utils/imageCompression.d.ts +152 -0
- package/lib/typescript/module/utils/index.d.ts +3 -0
- package/lib/typescript/module/validators/index.d.ts +51 -0
- package/lib/typescript/module/versionCheck/index.d.ts +80 -0
- package/package.json +298 -0
- package/src/specs/NativeSmsRetriever.ts +16 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Logger } from "../logger/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Header names whose VALUE is a credential. Matched case-insensitively against
|
|
7
|
+
* the whole name, since `Authorization` and `authorization` are the same header
|
|
8
|
+
* and axios does not normalise the case of headers a caller sets by hand.
|
|
9
|
+
*/
|
|
10
|
+
const SECRET_HEADERS = new Set(['authorization', 'proxy-authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-auth-token', 'x-access-token', 'x-refresh-token', 'api-key']);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Body fields whose value is a credential. Matched on the key alone, so this
|
|
14
|
+
* catches `password`, `newPassword` and `password_confirmation` alike.
|
|
15
|
+
*/
|
|
16
|
+
const SECRET_BODY_PATTERN = /pass(word|code)|secret|token|otp|pin|cvv|authoriz/i;
|
|
17
|
+
|
|
18
|
+
/** The axios per-method header buckets, which are config plumbing, not headers. */
|
|
19
|
+
const AXIOS_METHOD_KEYS = new Set(['common', 'delete', 'get', 'head', 'post', 'put', 'patch']);
|
|
20
|
+
const REDACTED = '<redacted>';
|
|
21
|
+
const redactBody = (value, seen, depth = 0) => {
|
|
22
|
+
if (value === null || typeof value !== 'object') {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
// Both guards are needed and they catch different shapes. `seen` breaks a
|
|
26
|
+
// true cycle, which would otherwise recurse forever AND make the eventual
|
|
27
|
+
// JSON.stringify throw. The depth bound catches a body that is merely
|
|
28
|
+
// enormous. A curl line is a debugging aid, and it must never be able to
|
|
29
|
+
// hang or fail the request it is describing.
|
|
30
|
+
if (seen.has(value)) {
|
|
31
|
+
return '<circular>';
|
|
32
|
+
}
|
|
33
|
+
if (depth > 6) {
|
|
34
|
+
return '<truncated>';
|
|
35
|
+
}
|
|
36
|
+
seen.add(value);
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return value.map(entry => redactBody(entry, seen, depth + 1));
|
|
39
|
+
}
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
42
|
+
out[key] = SECRET_BODY_PATTERN.test(key) ? REDACTED : redactBody(entry, seen, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
46
|
+
const buildUrl = config => {
|
|
47
|
+
const url = config.url ?? '';
|
|
48
|
+
const base = config.baseURL ? config.baseURL.replace(/\/$/, '') : '';
|
|
49
|
+
let complete = url.startsWith('http') ? url : `${base}/${url.replace(/^\//, '')}`;
|
|
50
|
+
if (config.params) {
|
|
51
|
+
const query = new URLSearchParams(config.params).toString();
|
|
52
|
+
if (query) {
|
|
53
|
+
complete += (complete.includes('?') ? '&' : '?') + query;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return complete;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builds a copy-pasteable curl command for a request, with every credential
|
|
61
|
+
* replaced by a placeholder.
|
|
62
|
+
*
|
|
63
|
+
* The redaction is the point, not a nicety. An unredacted version of this in a
|
|
64
|
+
* consumer app put `Authorization: Bearer <token>` and the login request's
|
|
65
|
+
* plaintext password into the device console of a *release* staging build.
|
|
66
|
+
* A curl line is worth having; a curl line that has to be trusted not to be
|
|
67
|
+
* enabled in the wrong build is not.
|
|
68
|
+
*/
|
|
69
|
+
export const buildCurl = config => {
|
|
70
|
+
const method = (config.method ?? 'get').toUpperCase();
|
|
71
|
+
let command = `curl -X ${method} "${buildUrl(config)}"`;
|
|
72
|
+
const headers = config.headers;
|
|
73
|
+
if (headers) {
|
|
74
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
75
|
+
if (AXIOS_METHOD_KEYS.has(key)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const shown = SECRET_HEADERS.has(key.toLowerCase()) ? REDACTED : value;
|
|
82
|
+
command += ` -H "${key}: ${shown}"`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (config.data !== undefined && config.data !== null) {
|
|
86
|
+
const redacted = typeof config.data === 'object' ? redactBody(config.data, new WeakSet()) : config.data;
|
|
87
|
+
const body = typeof redacted === 'object' ? JSON.stringify(redacted) : String(redacted);
|
|
88
|
+
command += ` -d '${body.replace(/'/g, "'\\''")}'`;
|
|
89
|
+
}
|
|
90
|
+
return command;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Logs the curl equivalent of a request. Never throws: a failure to describe a
|
|
95
|
+
* request must not be able to fail the request itself.
|
|
96
|
+
*/
|
|
97
|
+
export const logCurl = config => {
|
|
98
|
+
try {
|
|
99
|
+
Logger.info('CURL Command:', buildCurl(config));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
Logger.warn('[api] Failed to build a curl command for the request', error);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=curl.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { getConfigValue } from "../config/index.js";
|
|
5
|
+
import { Logger } from "../logger/index.js";
|
|
6
|
+
import { applyConfiguredBaseUrl } from "./baseUrl.js";
|
|
7
|
+
import { logCurl } from "./curl.js";
|
|
8
|
+
import { attachRetry } from "./retry.js";
|
|
9
|
+
|
|
10
|
+
// Define timeout for requests
|
|
11
|
+
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Public Axios Client
|
|
15
|
+
* - Base URL: Set your public API base URL.
|
|
16
|
+
* - No Authorization header is attached.
|
|
17
|
+
* - Includes timeout configuration and error handling.
|
|
18
|
+
* - Handles specific HTTP status codes.
|
|
19
|
+
*/
|
|
20
|
+
const publicClient = axios.create({
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
Accept: 'application/json',
|
|
24
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
25
|
+
},
|
|
26
|
+
timeout: REQUEST_TIMEOUT
|
|
27
|
+
});
|
|
28
|
+
applyConfiguredBaseUrl(publicClient);
|
|
29
|
+
|
|
30
|
+
// Registered before the logging interceptors below so they only ever observe
|
|
31
|
+
// the final outcome of a request, not its intermediate retries. The policy
|
|
32
|
+
// itself, and why a bare network error is never retried, lives in ./retry.
|
|
33
|
+
attachRetry(publicClient);
|
|
34
|
+
|
|
35
|
+
// Add request interceptor for logging
|
|
36
|
+
publicClient.interceptors.request.use(config => {
|
|
37
|
+
Logger.info(`Request: ${config.method?.toUpperCase()} ${config.url}`);
|
|
38
|
+
if (getConfigValue('logCurl')) {
|
|
39
|
+
logCurl(config);
|
|
40
|
+
}
|
|
41
|
+
return config;
|
|
42
|
+
}, error => {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
Logger.error('Request error:', message);
|
|
45
|
+
return Promise.reject(error);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Add response interceptor for error handling
|
|
49
|
+
publicClient.interceptors.response.use(response => {
|
|
50
|
+
Logger.info(`Response: ${response.status} from ${response.config.url}`);
|
|
51
|
+
return response;
|
|
52
|
+
}, error => {
|
|
53
|
+
if (axios.isAxiosError(error)) {
|
|
54
|
+
const originalRequest = error.config;
|
|
55
|
+
if (!error.response) {
|
|
56
|
+
Logger.error(`Network error or no response from server: ${error.message}, URL: ${originalRequest?.url}`);
|
|
57
|
+
} else {
|
|
58
|
+
const body = error.response.data;
|
|
59
|
+
const bodyText = body instanceof ArrayBuffer ? `binary response (${body.byteLength} bytes)` : typeof Blob !== 'undefined' && body instanceof Blob ? `binary response (${body.size} bytes)` : body ?? error.message;
|
|
60
|
+
Logger.error(`Response error: ${bodyText}, URL: ${originalRequest?.url}, Status: ${error.response.status}`);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
+
Logger.error(`Unknown response error: ${message}`);
|
|
65
|
+
}
|
|
66
|
+
return Promise.reject(error);
|
|
67
|
+
});
|
|
68
|
+
export default publicClient;
|
|
69
|
+
//# sourceMappingURL=publicClient.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Status-based ONLY, deliberately: a bare network error (no `error.response`)
|
|
5
|
+
* is never retryable. Both clients carry non-idempotent requests, login and OTP
|
|
6
|
+
* on the public one and every write on the user one, and a broken connection
|
|
7
|
+
* says nothing about whether the request reached the server. Retrying can
|
|
8
|
+
* therefore create a second session, or a duplicate record, that the user never
|
|
9
|
+
* sees. A generic network-error retry rule is not a substitute: it decides on
|
|
10
|
+
* the transport alone and never looks at the HTTP method, so it retries POSTs
|
|
11
|
+
* as readily as GETs.
|
|
12
|
+
*/
|
|
13
|
+
export const isRetryableStatus = status => typeof status === 'number' && (status === 429 || status >= 500 && status <= 599);
|
|
14
|
+
const DEFAULT_POLICY = {
|
|
15
|
+
retries: 3,
|
|
16
|
+
retryDelayMs: attempt => attempt <= 1 ? 0 : 2 ** attempt * 1000,
|
|
17
|
+
shouldRetry: error => isRetryableStatus(error.response?.status),
|
|
18
|
+
onRetry: () => undefined
|
|
19
|
+
};
|
|
20
|
+
const waitOrAbort = (delay, signal) => new Promise(resolve => {
|
|
21
|
+
const onAbort = () => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
resolve(false);
|
|
24
|
+
};
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
27
|
+
resolve(true);
|
|
28
|
+
}, delay);
|
|
29
|
+
signal?.addEventListener?.('abort', onAbort, {
|
|
30
|
+
once: true
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
export const attachRetry = (client, policy = {}) => {
|
|
34
|
+
client.interceptors.response.use(undefined, async error => {
|
|
35
|
+
const axiosError = error;
|
|
36
|
+
const config = axiosError?.config;
|
|
37
|
+
if (!config || config.retry === false) {
|
|
38
|
+
return Promise.reject(error);
|
|
39
|
+
}
|
|
40
|
+
const perRequest = typeof config.retry === 'object' ? config.retry : {};
|
|
41
|
+
const effective = {
|
|
42
|
+
...DEFAULT_POLICY,
|
|
43
|
+
...policy,
|
|
44
|
+
...perRequest
|
|
45
|
+
};
|
|
46
|
+
const attempt = (config.__retryAttempt ?? 0) + 1;
|
|
47
|
+
if (attempt > effective.retries || !effective.shouldRetry(axiosError)) {
|
|
48
|
+
return Promise.reject(error);
|
|
49
|
+
}
|
|
50
|
+
if (config.signal?.aborted) {
|
|
51
|
+
return Promise.reject(error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The attempt count rides on the config, which axios copies onto the config
|
|
55
|
+
// of the retried request, so nested attempts continue the same count and
|
|
56
|
+
// separate requests never share one.
|
|
57
|
+
config.__retryAttempt = attempt;
|
|
58
|
+
// The body was serialised by the first pass; transforming it again would
|
|
59
|
+
// JSON-encode the already-encoded string.
|
|
60
|
+
config.transformRequest = [data => data];
|
|
61
|
+
effective.onRetry(attempt, axiosError);
|
|
62
|
+
const proceed = await waitOrAbort(effective.retryDelayMs(attempt), config.signal);
|
|
63
|
+
if (!proceed) {
|
|
64
|
+
return Promise.reject(error);
|
|
65
|
+
}
|
|
66
|
+
return client.request(config);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { ACCESS_TOKEN_KEY, getToken, REFRESH_TOKEN_KEY, storeToken } from "../auth/authStore.js";
|
|
5
|
+
import { Logger } from "../logger/index.js";
|
|
6
|
+
import publicClient from "./publicClient.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The one token-refresh flow in the library, run by userClient's interceptors.
|
|
10
|
+
* The library calls the refresh endpoint itself, so it can tell a rejected
|
|
11
|
+
* refresh token (400 or 401: the session is over) from a transient failure
|
|
12
|
+
* (network, timeout, 5xx: the session is kept and the caller sees the error).
|
|
13
|
+
* A refresh is single flight. A download goes through the app's client, so it
|
|
14
|
+
* reaches this only when that client is userClient.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Minimal Redux-store shape required by the user client to dispatch logout.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
let store;
|
|
22
|
+
let auth;
|
|
23
|
+
export const injectStore = _store => {
|
|
24
|
+
store = _store;
|
|
25
|
+
};
|
|
26
|
+
export const injectUserAuth = options => {
|
|
27
|
+
auth = options;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Single promise to track token refresh
|
|
31
|
+
let refreshTokenPromise = null;
|
|
32
|
+
|
|
33
|
+
// Queue for failed requests
|
|
34
|
+
let failedQueue = [];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Processes the queue of failed requests.
|
|
38
|
+
*/
|
|
39
|
+
const processQueue = (error, token = null) => {
|
|
40
|
+
failedQueue.forEach(({
|
|
41
|
+
resolve,
|
|
42
|
+
reject
|
|
43
|
+
}) => {
|
|
44
|
+
if (error) {
|
|
45
|
+
reject(error);
|
|
46
|
+
} else if (token !== null) {
|
|
47
|
+
resolve(token);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
failedQueue = [];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Clear any in-flight token refresh and reject everything queued behind it.
|
|
55
|
+
* Call this on logout.
|
|
56
|
+
*
|
|
57
|
+
* Without it, a refresh that was already in flight when the user logged out
|
|
58
|
+
* resolves afterwards and replays every queued request with a token for a
|
|
59
|
+
* session that has ended. The queue is module state, so it survives the logout
|
|
60
|
+
* entirely: nothing else clears it. A consumer app hit this and hand-wrote the
|
|
61
|
+
* same function locally, which is how it was found.
|
|
62
|
+
*/
|
|
63
|
+
export const resetRefreshState = () => {
|
|
64
|
+
refreshTokenPromise = null;
|
|
65
|
+
processQueue(new Error('[@webority/mobile-core] Session ended'));
|
|
66
|
+
};
|
|
67
|
+
export const getErrorMessage = error => {
|
|
68
|
+
if (error instanceof Error) {
|
|
69
|
+
return error.message;
|
|
70
|
+
}
|
|
71
|
+
if (typeof error === 'string') {
|
|
72
|
+
return error;
|
|
73
|
+
}
|
|
74
|
+
return 'Unknown error';
|
|
75
|
+
};
|
|
76
|
+
const requireAuth = () => {
|
|
77
|
+
if (!store || !auth) {
|
|
78
|
+
throw new Error('User package store is not initialized. Call initUserAuth first.');
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
store,
|
|
82
|
+
auth
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** The refresh endpoint's path, once initUserAuth has run. */
|
|
87
|
+
export const getRefreshPath = () => auth?.refresh.path;
|
|
88
|
+
export const dispatchLogout = async () => {
|
|
89
|
+
const wired = requireAuth();
|
|
90
|
+
await wired.store.dispatch(wired.auth.logout());
|
|
91
|
+
};
|
|
92
|
+
export const isRefreshInFlight = () => refreshTokenPromise !== null;
|
|
93
|
+
|
|
94
|
+
/** Wait for the in-flight refresh without joining it: resolved or rejected by processQueue. */
|
|
95
|
+
export const queueBehindRefresh = () => new Promise((resolve, reject) => {
|
|
96
|
+
failedQueue.push({
|
|
97
|
+
resolve,
|
|
98
|
+
reject
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/** A failure that proves the session is over, as opposed to one worth retrying later. */
|
|
103
|
+
class SessionEndedError extends Error {
|
|
104
|
+
constructor(message) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = 'SessionEndedError';
|
|
107
|
+
// Babel's class transform can drop the prototype link, which makes
|
|
108
|
+
// `instanceof SessionEndedError` false in the transpiled build.
|
|
109
|
+
Object.setPrototypeOf(this, SessionEndedError.prototype);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const endsSession = error => error instanceof SessionEndedError || axios.isAxiosError(error) && (error.response?.status === 400 || error.response?.status === 401);
|
|
113
|
+
const asString = (value, field) => {
|
|
114
|
+
if (typeof value !== 'string' || value === '') {
|
|
115
|
+
throw new Error(`The refresh response has no "${field}".`);
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** The Webority API's refresh response. */
|
|
121
|
+
const readWeborityTokens = data => {
|
|
122
|
+
const body = data ?? {};
|
|
123
|
+
return {
|
|
124
|
+
accessToken: asString(body.accessToken, 'accessToken'),
|
|
125
|
+
accessTokenExpiry: asString(body.accessTokenExpiration, 'accessTokenExpiration'),
|
|
126
|
+
refreshToken: asString(body.refreshToken, 'refreshToken'),
|
|
127
|
+
refreshTokenExpiry: asString(body.refreshTokenExpiration, 'refreshTokenExpiration')
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
const callRefreshEndpoint = async refresh => {
|
|
131
|
+
const refreshToken = await getToken(REFRESH_TOKEN_KEY);
|
|
132
|
+
if (!refreshToken?.token) {
|
|
133
|
+
throw new SessionEndedError('No refresh token available');
|
|
134
|
+
}
|
|
135
|
+
// An expired refresh token would only earn a 401 from the endpoint before the same logout.
|
|
136
|
+
if (Date.parse(refreshToken.expiry) <= Date.now()) {
|
|
137
|
+
throw new SessionEndedError('Refresh token expired');
|
|
138
|
+
}
|
|
139
|
+
const accessToken = await getToken(ACCESS_TOKEN_KEY);
|
|
140
|
+
const tokens = {
|
|
141
|
+
accessToken: accessToken?.token ?? null,
|
|
142
|
+
refreshToken: refreshToken.token
|
|
143
|
+
};
|
|
144
|
+
const body = refresh.body ? await refresh.body(tokens) : tokens;
|
|
145
|
+
Logger.info('Refreshing the access token');
|
|
146
|
+
const {
|
|
147
|
+
data
|
|
148
|
+
} = await publicClient.post(refresh.path, body);
|
|
149
|
+
const next = (refresh.readTokens ?? readWeborityTokens)(data);
|
|
150
|
+
await storeToken(ACCESS_TOKEN_KEY, next.accessToken, next.accessTokenExpiry);
|
|
151
|
+
await storeToken(REFRESH_TOKEN_KEY, next.refreshToken, next.refreshTokenExpiry);
|
|
152
|
+
Logger.info('Access token refreshed');
|
|
153
|
+
return next.accessToken;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Refresh the access token, joining a refresh already in flight rather than
|
|
158
|
+
* starting a second one. Every request queued behind it is replayed on success
|
|
159
|
+
* and rejected on failure. Logs out only when the failure proves the session
|
|
160
|
+
* is over; a network error, timeout or 5xx keeps it and rejects.
|
|
161
|
+
*/
|
|
162
|
+
export const refreshAccessToken = () => {
|
|
163
|
+
if (refreshTokenPromise) {
|
|
164
|
+
return refreshTokenPromise;
|
|
165
|
+
}
|
|
166
|
+
const run = async () => {
|
|
167
|
+
try {
|
|
168
|
+
const token = await callRefreshEndpoint(requireAuth().auth.refresh);
|
|
169
|
+
processQueue(null, token);
|
|
170
|
+
return token;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const responseData = axios.isAxiosError(error) ? error.response?.data : undefined;
|
|
173
|
+
Logger.error('Token refresh failed:', responseData ?? getErrorMessage(error));
|
|
174
|
+
processQueue(error, null);
|
|
175
|
+
if (endsSession(error)) {
|
|
176
|
+
await dispatchLogout();
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
// Cleared in a chained finally, not inside run: a failure before run's first
|
|
182
|
+
// await would otherwise clear the slot before it was assigned, leaving a
|
|
183
|
+
// rejected promise that every later refresh joins.
|
|
184
|
+
const promise = run().finally(() => {
|
|
185
|
+
if (refreshTokenPromise === promise) {
|
|
186
|
+
refreshTokenPromise = null;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
refreshTokenPromise = promise;
|
|
190
|
+
return promise;
|
|
191
|
+
};
|
|
192
|
+
//# sourceMappingURL=tokenRefresh.js.map
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import axios, { AxiosHeaders } from 'axios';
|
|
4
|
+
import { ACCESS_TOKEN_KEY, getToken } from "../auth/authStore.js";
|
|
5
|
+
import { getConfigValue } from "../config/index.js";
|
|
6
|
+
import { Logger } from "../logger/index.js";
|
|
7
|
+
import { getNetworkStatus } from "../network/networkStatus.js";
|
|
8
|
+
import { applyConfiguredBaseUrl } from "./baseUrl.js";
|
|
9
|
+
import { logCurl } from "./curl.js";
|
|
10
|
+
import { attachRetry } from "./retry.js";
|
|
11
|
+
import { dispatchLogout, getErrorMessage, getRefreshPath, isRefreshInFlight, queueBehindRefresh, refreshAccessToken, resetRefreshState } from "./tokenRefresh.js";
|
|
12
|
+
export { injectStore, injectUserAuth, resetRefreshState } from "./tokenRefresh.js";
|
|
13
|
+
|
|
14
|
+
// Define timeout for requests
|
|
15
|
+
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Thrown by the request interceptor when the device is offline, before the
|
|
19
|
+
* request ever reaches the adapter (and therefore the network). Distinct from
|
|
20
|
+
* an AxiosError so callers can show an offline message instead of a generic
|
|
21
|
+
* request failure.
|
|
22
|
+
*/
|
|
23
|
+
export class RequestOfflineError extends Error {
|
|
24
|
+
constructor(message = 'Request blocked: device is offline') {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'RequestOfflineError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sub-codes a 401 response body carries when the session itself is dead
|
|
32
|
+
* (terminated by an admin, or expired server-side) rather than merely
|
|
33
|
+
* out-of-date. These never warrant a refresh attempt — only logout.
|
|
34
|
+
*/
|
|
35
|
+
export const SESSION_TERMINATION_CODES = {
|
|
36
|
+
TERMINATED: 'SESSION_TERMINATED',
|
|
37
|
+
EXPIRED: 'SESSION_EXPIRED'
|
|
38
|
+
};
|
|
39
|
+
const TERMINATION_CODES = new Set(Object.values(SESSION_TERMINATION_CODES));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The header is the AUTHORITATIVE signal, verified against the backend:
|
|
43
|
+
* `WeborityJwtMiddleware.ReturnUnauthorized` answers an invalidated session with
|
|
44
|
+
* `401` + `X-Security-Stamp-Invalid: true` and a body of
|
|
45
|
+
* `{ error: 'Unauthorized', message: '...' }` — no code field at all, and
|
|
46
|
+
* `error` is a string rather than an object. An earlier version of this checked
|
|
47
|
+
* only body fields and would therefore never have fired against a real server.
|
|
48
|
+
*
|
|
49
|
+
* The body codes below are kept as a secondary path for any head that does send
|
|
50
|
+
* an explicit sub-code; they match on exact strings, so looking is free.
|
|
51
|
+
*/
|
|
52
|
+
export const SESSION_INVALID_HEADER = 'x-security-stamp-invalid';
|
|
53
|
+
const hasSessionInvalidHeader = headers => {
|
|
54
|
+
if (!headers || typeof headers !== 'object') {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const get = headers.get;
|
|
58
|
+
const raw = typeof get === 'function' ? get.call(headers, SESSION_INVALID_HEADER) : headers[SESSION_INVALID_HEADER];
|
|
59
|
+
return String(raw).toLowerCase() === 'true';
|
|
60
|
+
};
|
|
61
|
+
const getSessionTerminationCode = data => {
|
|
62
|
+
if (!data || typeof data !== 'object') {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const body = data;
|
|
66
|
+
const nested = body.error;
|
|
67
|
+
const candidates = [body.code, body.errorCode, nested && typeof nested === 'object' ? nested.code : undefined];
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (typeof candidate === 'string' && TERMINATION_CODES.has(candidate)) {
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Internal extension of axios request config to track retry attempts.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
const userClient = axios.create({
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
84
|
+
},
|
|
85
|
+
timeout: REQUEST_TIMEOUT // Set timeout for all requests
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Registered before the auth interceptors below, so a retryable 5xx is retried
|
|
89
|
+
// before anything else sees it while a 401 still falls through to the refresh
|
|
90
|
+
// flow. Policy and reasoning live in ./retry.
|
|
91
|
+
attachRetry(userClient);
|
|
92
|
+
const setAuthHeader = (config, token) => {
|
|
93
|
+
if (config.headers instanceof AxiosHeaders) {
|
|
94
|
+
config.headers.set('Authorization', `Bearer ${token}`);
|
|
95
|
+
} else {
|
|
96
|
+
// Fallback in case headers were provided as a plain object.
|
|
97
|
+
config.headers = AxiosHeaders.from({
|
|
98
|
+
...config.headers,
|
|
99
|
+
Authorization: `Bearer ${token}`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const isRefreshTokenRequest = url => {
|
|
104
|
+
const path = getRefreshPath();
|
|
105
|
+
return !!path && !!url?.includes(path);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Attach Authorization Token Before Requests
|
|
110
|
+
*/
|
|
111
|
+
// Registered after the curl logger and before the auth interceptor, so that it
|
|
112
|
+
// RUNS between them: axios unshifts its request chain, making the first
|
|
113
|
+
// registered run last. The curl line therefore describes a request that already
|
|
114
|
+
// has both its baseURL and its Authorization header.
|
|
115
|
+
applyConfiguredBaseUrl(userClient);
|
|
116
|
+
|
|
117
|
+
// Registered BEFORE the auth interceptor below deliberately. Axios builds its
|
|
118
|
+
// request chain by unshifting, so the FIRST-registered request interceptor runs
|
|
119
|
+
// LAST — closest to dispatch. This one therefore observes the final config,
|
|
120
|
+
// with the Authorization header already attached, which is the whole point of
|
|
121
|
+
// a curl line. Registering it after would log the request before it is signed.
|
|
122
|
+
userClient.interceptors.request.use(config => {
|
|
123
|
+
if (getConfigValue('logCurl')) {
|
|
124
|
+
logCurl(config);
|
|
125
|
+
}
|
|
126
|
+
return config;
|
|
127
|
+
});
|
|
128
|
+
userClient.interceptors.request.use(async config => {
|
|
129
|
+
// Reject before any network call when the device is known to be offline.
|
|
130
|
+
// Unknown connectivity (netinfo not installed, or undetermined) is treated
|
|
131
|
+
// as online — never block a request on an unavailable probe.
|
|
132
|
+
const network = await getNetworkStatus();
|
|
133
|
+
if (network.isConnected === false) {
|
|
134
|
+
Logger.warn(`Request blocked: device is offline. URL: ${config.url}`);
|
|
135
|
+
return Promise.reject(new RequestOfflineError());
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
// Skip token check for refresh token endpoint to prevent infinite loop
|
|
139
|
+
if (isRefreshTokenRequest(config.url)) {
|
|
140
|
+
return config;
|
|
141
|
+
}
|
|
142
|
+
const token = await getToken(ACCESS_TOKEN_KEY);
|
|
143
|
+
|
|
144
|
+
// Check if token exists
|
|
145
|
+
if (!token?.token) {
|
|
146
|
+
throw new Error('No access token available');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if token is expired
|
|
150
|
+
const currentTime = Date.now();
|
|
151
|
+
const tokenExpiry = Date.parse(token.expiry);
|
|
152
|
+
const bufferTime = 30 * 1000; // 30 seconds buffer
|
|
153
|
+
|
|
154
|
+
// If token is expired or about to expire, try to refresh it first
|
|
155
|
+
if (tokenExpiry <= currentTime + bufferTime) {
|
|
156
|
+
Logger.info('Access token expired or about to expire, refreshing before request');
|
|
157
|
+
|
|
158
|
+
// Joins a refresh already in flight rather than starting a second one.
|
|
159
|
+
const newToken = await refreshAccessToken();
|
|
160
|
+
setAuthHeader(config, newToken);
|
|
161
|
+
return config;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Token is valid, proceed with request
|
|
165
|
+
setAuthHeader(config, token.token);
|
|
166
|
+
return config;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
Logger.error('Failed to attach token:', getErrorMessage(error));
|
|
169
|
+
return Promise.reject(error);
|
|
170
|
+
}
|
|
171
|
+
}, error => {
|
|
172
|
+
Logger.error('Request error:', getErrorMessage(error));
|
|
173
|
+
return Promise.reject(error);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle Response & Token Refresh
|
|
178
|
+
*/
|
|
179
|
+
userClient.interceptors.response.use(response => response, async error => {
|
|
180
|
+
if (!axios.isAxiosError(error)) {
|
|
181
|
+
Logger.error(`Non-axios error in response interceptor: ${getErrorMessage(error)}`);
|
|
182
|
+
return Promise.reject(error);
|
|
183
|
+
}
|
|
184
|
+
const axiosError = error;
|
|
185
|
+
const originalRequest = axiosError.config;
|
|
186
|
+
if (!axiosError.response) {
|
|
187
|
+
Logger.error(`Network error or no response from server: ${axiosError.message}, URL: ${originalRequest?.url}`);
|
|
188
|
+
return Promise.reject(error);
|
|
189
|
+
}
|
|
190
|
+
if (axiosError.response.status === 401 && originalRequest && !originalRequest._retry) {
|
|
191
|
+
// The refresh call goes through this client. A 401 on it must not queue behind
|
|
192
|
+
// refreshTokenPromise: that promise is the call itself, so queueing deadlocks it
|
|
193
|
+
// and every later 401 waits forever.
|
|
194
|
+
if (isRefreshTokenRequest(originalRequest.url)) {
|
|
195
|
+
return Promise.reject(error);
|
|
196
|
+
}
|
|
197
|
+
const terminationCode = getSessionTerminationCode(axiosError.response.data) ?? (hasSessionInvalidHeader(axiosError.response.headers) ? SESSION_TERMINATION_CODES.TERMINATED : undefined);
|
|
198
|
+
if (terminationCode) {
|
|
199
|
+
// The session is dead server-side — a refresh would only burn a
|
|
200
|
+
// pointless round-trip, so go straight to logout.
|
|
201
|
+
Logger.warn(`Session ${terminationCode}, logging out without refresh. URL: ${originalRequest.url}`);
|
|
202
|
+
resetRefreshState();
|
|
203
|
+
await dispatchLogout();
|
|
204
|
+
return Promise.reject(error);
|
|
205
|
+
}
|
|
206
|
+
Logger.warn(`Unauthorized URL: ${originalRequest.url}`);
|
|
207
|
+
originalRequest._retry = true;
|
|
208
|
+
|
|
209
|
+
// If we already have a refresh in progress, queue this request
|
|
210
|
+
if (isRefreshInFlight()) {
|
|
211
|
+
return queueBehindRefresh().then(token => {
|
|
212
|
+
Logger.info('Retrying request after token refresh:', originalRequest.url);
|
|
213
|
+
setAuthHeader(originalRequest, token);
|
|
214
|
+
return userClient(originalRequest);
|
|
215
|
+
}).catch(err => {
|
|
216
|
+
Logger.error('Failed to retry request after token refresh:', getErrorMessage(err));
|
|
217
|
+
return Promise.reject(err);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
// Start a new token refresh process and wait for it to complete
|
|
222
|
+
const newToken = await refreshAccessToken();
|
|
223
|
+
setAuthHeader(originalRequest, newToken);
|
|
224
|
+
return userClient(originalRequest);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return Promise.reject(err);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
Logger.error(`Response error: ${axiosError.response?.data ?? axiosError.message}, URL: ${originalRequest?.url}, Status: ${axiosError.response.status}`);
|
|
230
|
+
return Promise.reject(error);
|
|
231
|
+
});
|
|
232
|
+
export default userClient;
|
|
233
|
+
//# sourceMappingURL=userClient.js.map
|