@webority-technologies/mobile-core 0.0.1
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/README.md +15 -0
- package/lib/commonjs/api/publicClient.js +80 -0
- package/lib/commonjs/api/userClient.js +368 -0
- package/lib/commonjs/auth/authStore.js +155 -0
- package/lib/commonjs/auth/index.js +44 -0
- package/lib/commonjs/auth/jwt.js +88 -0
- package/lib/commonjs/config/index.js +65 -0
- package/lib/commonjs/formatters/currency.js +69 -0
- package/lib/commonjs/formatters/date.js +168 -0
- package/lib/commonjs/formatters/index.js +77 -0
- package/lib/commonjs/formatters/initials.js +32 -0
- package/lib/commonjs/formatters/number.js +88 -0
- package/lib/commonjs/formatters/phone.js +67 -0
- package/lib/commonjs/index.js +383 -0
- package/lib/commonjs/initSDK.js +45 -0
- package/lib/commonjs/initUserAuth.js +38 -0
- package/lib/commonjs/logger/index.js +137 -0
- package/lib/commonjs/network/index.js +25 -0
- package/lib/commonjs/network/networkStatus.js +98 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/permissions/index.js +200 -0
- package/lib/commonjs/storage/index.js +193 -0
- package/lib/commonjs/utils/imageCompression.js +75 -0
- package/lib/commonjs/utils/index.js +13 -0
- package/lib/commonjs/validators/index.js +193 -0
- package/lib/commonjs/versionCheck/index.js +395 -0
- package/lib/module/api/publicClient.js +76 -0
- package/lib/module/api/userClient.js +360 -0
- package/lib/module/auth/authStore.js +148 -0
- package/lib/module/auth/index.js +5 -0
- package/lib/module/auth/jwt.js +82 -0
- package/lib/module/config/index.js +57 -0
- package/lib/module/formatters/currency.js +64 -0
- package/lib/module/formatters/date.js +160 -0
- package/lib/module/formatters/index.js +8 -0
- package/lib/module/formatters/initials.js +27 -0
- package/lib/module/formatters/number.js +81 -0
- package/lib/module/formatters/phone.js +61 -0
- package/lib/module/index.js +30 -0
- package/lib/module/initSDK.js +40 -0
- package/lib/module/initUserAuth.js +34 -0
- package/lib/module/logger/index.js +133 -0
- package/lib/module/network/index.js +4 -0
- package/lib/module/network/networkStatus.js +91 -0
- package/lib/module/package.json +1 -0
- package/lib/module/permissions/index.js +197 -0
- package/lib/module/storage/index.js +189 -0
- package/lib/module/utils/imageCompression.js +69 -0
- package/lib/module/utils/index.js +4 -0
- package/lib/module/validators/index.js +171 -0
- package/lib/module/versionCheck/index.js +390 -0
- package/lib/typescript/commonjs/api/publicClient.d.ts +11 -0
- package/lib/typescript/commonjs/api/userClient.d.ts +49 -0
- package/lib/typescript/commonjs/auth/authStore.d.ts +43 -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 +49 -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/index.d.ts +8 -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 +13 -0
- package/lib/typescript/commonjs/index.d.ts +30 -0
- package/lib/typescript/commonjs/initSDK.d.ts +40 -0
- package/lib/typescript/commonjs/initUserAuth.d.ts +22 -0
- package/lib/typescript/commonjs/logger/index.d.ts +42 -0
- package/lib/typescript/commonjs/network/index.d.ts +3 -0
- package/lib/typescript/commonjs/network/networkStatus.d.ts +27 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/permissions/index.d.ts +20 -0
- package/lib/typescript/commonjs/storage/index.d.ts +42 -0
- package/lib/typescript/commonjs/utils/imageCompression.d.ts +31 -0
- package/lib/typescript/commonjs/utils/index.d.ts +3 -0
- package/lib/typescript/commonjs/validators/index.d.ts +46 -0
- package/lib/typescript/commonjs/versionCheck/index.d.ts +80 -0
- package/lib/typescript/module/api/publicClient.d.ts +11 -0
- package/lib/typescript/module/api/userClient.d.ts +49 -0
- package/lib/typescript/module/auth/authStore.d.ts +43 -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 +49 -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/index.d.ts +8 -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 +13 -0
- package/lib/typescript/module/index.d.ts +30 -0
- package/lib/typescript/module/initSDK.d.ts +40 -0
- package/lib/typescript/module/initUserAuth.d.ts +22 -0
- package/lib/typescript/module/logger/index.d.ts +42 -0
- package/lib/typescript/module/network/index.d.ts +3 -0
- package/lib/typescript/module/network/networkStatus.d.ts +27 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/permissions/index.d.ts +20 -0
- package/lib/typescript/module/storage/index.d.ts +42 -0
- package/lib/typescript/module/utils/imageCompression.d.ts +31 -0
- package/lib/typescript/module/utils/index.d.ts +3 -0
- package/lib/typescript/module/validators/index.d.ts +46 -0
- package/lib/typescript/module/versionCheck/index.d.ts +80 -0
- package/package.json +112 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import axios, { AxiosHeaders } from 'axios';
|
|
4
|
+
import axiosRetry from 'axios-retry';
|
|
5
|
+
import { getToken } from "../auth/authStore.js";
|
|
6
|
+
import { getConfigValue } from "../config/index.js";
|
|
7
|
+
import { Logger } from "../logger/index.js";
|
|
8
|
+
import { getNetworkStatus } from "../network/networkStatus.js";
|
|
9
|
+
|
|
10
|
+
// Define timeout for requests
|
|
11
|
+
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Thrown by the request interceptor when the device is offline, before the
|
|
15
|
+
* request ever reaches the adapter (and therefore the network). Distinct from
|
|
16
|
+
* an AxiosError so callers can show an offline message instead of a generic
|
|
17
|
+
* request failure.
|
|
18
|
+
*/
|
|
19
|
+
export class RequestOfflineError extends Error {
|
|
20
|
+
constructor(message = 'Request blocked: device is offline') {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'RequestOfflineError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sub-codes a 401 response body carries when the session itself is dead
|
|
28
|
+
* (terminated by an admin, or expired server-side) rather than merely
|
|
29
|
+
* out-of-date. These never warrant a refresh attempt — only logout.
|
|
30
|
+
*/
|
|
31
|
+
export const SESSION_TERMINATION_CODES = {
|
|
32
|
+
TERMINATED: 'SESSION_TERMINATED',
|
|
33
|
+
EXPIRED: 'SESSION_EXPIRED'
|
|
34
|
+
};
|
|
35
|
+
const TERMINATION_CODES = new Set(Object.values(SESSION_TERMINATION_CODES));
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The header is the AUTHORITATIVE signal, verified against the backend:
|
|
39
|
+
* `WeborityJwtMiddleware.ReturnUnauthorized` answers an invalidated session with
|
|
40
|
+
* `401` + `X-Security-Stamp-Invalid: true` and a body of
|
|
41
|
+
* `{ error: 'Unauthorized', message: '...' }` — no code field at all, and
|
|
42
|
+
* `error` is a string rather than an object. An earlier version of this checked
|
|
43
|
+
* only body fields and would therefore never have fired against a real server.
|
|
44
|
+
*
|
|
45
|
+
* The body codes below are kept as a secondary path for any head that does send
|
|
46
|
+
* an explicit sub-code; they match on exact strings, so looking is free.
|
|
47
|
+
*/
|
|
48
|
+
export const SESSION_INVALID_HEADER = 'x-security-stamp-invalid';
|
|
49
|
+
const hasSessionInvalidHeader = headers => {
|
|
50
|
+
if (!headers || typeof headers !== 'object') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const get = headers.get;
|
|
54
|
+
const raw = typeof get === 'function' ? get.call(headers, SESSION_INVALID_HEADER) : headers[SESSION_INVALID_HEADER];
|
|
55
|
+
return String(raw).toLowerCase() === 'true';
|
|
56
|
+
};
|
|
57
|
+
const getSessionTerminationCode = data => {
|
|
58
|
+
if (!data || typeof data !== 'object') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const body = data;
|
|
62
|
+
const nested = body.error;
|
|
63
|
+
const candidates = [body.code, body.errorCode, nested && typeof nested === 'object' ? nested.code : undefined];
|
|
64
|
+
for (const candidate of candidates) {
|
|
65
|
+
if (typeof candidate === 'string' && TERMINATION_CODES.has(candidate)) {
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Minimal Redux-store shape required by the user client to dispatch
|
|
74
|
+
* refresh-token / logout actions.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Internal extension of axios request config to track retry attempts.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
const userClient = axios.create({
|
|
82
|
+
baseURL: getConfigValue('baseUrl') ?? undefined,
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
86
|
+
},
|
|
87
|
+
timeout: REQUEST_TIMEOUT // Set timeout for all requests
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Configure axios-retry with modified exponential backoff
|
|
91
|
+
axiosRetry(userClient, {
|
|
92
|
+
retries: 3,
|
|
93
|
+
retryDelay: retryCount => retryCount === 0 ? 0 : Math.pow(2, retryCount) * 1000,
|
|
94
|
+
// Immediate retry for the first attempt
|
|
95
|
+
retryCondition: error => {
|
|
96
|
+
// Status-based ONLY — see publicClient for the reasoning. A bare network
|
|
97
|
+
// error carries no response, so it is not retried: the write may already
|
|
98
|
+
// have landed server-side, and this client issues non-idempotent requests.
|
|
99
|
+
const status = error.response?.status;
|
|
100
|
+
return typeof status === 'number' && (status === 429 || status >= 500 && status <= 599);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
let store;
|
|
104
|
+
let authActions = {
|
|
105
|
+
logout: () => undefined,
|
|
106
|
+
refreshToken: () => undefined
|
|
107
|
+
};
|
|
108
|
+
export const injectStore = _store => {
|
|
109
|
+
store = _store;
|
|
110
|
+
};
|
|
111
|
+
export const injectAuthActions = actions => {
|
|
112
|
+
authActions = {
|
|
113
|
+
...authActions,
|
|
114
|
+
...actions
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Single promise to track token refresh
|
|
119
|
+
let refreshTokenPromise = null;
|
|
120
|
+
|
|
121
|
+
// Queue for failed requests
|
|
122
|
+
let failedQueue = [];
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Processes the queue of failed requests.
|
|
126
|
+
*/
|
|
127
|
+
const processQueue = (error, token = null) => {
|
|
128
|
+
failedQueue.forEach(({
|
|
129
|
+
resolve,
|
|
130
|
+
reject
|
|
131
|
+
}) => {
|
|
132
|
+
if (error) {
|
|
133
|
+
reject(error);
|
|
134
|
+
} else if (token !== null) {
|
|
135
|
+
resolve(token);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
failedQueue = [];
|
|
139
|
+
};
|
|
140
|
+
const getErrorMessage = error => {
|
|
141
|
+
if (error instanceof Error) {
|
|
142
|
+
return error.message;
|
|
143
|
+
}
|
|
144
|
+
if (typeof error === 'string') {
|
|
145
|
+
return error;
|
|
146
|
+
}
|
|
147
|
+
return 'Unknown error';
|
|
148
|
+
};
|
|
149
|
+
const setAuthHeader = (config, token) => {
|
|
150
|
+
if (config.headers instanceof AxiosHeaders) {
|
|
151
|
+
config.headers.set('Authorization', `Bearer ${token}`);
|
|
152
|
+
} else {
|
|
153
|
+
// Fallback in case headers were provided as a plain object.
|
|
154
|
+
config.headers = AxiosHeaders.from({
|
|
155
|
+
...config.headers,
|
|
156
|
+
Authorization: `Bearer ${token}`
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const requireStore = () => {
|
|
161
|
+
if (!store) {
|
|
162
|
+
throw new Error('User package store is not initialized. Call initUserAuth first.');
|
|
163
|
+
}
|
|
164
|
+
return store;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Attach Authorization Token Before Requests
|
|
169
|
+
*/
|
|
170
|
+
userClient.interceptors.request.use(async config => {
|
|
171
|
+
// Reject before any network call when the device is known to be offline.
|
|
172
|
+
// Unknown connectivity (netinfo not installed, or undetermined) is treated
|
|
173
|
+
// as online — never block a request on an unavailable probe.
|
|
174
|
+
const network = await getNetworkStatus();
|
|
175
|
+
if (network.isConnected === false) {
|
|
176
|
+
Logger.warn(`Request blocked: device is offline. URL: ${config.url}`);
|
|
177
|
+
return Promise.reject(new RequestOfflineError());
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
// Skip token check for refresh token endpoint to prevent infinite loop
|
|
181
|
+
if (config.url?.includes('/Account/RefreshToken')) {
|
|
182
|
+
return config;
|
|
183
|
+
}
|
|
184
|
+
const token = await getToken('accessToken');
|
|
185
|
+
|
|
186
|
+
// Check if token exists
|
|
187
|
+
if (!token?.token) {
|
|
188
|
+
throw new Error('No access token available');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if token is expired
|
|
192
|
+
const currentTime = Date.now();
|
|
193
|
+
const tokenExpiry = Date.parse(token.expiry);
|
|
194
|
+
const bufferTime = 30 * 1000; // 30 seconds buffer
|
|
195
|
+
|
|
196
|
+
// If token is expired or about to expire, try to refresh it first
|
|
197
|
+
if (tokenExpiry <= currentTime + bufferTime) {
|
|
198
|
+
Logger.info('Access token expired or about to expire, refreshing before request');
|
|
199
|
+
|
|
200
|
+
// If we already have a refresh in progress, wait for it
|
|
201
|
+
if (refreshTokenPromise) {
|
|
202
|
+
Logger.info('Token refresh already in progress, waiting for completion');
|
|
203
|
+
try {
|
|
204
|
+
const newToken = await refreshTokenPromise;
|
|
205
|
+
setAuthHeader(config, newToken);
|
|
206
|
+
return config;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
Logger.error('Failed to use refreshed token:', getErrorMessage(err));
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Start a new token refresh process
|
|
214
|
+
refreshTokenPromise = (async () => {
|
|
215
|
+
try {
|
|
216
|
+
const refreshToken = await getToken('refreshToken');
|
|
217
|
+
if (!refreshToken?.token) {
|
|
218
|
+
Logger.error('No refresh token available');
|
|
219
|
+
throw new Error('No refresh token available');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check if refresh token is expired
|
|
223
|
+
const refreshTokenExpiry = Date.parse(refreshToken.expiry);
|
|
224
|
+
if (refreshTokenExpiry <= currentTime) {
|
|
225
|
+
Logger.error('Refresh token expired');
|
|
226
|
+
throw new Error('Refresh token expired');
|
|
227
|
+
}
|
|
228
|
+
Logger.info('Dispatching refresh token action');
|
|
229
|
+
const activeStore = requireStore();
|
|
230
|
+
const refreshResult = await activeStore.dispatch(authActions.refreshToken());
|
|
231
|
+
if (refreshResult.error) {
|
|
232
|
+
throw new Error(`Token refresh failed: ${refreshResult.error.message || 'Unknown error'}`);
|
|
233
|
+
}
|
|
234
|
+
const newAccessToken = await getToken('accessToken');
|
|
235
|
+
if (!newAccessToken?.token) {
|
|
236
|
+
throw new Error('Failed to retrieve new access token after refresh');
|
|
237
|
+
}
|
|
238
|
+
Logger.info('Token refreshed successfully before request');
|
|
239
|
+
return newAccessToken.token;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
Logger.error('Token refresh failed before request:', getErrorMessage(error));
|
|
242
|
+
await requireStore().dispatch(authActions.logout());
|
|
243
|
+
throw error;
|
|
244
|
+
} finally {
|
|
245
|
+
refreshTokenPromise = null;
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
try {
|
|
249
|
+
// Wait for the refresh to complete
|
|
250
|
+
const newToken = await refreshTokenPromise;
|
|
251
|
+
setAuthHeader(config, newToken);
|
|
252
|
+
return config;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return Promise.reject(err);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Token is valid, proceed with request
|
|
259
|
+
setAuthHeader(config, token.token);
|
|
260
|
+
return config;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
Logger.error('Failed to attach token:', getErrorMessage(error));
|
|
263
|
+
return Promise.reject(error);
|
|
264
|
+
}
|
|
265
|
+
}, error => {
|
|
266
|
+
Logger.error('Request error:', getErrorMessage(error));
|
|
267
|
+
return Promise.reject(error);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Handle Response & Token Refresh
|
|
272
|
+
*/
|
|
273
|
+
userClient.interceptors.response.use(response => response, async error => {
|
|
274
|
+
if (!axios.isAxiosError(error)) {
|
|
275
|
+
Logger.error(`Non-axios error in response interceptor: ${getErrorMessage(error)}`);
|
|
276
|
+
return Promise.reject(error);
|
|
277
|
+
}
|
|
278
|
+
const axiosError = error;
|
|
279
|
+
const originalRequest = axiosError.config;
|
|
280
|
+
if (!axiosError.response) {
|
|
281
|
+
Logger.error(`Network error or no response from server: ${axiosError.message}, URL: ${originalRequest?.url}`);
|
|
282
|
+
return Promise.reject(error);
|
|
283
|
+
}
|
|
284
|
+
if (axiosError.response.status === 401 && originalRequest && !originalRequest._retry) {
|
|
285
|
+
const terminationCode = getSessionTerminationCode(axiosError.response.data) ?? (hasSessionInvalidHeader(axiosError.response.headers) ? SESSION_TERMINATION_CODES.TERMINATED : undefined);
|
|
286
|
+
if (terminationCode) {
|
|
287
|
+
// The session is dead server-side — a refresh would only burn a
|
|
288
|
+
// pointless round-trip, so go straight to logout.
|
|
289
|
+
Logger.warn(`Session ${terminationCode}, logging out without refresh. URL: ${originalRequest.url}`);
|
|
290
|
+
await requireStore().dispatch(authActions.logout());
|
|
291
|
+
return Promise.reject(error);
|
|
292
|
+
}
|
|
293
|
+
Logger.warn(`Unauthorized URL: ${originalRequest.url}`);
|
|
294
|
+
originalRequest._retry = true;
|
|
295
|
+
|
|
296
|
+
// If we already have a refresh in progress, queue this request
|
|
297
|
+
if (refreshTokenPromise) {
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
failedQueue.push({
|
|
300
|
+
resolve,
|
|
301
|
+
reject
|
|
302
|
+
});
|
|
303
|
+
}).then(token => {
|
|
304
|
+
Logger.info('Retrying request after token refresh:', originalRequest.url);
|
|
305
|
+
setAuthHeader(originalRequest, token);
|
|
306
|
+
return userClient(originalRequest);
|
|
307
|
+
}).catch(err => {
|
|
308
|
+
Logger.error('Failed to retry request after token refresh:', getErrorMessage(err));
|
|
309
|
+
return Promise.reject(err);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Start a new token refresh process and track it
|
|
314
|
+
refreshTokenPromise = (async () => {
|
|
315
|
+
try {
|
|
316
|
+
Logger.info('User Client: Refreshing token...');
|
|
317
|
+
const refreshToken = await getToken('refreshToken');
|
|
318
|
+
if (!refreshToken || !refreshToken.token) {
|
|
319
|
+
Logger.error('No refresh token available or token is invalid');
|
|
320
|
+
throw new Error('No refresh token available');
|
|
321
|
+
}
|
|
322
|
+
Logger.info('Dispatching refresh token action');
|
|
323
|
+
const activeStore = requireStore();
|
|
324
|
+
const refreshResult = await activeStore.dispatch(authActions.refreshToken());
|
|
325
|
+
if (refreshResult.error) {
|
|
326
|
+
Logger.error('Token refresh action failed:', refreshResult.error);
|
|
327
|
+
throw new Error(`Token refresh failed: ${refreshResult.error.message || 'Unknown error'}`);
|
|
328
|
+
}
|
|
329
|
+
Logger.info('Getting new access token after refresh');
|
|
330
|
+
const newAccessToken = await getToken('accessToken');
|
|
331
|
+
if (!newAccessToken?.token) {
|
|
332
|
+
throw new Error('Failed to retrieve new access token');
|
|
333
|
+
}
|
|
334
|
+
Logger.info('Token refreshed successfully');
|
|
335
|
+
processQueue(null, newAccessToken.token);
|
|
336
|
+
return newAccessToken.token;
|
|
337
|
+
} catch (refreshError) {
|
|
338
|
+
const responseData = axios.isAxiosError(refreshError) ? refreshError.response?.data : undefined;
|
|
339
|
+
Logger.error('Token refresh failed:', responseData ?? getErrorMessage(refreshError));
|
|
340
|
+
processQueue(refreshError, null);
|
|
341
|
+
await requireStore().dispatch(authActions.logout());
|
|
342
|
+
throw refreshError;
|
|
343
|
+
} finally {
|
|
344
|
+
refreshTokenPromise = null;
|
|
345
|
+
}
|
|
346
|
+
})();
|
|
347
|
+
try {
|
|
348
|
+
// Wait for the refresh token process to complete
|
|
349
|
+
const newToken = await refreshTokenPromise;
|
|
350
|
+
setAuthHeader(originalRequest, newToken);
|
|
351
|
+
return userClient(originalRequest);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
return Promise.reject(err);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
Logger.error(`Response error: ${axiosError.response?.data ?? axiosError.message}, URL: ${originalRequest?.url}, Status: ${axiosError.response.status}`);
|
|
357
|
+
return Promise.reject(error);
|
|
358
|
+
});
|
|
359
|
+
export default userClient;
|
|
360
|
+
//# sourceMappingURL=userClient.js.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Logger } from "../logger/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Token data persisted in the keychain.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Minimal keychain shape we depend on.
|
|
11
|
+
* Lets us avoid a hard import on `react-native-keychain`, so an app that never
|
|
12
|
+
* stores a token does not have to install it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
let backend = null;
|
|
16
|
+
let resolved = false;
|
|
17
|
+
const resolveBackend = () => {
|
|
18
|
+
if (resolved) {
|
|
19
|
+
return backend;
|
|
20
|
+
}
|
|
21
|
+
resolved = true;
|
|
22
|
+
try {
|
|
23
|
+
const mod = require('react-native-keychain');
|
|
24
|
+
// Prefer named exports: the real module and the test mock both expose them
|
|
25
|
+
// flat, while `default` is a differently-shaped convenience object.
|
|
26
|
+
backend = typeof mod?.setInternetCredentials === 'function' ? mod : mod?.default;
|
|
27
|
+
} catch {
|
|
28
|
+
backend = null;
|
|
29
|
+
}
|
|
30
|
+
return backend;
|
|
31
|
+
};
|
|
32
|
+
const requireBackend = op => {
|
|
33
|
+
const b = resolveBackend();
|
|
34
|
+
if (!b) {
|
|
35
|
+
const err = new Error(`[authStore] react-native-keychain is not installed; cannot ${op}. ` + 'Install the peer dep or call setKeychainImplementation() with a custom backend.');
|
|
36
|
+
Logger.error(err.message);
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
return b;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Inject a custom keychain backend at runtime.
|
|
44
|
+
* Pass `null` to reset to the default react-native-keychain backend.
|
|
45
|
+
*/
|
|
46
|
+
export const setKeychainImplementation = impl => {
|
|
47
|
+
backend = impl;
|
|
48
|
+
resolved = impl !== null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Store token with expiry.
|
|
53
|
+
*/
|
|
54
|
+
export const storeToken = async (key, token, expiry) => {
|
|
55
|
+
// Never log the token or expiry values: Logger forwards to whatever remote
|
|
56
|
+
// sink the app wired (Crashlytics/Sentry), so a token here leaves the device.
|
|
57
|
+
Logger.info(`Storing token for key: ${key}`);
|
|
58
|
+
try {
|
|
59
|
+
const data = JSON.stringify({
|
|
60
|
+
token,
|
|
61
|
+
expiry
|
|
62
|
+
});
|
|
63
|
+
await requireBackend('store a token').setInternetCredentials(key, 'token', data, {
|
|
64
|
+
service: key
|
|
65
|
+
}); // Ensure service matches
|
|
66
|
+
Logger.info(`Token stored successfully for key: ${key}`);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Log AND rethrow. Swallowing this told the caller the token was persisted
|
|
69
|
+
// when it was not, so an app would show a signed-in user holding a token
|
|
70
|
+
// that never reached the keychain — the session then dies at the next
|
|
71
|
+
// cold start with no failure anyone could have seen.
|
|
72
|
+
Logger.error(`Error storing token for key: ${key}`, error);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse token data from credentials.
|
|
79
|
+
*/
|
|
80
|
+
const parseTokenData = credentials => {
|
|
81
|
+
if (!credentials) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const {
|
|
85
|
+
password
|
|
86
|
+
} = credentials;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(password);
|
|
89
|
+
if (parsed && typeof parsed === 'object' && 'token' in parsed && 'expiry' in parsed && typeof parsed.token === 'string' && typeof parsed.expiry === 'string') {
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
Logger.error('Failed to parse token data', error);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Retrieve token and check if expired.
|
|
101
|
+
*/
|
|
102
|
+
export const getToken = async key => {
|
|
103
|
+
Logger.info(`Retrieving token for key: ${key}`);
|
|
104
|
+
try {
|
|
105
|
+
const credentials = await requireBackend('read a token').getInternetCredentials(key, {
|
|
106
|
+
service: key
|
|
107
|
+
}); // Ensure service matches
|
|
108
|
+
const parsedData = parseTokenData(credentials);
|
|
109
|
+
Logger.info(`Parsed token data for key: ${key}: ${parsedData ? 'present' : 'absent'}`);
|
|
110
|
+
if (!parsedData) {
|
|
111
|
+
Logger.warn(`Invalid or no token data found for key: ${key}`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const {
|
|
115
|
+
token,
|
|
116
|
+
expiry
|
|
117
|
+
} = parsedData;
|
|
118
|
+
Logger.info(`Token retrieved for key: ${key}`);
|
|
119
|
+
return {
|
|
120
|
+
token,
|
|
121
|
+
expiry
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
125
|
+
if (message.includes('Key permanently invalidated')) {
|
|
126
|
+
Logger.error(`Key permanently invalidated for key: ${key}. Clearing the invalidated key.`);
|
|
127
|
+
await removeToken(key); // Clear the invalidated key
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
Logger.error(`Error retrieving token for key: ${key}`, error);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove token by key.
|
|
137
|
+
*/
|
|
138
|
+
export const removeToken = async key => {
|
|
139
|
+
try {
|
|
140
|
+
await requireBackend('remove a token').resetInternetCredentials({
|
|
141
|
+
service: key
|
|
142
|
+
});
|
|
143
|
+
Logger.info(`Token removed successfully for key: ${key}`);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
Logger.error(`Failed to remove token for key: ${key}`, error);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
//# sourceMappingURL=authStore.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Logger } from "../logger/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Standard claims plus arbitrary keys.
|
|
7
|
+
* `sub`, `exp`, `iat`, `nbf` follow RFC 7519 conventions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const base64UrlToBase64 = input => {
|
|
11
|
+
const padded = input + '='.repeat((4 - input.length % 4) % 4);
|
|
12
|
+
return padded.replace(/-/g, '+').replace(/_/g, '/');
|
|
13
|
+
};
|
|
14
|
+
const decodeBase64 = input => {
|
|
15
|
+
// RN runtimes expose `atob` (Hermes 0.71+); fall back to a Buffer-based decode otherwise.
|
|
16
|
+
if (typeof atob === 'function') {
|
|
17
|
+
return atob(input);
|
|
18
|
+
}
|
|
19
|
+
const {
|
|
20
|
+
Buffer
|
|
21
|
+
} = require('buffer');
|
|
22
|
+
return Buffer.from(input, 'base64').toString('utf-8');
|
|
23
|
+
};
|
|
24
|
+
const utf8FromBinary = binary => {
|
|
25
|
+
// Convert percent-encoded UTF-8 sequence so multi-byte chars survive.
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(binary.split('').map(c => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')).join(''));
|
|
28
|
+
} catch {
|
|
29
|
+
return binary;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decode a JWT's payload claims. Does **not** verify the signature.
|
|
35
|
+
*
|
|
36
|
+
* @returns claims object on success, `null` if the token is malformed.
|
|
37
|
+
*/
|
|
38
|
+
export const decodeJWT = token => {
|
|
39
|
+
if (!token || typeof token !== 'string') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const parts = token.split('.');
|
|
43
|
+
if (parts.length !== 3) {
|
|
44
|
+
Logger.warn('[jwt] decodeJWT: token is not a valid JWT (expected 3 segments)');
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const payload = parts[1];
|
|
49
|
+
const json = utf8FromBinary(decodeBase64(base64UrlToBase64(payload)));
|
|
50
|
+
return JSON.parse(json);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
Logger.warn('[jwt] decodeJWT failed to parse payload', error);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns true when the token is expired (or unparseable / missing `exp`).
|
|
59
|
+
*
|
|
60
|
+
* @param token JWT string
|
|
61
|
+
* @param leewaySeconds amount of clock-skew tolerance to apply (default 30s)
|
|
62
|
+
*/
|
|
63
|
+
export const isJWTExpired = (token, leewaySeconds = 30) => {
|
|
64
|
+
const claims = decodeJWT(token);
|
|
65
|
+
if (!claims || typeof claims.exp !== 'number') {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
69
|
+
return claims.exp <= nowSeconds + leewaySeconds;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the token's expiry as a Date, or `null` when no `exp` claim exists.
|
|
74
|
+
*/
|
|
75
|
+
export const getJWTExpiry = token => {
|
|
76
|
+
const claims = decodeJWT(token);
|
|
77
|
+
if (!claims || typeof claims.exp !== 'number') {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return new Date(claims.exp * 1000);
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=jwt.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration management for Webority packages
|
|
5
|
+
* Provides a flexible way to inject configuration without hard dependencies
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
baseUrl: null,
|
|
10
|
+
loggingEnabled: true,
|
|
11
|
+
logLevel: 'info',
|
|
12
|
+
environment: 'Development',
|
|
13
|
+
country: 'in',
|
|
14
|
+
appId: null,
|
|
15
|
+
appVersion: null
|
|
16
|
+
};
|
|
17
|
+
let globalConfig = {
|
|
18
|
+
...DEFAULT_CONFIG
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Set global configuration for all Webority packages
|
|
23
|
+
*/
|
|
24
|
+
export const setConfig = config => {
|
|
25
|
+
globalConfig = {
|
|
26
|
+
...globalConfig,
|
|
27
|
+
...config
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get current configuration
|
|
33
|
+
*/
|
|
34
|
+
export const getConfig = () => globalConfig;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get a specific config value with optional fallback.
|
|
38
|
+
* If the stored value is `null`, the fallback is returned (when provided).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
export function getConfigValue(key, fallback) {
|
|
42
|
+
const value = globalConfig[key];
|
|
43
|
+
if (value !== null) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reset configuration to defaults
|
|
51
|
+
*/
|
|
52
|
+
export const resetConfig = () => {
|
|
53
|
+
globalConfig = {
|
|
54
|
+
...DEFAULT_CONFIG
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=index.js.map
|