@webority/mobile-core 0.0.29 → 0.2.0
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 +32 -0
- package/lib/commonjs/api/baseUrl.js +1 -34
- package/lib/commonjs/api/clientHeaders.js +1 -0
- package/lib/commonjs/api/curl.js +1 -109
- package/lib/commonjs/api/minAppVersion.js +1 -0
- package/lib/commonjs/api/multipart.js +1 -21
- package/lib/commonjs/api/publicClient.js +1 -79
- package/lib/commonjs/api/requestDefaults.js +1 -0
- package/lib/commonjs/api/responseBodyText.js +1 -0
- package/lib/commonjs/api/retry.js +1 -91
- package/lib/commonjs/api/tokenRefresh.js +1 -205
- package/lib/commonjs/api/userClient.js +1 -265
- package/lib/commonjs/auth/authStore.js +1 -265
- package/lib/commonjs/auth/index.js +1 -44
- package/lib/commonjs/auth/jwt.js +1 -112
- package/lib/commonjs/config/index.js +1 -72
- package/lib/commonjs/deepLink/index.js +1 -286
- package/lib/commonjs/download/adapters/expoFs.js +1 -69
- package/lib/commonjs/download/adapters/shared.js +1 -13
- package/lib/commonjs/download/index.js +1 -250
- package/lib/commonjs/errors/extractApiError.js +1 -0
- package/lib/commonjs/errors/index.js +1 -0
- package/lib/commonjs/formatters/currency.js +1 -70
- package/lib/commonjs/formatters/date.js +1 -167
- package/lib/commonjs/formatters/dateMath.js +1 -311
- package/lib/commonjs/formatters/formatConfig.js +1 -0
- package/lib/commonjs/formatters/house.js +1 -0
- package/lib/commonjs/formatters/index.js +1 -246
- package/lib/commonjs/formatters/initials.js +1 -32
- package/lib/commonjs/formatters/intlCache.js +1 -37
- package/lib/commonjs/formatters/number.js +1 -89
- package/lib/commonjs/formatters/phone.js +1 -369
- package/lib/commonjs/formatters/windowsToIanaTable.js +1 -164
- package/lib/commonjs/geolocation/adapters/expoLocation.js +1 -126
- package/lib/commonjs/geolocation/index.js +1 -355
- package/lib/commonjs/index.js +1 -644
- package/lib/commonjs/initSDK.js +1 -66
- package/lib/commonjs/initUserAuth.js +1 -37
- package/lib/commonjs/logger/crashlytics.js +1 -0
- package/lib/commonjs/logger/index.js +1 -301
- package/lib/commonjs/logger/redact.js +1 -50
- package/lib/commonjs/network/index.js +1 -31
- package/lib/commonjs/network/networkStatus.js +1 -171
- package/lib/commonjs/permissions/calendar.js +1 -14
- package/lib/commonjs/permissions/camera.js +1 -14
- package/lib/commonjs/permissions/contacts.js +1 -10
- package/lib/commonjs/permissions/index.js +1 -133
- package/lib/commonjs/permissions/location.js +1 -14
- package/lib/commonjs/permissions/mediaLibrary.js +1 -10
- package/lib/commonjs/permissions/notifications.js +1 -10
- package/lib/commonjs/permissions/photos.js +1 -14
- package/lib/commonjs/phone/countries.js +1 -265
- package/lib/commonjs/push/index.js +1 -61
- package/lib/commonjs/push/push.js +1 -468
- package/lib/commonjs/push/status.js +1 -0
- package/lib/commonjs/push/task.js +1 -43
- package/lib/commonjs/query/apiBaseQuery.js +1 -0
- package/lib/commonjs/query/createAppApi.js +1 -0
- package/lib/commonjs/query/index.js +1 -0
- package/lib/commonjs/query/listeners.js +1 -0
- package/lib/commonjs/query/paging.js +1 -0
- package/lib/commonjs/query/tags.js +1 -0
- package/lib/commonjs/redux/errorToastMiddleware.js +1 -0
- package/lib/commonjs/redux/index.js +1 -0
- package/lib/commonjs/redux/rootReducer.js +1 -0
- package/lib/commonjs/redux/serializableCheck.js +1 -0
- package/lib/commonjs/secureScreen/index.js +1 -0
- package/lib/commonjs/share/index.js +1 -70
- package/lib/commonjs/smsRetriever/index.js +1 -91
- package/lib/commonjs/specs/NativeSmsRetriever.js +1 -15
- package/lib/commonjs/sqlite/adapters/expo.js +1 -53
- package/lib/commonjs/sqlite/index.js +1 -604
- package/lib/commonjs/storage/index.js +1 -214
- package/lib/commonjs/types/axios.js +1 -4
- package/lib/commonjs/types/globals.d.js +1 -6
- package/lib/commonjs/utils/base64.js +1 -24
- package/lib/commonjs/utils/imageCompression.js +1 -389
- package/lib/commonjs/utils/index.js +1 -19
- package/lib/commonjs/validators/index.js +1 -227
- package/lib/commonjs/versionCheck/buildNumber.js +1 -0
- package/lib/commonjs/versionCheck/index.js +1 -419
- package/lib/module/api/baseUrl.js +1 -30
- package/lib/module/api/clientHeaders.js +1 -0
- package/lib/module/api/curl.js +1 -104
- package/lib/module/api/minAppVersion.js +1 -0
- package/lib/module/api/multipart.js +1 -16
- package/lib/module/api/publicClient.js +1 -75
- package/lib/module/api/requestDefaults.js +1 -0
- package/lib/module/api/responseBodyText.js +1 -0
- package/lib/module/api/retry.js +1 -85
- package/lib/module/api/tokenRefresh.js +1 -192
- package/lib/module/api/userClient.js +1 -243
- package/lib/module/auth/authStore.js +1 -257
- package/lib/module/auth/index.js +1 -5
- package/lib/module/auth/jwt.js +1 -106
- package/lib/module/config/index.js +1 -64
- package/lib/module/deepLink/index.js +1 -280
- package/lib/module/download/adapters/expoFs.js +1 -64
- package/lib/module/download/adapters/shared.js +1 -8
- package/lib/module/download/index.js +1 -242
- package/lib/module/errors/extractApiError.js +1 -0
- package/lib/module/errors/index.js +1 -0
- package/lib/module/formatters/currency.js +1 -65
- package/lib/module/formatters/date.js +1 -159
- package/lib/module/formatters/dateMath.js +1 -295
- package/lib/module/formatters/formatConfig.js +1 -0
- package/lib/module/formatters/house.js +1 -0
- package/lib/module/formatters/index.js +1 -9
- package/lib/module/formatters/initials.js +1 -27
- package/lib/module/formatters/intlCache.js +1 -30
- package/lib/module/formatters/number.js +1 -82
- package/lib/module/formatters/phone.js +1 -302
- package/lib/module/formatters/windowsToIanaTable.js +1 -160
- package/lib/module/geolocation/adapters/expoLocation.js +1 -121
- package/lib/module/geolocation/index.js +1 -347
- package/lib/module/index.js +1 -57
- package/lib/module/initSDK.js +1 -61
- package/lib/module/initUserAuth.js +1 -33
- package/lib/module/logger/crashlytics.js +1 -0
- package/lib/module/logger/index.js +1 -295
- package/lib/module/logger/redact.js +1 -44
- package/lib/module/network/index.js +1 -4
- package/lib/module/network/networkStatus.js +1 -161
- package/lib/module/permissions/calendar.js +1 -15
- package/lib/module/permissions/camera.js +1 -15
- package/lib/module/permissions/contacts.js +1 -11
- package/lib/module/permissions/index.js +1 -128
- package/lib/module/permissions/location.js +1 -15
- package/lib/module/permissions/mediaLibrary.js +1 -11
- package/lib/module/permissions/notifications.js +1 -11
- package/lib/module/permissions/photos.js +1 -15
- package/lib/module/phone/countries.js +1 -261
- package/lib/module/push/index.js +1 -4
- package/lib/module/push/push.js +1 -452
- package/lib/module/push/status.js +1 -0
- package/lib/module/push/task.js +1 -38
- package/lib/module/query/apiBaseQuery.js +1 -0
- package/lib/module/query/createAppApi.js +1 -0
- package/lib/module/query/index.js +1 -0
- package/lib/module/query/listeners.js +1 -0
- package/lib/module/query/paging.js +1 -0
- package/lib/module/query/tags.js +1 -0
- package/lib/module/redux/errorToastMiddleware.js +1 -0
- package/lib/module/redux/index.js +1 -0
- package/lib/module/redux/rootReducer.js +1 -0
- package/lib/module/redux/serializableCheck.js +1 -0
- package/lib/module/secureScreen/index.js +1 -0
- package/lib/module/share/index.js +1 -64
- package/lib/module/smsRetriever/index.js +1 -86
- package/lib/module/specs/NativeSmsRetriever.js +1 -13
- package/lib/module/sqlite/adapters/expo.js +1 -48
- package/lib/module/sqlite/index.js +1 -594
- package/lib/module/storage/index.js +1 -208
- package/lib/module/types/axios.js +1 -4
- package/lib/module/types/globals.d.js +1 -10
- package/lib/module/utils/base64.js +1 -19
- package/lib/module/utils/imageCompression.js +1 -383
- package/lib/module/utils/index.js +1 -4
- package/lib/module/validators/index.js +1 -205
- package/lib/module/versionCheck/buildNumber.js +1 -0
- package/lib/module/versionCheck/index.js +1 -413
- package/lib/typescript/commonjs/api/clientHeaders.d.ts +25 -0
- package/lib/typescript/commonjs/api/minAppVersion.d.ts +24 -0
- package/lib/typescript/commonjs/api/requestDefaults.d.ts +21 -0
- package/lib/typescript/commonjs/api/responseBodyText.d.ts +6 -0
- package/lib/typescript/commonjs/api/retry.d.ts +23 -9
- package/lib/typescript/commonjs/api/tokenRefresh.d.ts +14 -2
- package/lib/typescript/commonjs/api/userClient.d.ts +1 -1
- package/lib/typescript/commonjs/auth/authStore.d.ts +76 -0
- package/lib/typescript/commonjs/auth/index.d.ts +2 -2
- package/lib/typescript/commonjs/config/index.d.ts +31 -2
- package/lib/typescript/commonjs/errors/extractApiError.d.ts +69 -0
- package/lib/typescript/commonjs/errors/index.d.ts +3 -0
- package/lib/typescript/commonjs/formatters/currency.d.ts +22 -7
- package/lib/typescript/commonjs/formatters/date.d.ts +13 -13
- package/lib/typescript/commonjs/formatters/dateMath.d.ts +4 -2
- package/lib/typescript/commonjs/formatters/formatConfig.d.ts +48 -0
- package/lib/typescript/commonjs/formatters/house.d.ts +143 -0
- package/lib/typescript/commonjs/formatters/index.d.ts +4 -0
- package/lib/typescript/commonjs/formatters/number.d.ts +15 -5
- package/lib/typescript/commonjs/index.d.ts +26 -8
- package/lib/typescript/commonjs/initSDK.d.ts +18 -6
- package/lib/typescript/commonjs/initUserAuth.d.ts +29 -0
- package/lib/typescript/commonjs/logger/crashlytics.d.ts +31 -0
- package/lib/typescript/commonjs/logger/index.d.ts +17 -0
- package/lib/typescript/commonjs/push/index.d.ts +3 -1
- package/lib/typescript/commonjs/push/push.d.ts +1 -1
- package/lib/typescript/commonjs/push/status.d.ts +9 -0
- package/lib/typescript/commonjs/query/apiBaseQuery.d.ts +55 -0
- package/lib/typescript/commonjs/query/createAppApi.d.ts +21 -0
- package/lib/typescript/commonjs/query/index.d.ts +9 -0
- package/lib/typescript/commonjs/query/listeners.d.ts +11 -0
- package/lib/typescript/commonjs/query/paging.d.ts +42 -0
- package/lib/typescript/commonjs/query/tags.d.ts +31 -0
- package/lib/typescript/commonjs/redux/errorToastMiddleware.d.ts +51 -0
- package/lib/typescript/commonjs/redux/index.d.ts +6 -0
- package/lib/typescript/commonjs/redux/rootReducer.d.ts +19 -0
- package/lib/typescript/commonjs/redux/serializableCheck.d.ts +14 -0
- package/lib/typescript/commonjs/secureScreen/index.d.ts +25 -0
- package/lib/typescript/commonjs/versionCheck/buildNumber.d.ts +14 -0
- package/lib/typescript/commonjs/versionCheck/index.d.ts +28 -17
- package/lib/typescript/module/api/clientHeaders.d.ts +25 -0
- package/lib/typescript/module/api/minAppVersion.d.ts +24 -0
- package/lib/typescript/module/api/requestDefaults.d.ts +21 -0
- package/lib/typescript/module/api/responseBodyText.d.ts +6 -0
- package/lib/typescript/module/api/retry.d.ts +23 -9
- package/lib/typescript/module/api/tokenRefresh.d.ts +14 -2
- package/lib/typescript/module/api/userClient.d.ts +1 -1
- package/lib/typescript/module/auth/authStore.d.ts +76 -0
- package/lib/typescript/module/auth/index.d.ts +2 -2
- package/lib/typescript/module/config/index.d.ts +31 -2
- package/lib/typescript/module/errors/extractApiError.d.ts +69 -0
- package/lib/typescript/module/errors/index.d.ts +3 -0
- package/lib/typescript/module/formatters/currency.d.ts +22 -7
- package/lib/typescript/module/formatters/date.d.ts +13 -13
- package/lib/typescript/module/formatters/dateMath.d.ts +4 -2
- package/lib/typescript/module/formatters/formatConfig.d.ts +48 -0
- package/lib/typescript/module/formatters/house.d.ts +143 -0
- package/lib/typescript/module/formatters/index.d.ts +4 -0
- package/lib/typescript/module/formatters/number.d.ts +15 -5
- package/lib/typescript/module/index.d.ts +26 -8
- package/lib/typescript/module/initSDK.d.ts +18 -6
- package/lib/typescript/module/initUserAuth.d.ts +29 -0
- package/lib/typescript/module/logger/crashlytics.d.ts +31 -0
- package/lib/typescript/module/logger/index.d.ts +17 -0
- package/lib/typescript/module/push/index.d.ts +3 -1
- package/lib/typescript/module/push/push.d.ts +1 -1
- package/lib/typescript/module/push/status.d.ts +9 -0
- package/lib/typescript/module/query/apiBaseQuery.d.ts +55 -0
- package/lib/typescript/module/query/createAppApi.d.ts +21 -0
- package/lib/typescript/module/query/index.d.ts +9 -0
- package/lib/typescript/module/query/listeners.d.ts +11 -0
- package/lib/typescript/module/query/paging.d.ts +42 -0
- package/lib/typescript/module/query/tags.d.ts +31 -0
- package/lib/typescript/module/redux/errorToastMiddleware.d.ts +51 -0
- package/lib/typescript/module/redux/index.d.ts +6 -0
- package/lib/typescript/module/redux/rootReducer.d.ts +19 -0
- package/lib/typescript/module/redux/serializableCheck.d.ts +14 -0
- package/lib/typescript/module/secureScreen/index.d.ts +25 -0
- package/lib/typescript/module/versionCheck/buildNumber.d.ts +14 -0
- package/lib/typescript/module/versionCheck/index.d.ts +28 -17
- package/package.json +52 -5
|
@@ -1,243 +1 @@
|
|
|
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 { getLastKnownNetworkStatus } from "../network/networkStatus.js";
|
|
8
|
-
import "../types/axios.js";
|
|
9
|
-
import { applyConfiguredBaseUrl } from "./baseUrl.js";
|
|
10
|
-
import { logCurl } from "./curl.js";
|
|
11
|
-
import { sendFormDataAsMultipart } from "./multipart.js";
|
|
12
|
-
import { attachRetry } from "./retry.js";
|
|
13
|
-
import { dispatchLogout, getErrorMessage, getRefreshPath, isRefreshInFlight, queueBehindRefresh, refreshAccessToken, resetRefreshState } from "./tokenRefresh.js";
|
|
14
|
-
export { injectStore, injectUserAuth, resetRefreshState } from "./tokenRefresh.js";
|
|
15
|
-
|
|
16
|
-
// Define timeout for requests
|
|
17
|
-
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Thrown by the request interceptor when the device is offline, before the
|
|
21
|
-
* request ever reaches the adapter (and therefore the network). Distinct from
|
|
22
|
-
* an AxiosError so callers can show an offline message instead of a generic
|
|
23
|
-
* request failure.
|
|
24
|
-
*/
|
|
25
|
-
export class RequestOfflineError extends Error {
|
|
26
|
-
constructor(message = 'Request blocked: device is offline') {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = 'RequestOfflineError';
|
|
29
|
-
// Babel's class transform can drop the prototype link, which makes
|
|
30
|
-
// `instanceof RequestOfflineError` false in the transpiled build.
|
|
31
|
-
Object.setPrototypeOf(this, RequestOfflineError.prototype);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Sub-codes a 401 response body carries when the session itself is dead
|
|
37
|
-
* (terminated by an admin, or expired server-side) rather than merely
|
|
38
|
-
* out-of-date. These never warrant a refresh attempt — only logout.
|
|
39
|
-
*/
|
|
40
|
-
export const SESSION_TERMINATION_CODES = {
|
|
41
|
-
TERMINATED: 'SESSION_TERMINATED',
|
|
42
|
-
EXPIRED: 'SESSION_EXPIRED'
|
|
43
|
-
};
|
|
44
|
-
const TERMINATION_CODES = new Set(Object.values(SESSION_TERMINATION_CODES));
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The header is the AUTHORITATIVE signal, verified against the backend:
|
|
48
|
-
* `WeborityJwtMiddleware.ReturnUnauthorized` answers an invalidated session with
|
|
49
|
-
* `401` + `X-Security-Stamp-Invalid: true` and a body of
|
|
50
|
-
* `{ error: 'Unauthorized', message: '...' }` — no code field at all, and
|
|
51
|
-
* `error` is a string rather than an object. An earlier version of this checked
|
|
52
|
-
* only body fields and would therefore never have fired against a real server.
|
|
53
|
-
*
|
|
54
|
-
* The body codes below are kept as a secondary path for any head that does send
|
|
55
|
-
* an explicit sub-code; they match on exact strings, so looking is free.
|
|
56
|
-
*/
|
|
57
|
-
export const SESSION_INVALID_HEADER = 'x-security-stamp-invalid';
|
|
58
|
-
const hasSessionInvalidHeader = headers => {
|
|
59
|
-
if (!headers || typeof headers !== 'object') {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
const get = headers.get;
|
|
63
|
-
const raw = typeof get === 'function' ? get.call(headers, SESSION_INVALID_HEADER) : headers[SESSION_INVALID_HEADER];
|
|
64
|
-
return String(raw).toLowerCase() === 'true';
|
|
65
|
-
};
|
|
66
|
-
const getSessionTerminationCode = data => {
|
|
67
|
-
if (!data || typeof data !== 'object') {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
const body = data;
|
|
71
|
-
const nested = body.error;
|
|
72
|
-
const candidates = [body.code, body.errorCode, nested && typeof nested === 'object' ? nested.code : undefined];
|
|
73
|
-
for (const candidate of candidates) {
|
|
74
|
-
if (typeof candidate === 'string' && TERMINATION_CODES.has(candidate)) {
|
|
75
|
-
return candidate;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return undefined;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Internal extension of axios request config to track retry attempts.
|
|
83
|
-
*/
|
|
84
|
-
|
|
85
|
-
const userClient = axios.create({
|
|
86
|
-
headers: {
|
|
87
|
-
'Content-Type': 'application/json',
|
|
88
|
-
'Accept-Encoding': 'gzip, deflate, br'
|
|
89
|
-
},
|
|
90
|
-
timeout: REQUEST_TIMEOUT // Set timeout for all requests
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Registered before the auth interceptors below, so a retryable 5xx is retried
|
|
94
|
-
// before anything else sees it while a 401 still falls through to the refresh
|
|
95
|
-
// flow. Policy and reasoning live in ./retry.
|
|
96
|
-
attachRetry(userClient);
|
|
97
|
-
const setAuthHeader = (config, token) => {
|
|
98
|
-
if (config.headers instanceof AxiosHeaders) {
|
|
99
|
-
config.headers.set('Authorization', `Bearer ${token}`);
|
|
100
|
-
} else {
|
|
101
|
-
// Fallback in case headers were provided as a plain object.
|
|
102
|
-
config.headers = AxiosHeaders.from({
|
|
103
|
-
...config.headers,
|
|
104
|
-
Authorization: `Bearer ${token}`
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
const isRefreshTokenRequest = url => {
|
|
109
|
-
const path = getRefreshPath();
|
|
110
|
-
return !!path && !!url?.includes(path);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Attach Authorization Token Before Requests
|
|
115
|
-
*/
|
|
116
|
-
// Registered after the curl logger and before the auth interceptor, so that it
|
|
117
|
-
// RUNS between them: axios unshifts its request chain, making the first
|
|
118
|
-
// registered run last. The curl line therefore describes a request that already
|
|
119
|
-
// has both its baseURL and its Authorization header.
|
|
120
|
-
applyConfiguredBaseUrl(userClient);
|
|
121
|
-
sendFormDataAsMultipart(userClient);
|
|
122
|
-
|
|
123
|
-
// Registered BEFORE the auth interceptor below deliberately. Axios builds its
|
|
124
|
-
// request chain by unshifting, so the FIRST-registered request interceptor runs
|
|
125
|
-
// LAST — closest to dispatch. This one therefore observes the final config,
|
|
126
|
-
// with the Authorization header already attached, which is the whole point of
|
|
127
|
-
// a curl line. Registering it after would log the request before it is signed.
|
|
128
|
-
userClient.interceptors.request.use(config => {
|
|
129
|
-
if (getConfigValue('logCurl')) {
|
|
130
|
-
logCurl(config);
|
|
131
|
-
}
|
|
132
|
-
return config;
|
|
133
|
-
});
|
|
134
|
-
userClient.interceptors.request.use(async config => {
|
|
135
|
-
// Reject before any network call when the device is known to be offline.
|
|
136
|
-
// Unknown connectivity (netinfo not installed, or undetermined) is treated
|
|
137
|
-
// as online — never block a request on an unavailable probe.
|
|
138
|
-
const network = await getLastKnownNetworkStatus();
|
|
139
|
-
if (network.isConnected === false) {
|
|
140
|
-
Logger.warn(`Request blocked: device is offline. URL: ${config.url}`);
|
|
141
|
-
return Promise.reject(new RequestOfflineError());
|
|
142
|
-
}
|
|
143
|
-
try {
|
|
144
|
-
// Skip token check for refresh token endpoint to prevent infinite loop
|
|
145
|
-
if (isRefreshTokenRequest(config.url)) {
|
|
146
|
-
return config;
|
|
147
|
-
}
|
|
148
|
-
const token = await getToken(ACCESS_TOKEN_KEY);
|
|
149
|
-
|
|
150
|
-
// Check if token exists
|
|
151
|
-
if (!token?.token) {
|
|
152
|
-
throw new Error('No access token available');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Check if token is expired
|
|
156
|
-
const currentTime = Date.now();
|
|
157
|
-
const tokenExpiry = Date.parse(token.expiry);
|
|
158
|
-
const bufferTime = 30 * 1000; // 30 seconds buffer
|
|
159
|
-
|
|
160
|
-
// If token is expired or about to expire, try to refresh it first
|
|
161
|
-
if (tokenExpiry <= currentTime + bufferTime) {
|
|
162
|
-
Logger.info('Access token expired or about to expire, refreshing before request');
|
|
163
|
-
|
|
164
|
-
// Joins a refresh already in flight rather than starting a second one.
|
|
165
|
-
const newToken = await refreshAccessToken();
|
|
166
|
-
setAuthHeader(config, newToken);
|
|
167
|
-
return config;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Token is valid, proceed with request
|
|
171
|
-
setAuthHeader(config, token.token);
|
|
172
|
-
return config;
|
|
173
|
-
} catch (error) {
|
|
174
|
-
Logger.error('Failed to attach token:', getErrorMessage(error));
|
|
175
|
-
return Promise.reject(error);
|
|
176
|
-
}
|
|
177
|
-
}, error => {
|
|
178
|
-
Logger.error('Request error:', getErrorMessage(error));
|
|
179
|
-
return Promise.reject(error);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Handle Response & Token Refresh
|
|
184
|
-
*/
|
|
185
|
-
userClient.interceptors.response.use(response => response, async error => {
|
|
186
|
-
if (!axios.isAxiosError(error)) {
|
|
187
|
-
Logger.error(`Non-axios error in response interceptor: ${getErrorMessage(error)}`);
|
|
188
|
-
return Promise.reject(error);
|
|
189
|
-
}
|
|
190
|
-
const axiosError = error;
|
|
191
|
-
const originalRequest = axiosError.config;
|
|
192
|
-
if (!axiosError.response) {
|
|
193
|
-
if (!originalRequest?.skipErrorLog) {
|
|
194
|
-
Logger.error(`Network error or no response from server: ${axiosError.message}, URL: ${originalRequest?.url}`);
|
|
195
|
-
}
|
|
196
|
-
return Promise.reject(error);
|
|
197
|
-
}
|
|
198
|
-
if (axiosError.response.status === 401 && originalRequest && !originalRequest._retry) {
|
|
199
|
-
// The refresh call goes through this client. A 401 on it must not queue behind
|
|
200
|
-
// refreshTokenPromise: that promise is the call itself, so queueing deadlocks it
|
|
201
|
-
// and every later 401 waits forever.
|
|
202
|
-
if (isRefreshTokenRequest(originalRequest.url)) {
|
|
203
|
-
return Promise.reject(error);
|
|
204
|
-
}
|
|
205
|
-
const terminationCode = getSessionTerminationCode(axiosError.response.data) ?? (hasSessionInvalidHeader(axiosError.response.headers) ? SESSION_TERMINATION_CODES.TERMINATED : undefined);
|
|
206
|
-
if (terminationCode) {
|
|
207
|
-
// The session is dead server-side — a refresh would only burn a
|
|
208
|
-
// pointless round-trip, so go straight to logout.
|
|
209
|
-
Logger.warn(`Session ${terminationCode}, logging out without refresh. URL: ${originalRequest.url}`);
|
|
210
|
-
resetRefreshState();
|
|
211
|
-
await dispatchLogout();
|
|
212
|
-
return Promise.reject(error);
|
|
213
|
-
}
|
|
214
|
-
Logger.warn(`Unauthorized URL: ${originalRequest.url}`);
|
|
215
|
-
originalRequest._retry = true;
|
|
216
|
-
|
|
217
|
-
// If we already have a refresh in progress, queue this request
|
|
218
|
-
if (isRefreshInFlight()) {
|
|
219
|
-
return queueBehindRefresh().then(token => {
|
|
220
|
-
Logger.info('Retrying request after token refresh:', originalRequest.url);
|
|
221
|
-
setAuthHeader(originalRequest, token);
|
|
222
|
-
return userClient(originalRequest);
|
|
223
|
-
}).catch(err => {
|
|
224
|
-
Logger.error('Failed to retry request after token refresh:', getErrorMessage(err));
|
|
225
|
-
return Promise.reject(err);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
// Start a new token refresh process and wait for it to complete
|
|
230
|
-
const newToken = await refreshAccessToken();
|
|
231
|
-
setAuthHeader(originalRequest, newToken);
|
|
232
|
-
return userClient(originalRequest);
|
|
233
|
-
} catch (err) {
|
|
234
|
-
return Promise.reject(err);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (!originalRequest?.skipErrorLog) {
|
|
238
|
-
Logger.error(`Response error: ${axiosError.response?.data ?? axiosError.message}, URL: ${originalRequest?.url}, Status: ${axiosError.response.status}`);
|
|
239
|
-
}
|
|
240
|
-
return Promise.reject(error);
|
|
241
|
-
});
|
|
242
|
-
export default userClient;
|
|
243
|
-
//# sourceMappingURL=userClient.js.map
|
|
1
|
+
"use strict";import axios,{AxiosHeaders}from"axios";import{ACCESS_TOKEN_KEY,getToken}from"../auth/authStore.js";import{getConfigValue}from"../config/index.js";import{Logger}from"../logger/index.js";import{getLastKnownNetworkStatus}from"../network/networkStatus.js";import"../types/axios.js";import{applyConfiguredBaseUrl}from"./baseUrl.js";import{applyAccountIdHeader,applyClientHeaders}from"./clientHeaders.js";import{logCurl}from"./curl.js";import{readMinAppVersion}from"./minAppVersion.js";import{sendFormDataAsMultipart}from"./multipart.js";import{applyRequestDefaults}from"./requestDefaults.js";import{responseBodyText}from"./responseBodyText.js";import{attachRetry}from"./retry.js";import{endSessionAndLogout,getAccountIdProvider,getErrorMessage,getRefreshPath,isRefreshInFlight,queueBehindRefresh,readSignOutReason,refreshAccessToken,resetRefreshState}from"./tokenRefresh.js";export{injectStore,injectUserAuth,resetRefreshState,SIGN_OUT_REASON_HEADER}from"./tokenRefresh.js";export class RequestOfflineError extends Error{constructor(e="Request blocked: device is offline"){super(e),this.name="RequestOfflineError",Object.setPrototypeOf(this,RequestOfflineError.prototype)}}export const SESSION_TERMINATION_CODES={TERMINATED:"SESSION_TERMINATED",EXPIRED:"SESSION_EXPIRED"};const TERMINATION_CODES=new Set(Object.values(SESSION_TERMINATION_CODES));export const SESSION_INVALID_HEADER="x-security-stamp-invalid";const hasSessionInvalidHeader=e=>{if(!e||"object"!=typeof e)return!1;const r=e.get,t="function"==typeof r?r.call(e,SESSION_INVALID_HEADER):e[SESSION_INVALID_HEADER];return"true"===String(t).toLowerCase()},getSessionTerminationCode=e=>{if(!e||"object"!=typeof e)return;const r=e,t=r.error,s=[r.code,r.errorCode,t&&"object"==typeof t?t.code:void 0];for(const e of s)if("string"==typeof e&&TERMINATION_CODES.has(e))return e},userClient=axios.create({headers:{"Content-Type":"application/json"}});readMinAppVersion(userClient),attachRetry(userClient);const setAuthHeader=(e,r)=>{e.headers instanceof AxiosHeaders?e.headers.set("Authorization",`Bearer ${r}`):e.headers=AxiosHeaders.from({...e.headers,Authorization:`Bearer ${r}`})},isRefreshTokenRequest=e=>{const r=getRefreshPath();return!!r&&!!e?.includes(r)};applyConfiguredBaseUrl(userClient),applyRequestDefaults(userClient),sendFormDataAsMultipart(userClient),userClient.interceptors.request.use(e=>(getConfigValue("logCurl")&&logCurl(e),e)),applyClientHeaders(userClient),userClient.interceptors.request.use(async e=>{if(!1===(await getLastKnownNetworkStatus()).isConnected)return Logger.warn(`Request blocked: device is offline. URL: ${e.url}`),Promise.reject(new RequestOfflineError);try{if(isRefreshTokenRequest(e.url))return e;const r=await getToken(ACCESS_TOKEN_KEY);if(!r?.token)throw new Error("No access token available");const t=Date.now(),s=Date.parse(r.expiry);if(s<=t+3e4){Logger.info("Access token expired or about to expire, refreshing before request");const r=await refreshAccessToken();return setAuthHeader(e,r),await applyAccountIdHeader(e.headers,getAccountIdProvider()),e}return setAuthHeader(e,r.token),await applyAccountIdHeader(e.headers,getAccountIdProvider()),e}catch(e){return Logger.error("Failed to attach token:",getErrorMessage(e)),Promise.reject(e)}},e=>(Logger.error("Request error:",getErrorMessage(e)),Promise.reject(e))),userClient.interceptors.response.use(e=>e,async e=>{if(!axios.isAxiosError(e))return Logger.error(`Non-axios error in response interceptor: ${getErrorMessage(e)}`),Promise.reject(e);const r=e,t=r.config;if(!r.response)return t?.skipErrorLog||Logger.error(`Network error or no response from server: ${r.message}, URL: ${t?.url}`),Promise.reject(e);if(401===r.response.status&&t&&!t._retry){if(isRefreshTokenRequest(t.url))return Promise.reject(e);const s=readSignOutReason(r.response.headers),o=getSessionTerminationCode(r.response.data)??(hasSessionInvalidHeader(r.response.headers)||null!==s?SESSION_TERMINATION_CODES.TERMINATED:void 0);if(o)return Logger.warn(`Session ${o}, logging out without refresh. URL: ${t.url}`),resetRefreshState(),await endSessionAndLogout("terminated",s),Promise.reject(e);if(Logger.warn(`Unauthorized URL: ${t.url}`),t._retry=!0,isRefreshInFlight())return queueBehindRefresh().then(e=>(Logger.info("Retrying request after token refresh:",t.url),setAuthHeader(t,e),userClient(t))).catch(e=>(Logger.error("Failed to retry request after token refresh:",getErrorMessage(e)),Promise.reject(e)));try{const e=await refreshAccessToken();return setAuthHeader(t,e),userClient(t)}catch(e){return Promise.reject(e)}}return t?.skipErrorLog||Logger.error(`Response error: ${responseBodyText(r.response?.data,r.message)}, URL: ${t?.url}, Status: ${r.response.status}`),Promise.reject(e)});export default userClient;
|
|
@@ -1,257 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import * as SecureStore from 'expo-secure-store';
|
|
4
|
-
import { Logger } from "../logger/index.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Token data persisted in the keychain.
|
|
8
|
-
*/
|
|
9
|
-
/**
|
|
10
|
-
* The two keys the library itself reads. Everything in the library that needs
|
|
11
|
-
* the bearer token, the HTTP client and the authenticated image cache,
|
|
12
|
-
* resolves it through these rather than repeating the literal, so
|
|
13
|
-
* one of them cannot silently drift and start reading a key nothing writes.
|
|
14
|
-
* Apps are free to store their own tokens under any other key.
|
|
15
|
-
*/
|
|
16
|
-
export const ACCESS_TOKEN_KEY = 'accessToken';
|
|
17
|
-
export const REFRESH_TOKEN_KEY = 'refreshToken';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The shape this module speaks to its store through. expo-secure-store is
|
|
21
|
-
* adapted to it below; an app or a test can supply anything else of this shape
|
|
22
|
-
* through `setKeychainImplementation`.
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* The GENERIC password API, not the internet-credentials one.
|
|
26
|
-
*
|
|
27
|
-
* `kSecClassInternetPassword` exists for credentials bound to a server, protocol
|
|
28
|
-
* and port, and it integrates with Safari and AutoFill. A bearer token keyed by
|
|
29
|
-
* a name like "accessToken" is none of those things, and the library was only
|
|
30
|
-
* satisfying that API's required `server` argument by passing the key twice,
|
|
31
|
-
* once as the server and once as the service. That is misuse of the class.
|
|
32
|
-
*
|
|
33
|
-
* `kSecClassGenericPassword` is the class for app-local secrets, which is what a
|
|
34
|
-
* token is. It is what expo-secure-store uses underneath, and what the consumer
|
|
35
|
-
* app being migrated had already chosen for itself.
|
|
36
|
-
*/
|
|
37
|
-
/**
|
|
38
|
-
* The credential shape the seam reads back, declared here rather than imported
|
|
39
|
-
* from whichever package a consumer's backend comes from. A type-only import of
|
|
40
|
-
* a package this library does not declare survives into the published `.d.ts`,
|
|
41
|
-
* where a consumer that has not installed it either fails to typecheck or,
|
|
42
|
-
* under the RN template's `skipLibCheck: true`, loses all checking on these
|
|
43
|
-
* members with no signal. Any credential object with these three members
|
|
44
|
-
* satisfies it structurally.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
// Only what a test or the showcase injected; null means use expo-secure-store.
|
|
48
|
-
let backend = null;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* expo-secure-store, adapted to the generic-password shape this module speaks.
|
|
52
|
-
*
|
|
53
|
-
* It is a flat key/value store, so the credential shape is reconstructed around
|
|
54
|
-
* it. The key is sanitised because expo rejects anything outside
|
|
55
|
-
* [A-Za-z0-9._-] at runtime, which would otherwise surface as a throw from deep
|
|
56
|
-
* inside the store rather than as a bad key.
|
|
57
|
-
*/
|
|
58
|
-
const keyFor = service => (service ?? 'default').replace(/[^A-Za-z0-9._-]/g, '_');
|
|
59
|
-
const expoSecureStoreBackend = {
|
|
60
|
-
setGenericPassword: async (username, password, options) => {
|
|
61
|
-
await SecureStore.setItemAsync(keyFor(options?.service), JSON.stringify({
|
|
62
|
-
username,
|
|
63
|
-
password
|
|
64
|
-
}));
|
|
65
|
-
return true;
|
|
66
|
-
},
|
|
67
|
-
getGenericPassword: async options => {
|
|
68
|
-
const service = options?.service;
|
|
69
|
-
const raw = await SecureStore.getItemAsync(keyFor(service));
|
|
70
|
-
if (raw === null) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
const parsed = JSON.parse(raw);
|
|
74
|
-
return {
|
|
75
|
-
username: parsed.username,
|
|
76
|
-
password: parsed.password,
|
|
77
|
-
service: service ?? 'default'
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
resetGenericPassword: async options => {
|
|
81
|
-
await SecureStore.deleteItemAsync(keyFor(options?.service));
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
const resolveBackend = () => backend ?? expoSecureStoreBackend;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Every request reads the access token, and a secure-store read is a native
|
|
89
|
-
* keychain/keystore decrypt. Reads are therefore served from memory, written
|
|
90
|
-
* through by `storeToken` and dropped by `removeToken`. `generation` moves on
|
|
91
|
-
* every write, removal and backend swap, so a read that started before one of
|
|
92
|
-
* those cannot cache the value it saw.
|
|
93
|
-
*/
|
|
94
|
-
const memo = new Map();
|
|
95
|
-
let generation = 0;
|
|
96
|
-
const forget = key => {
|
|
97
|
-
generation += 1;
|
|
98
|
-
if (key === undefined) {
|
|
99
|
-
memo.clear();
|
|
100
|
-
} else {
|
|
101
|
-
memo.delete(key);
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
const requireBackend = op => {
|
|
105
|
-
const b = resolveBackend();
|
|
106
|
-
if (!b) {
|
|
107
|
-
const err = new Error(`[authStore] no secure-store backend available; cannot ${op}.`);
|
|
108
|
-
Logger.error(err.message);
|
|
109
|
-
throw err;
|
|
110
|
-
}
|
|
111
|
-
return b;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Replace the secure-store backend. Intended for TESTS and for the showcase,
|
|
116
|
-
* which injects a failing store to demo the write-failure path; it is not how
|
|
117
|
-
* an app configures the library. Pass `null` to restore expo-secure-store.
|
|
118
|
-
*/
|
|
119
|
-
export const setKeychainImplementation = impl => {
|
|
120
|
-
backend = impl;
|
|
121
|
-
forget();
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Store token with expiry.
|
|
126
|
-
*/
|
|
127
|
-
export const storeToken = async (key, token, expiry) => {
|
|
128
|
-
// Never log the token or expiry values: Logger forwards to whatever remote
|
|
129
|
-
// sink the app wired (Crashlytics/Sentry), so a token here leaves the device.
|
|
130
|
-
Logger.info(`Storing token for key: ${key}`);
|
|
131
|
-
forget(key);
|
|
132
|
-
try {
|
|
133
|
-
const data = JSON.stringify({
|
|
134
|
-
token,
|
|
135
|
-
expiry
|
|
136
|
-
});
|
|
137
|
-
await requireBackend('store a token').setGenericPassword('token', data, {
|
|
138
|
-
service: key
|
|
139
|
-
});
|
|
140
|
-
forget(key);
|
|
141
|
-
memo.set(key, {
|
|
142
|
-
token,
|
|
143
|
-
expiry
|
|
144
|
-
});
|
|
145
|
-
Logger.info(`Token stored successfully for key: ${key}`);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
forget(key);
|
|
148
|
-
// Log AND rethrow. Swallowing this told the caller the token was persisted
|
|
149
|
-
// when it was not, so an app would show a signed-in user holding a token
|
|
150
|
-
// that never reached the keychain — the session then dies at the next
|
|
151
|
-
// cold start with no failure anyone could have seen.
|
|
152
|
-
Logger.error(`Error storing token for key: ${key}`, error);
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Parse token data from credentials.
|
|
159
|
-
*/
|
|
160
|
-
const parseTokenData = credentials => {
|
|
161
|
-
if (!credentials) {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
const {
|
|
165
|
-
password
|
|
166
|
-
} = credentials;
|
|
167
|
-
try {
|
|
168
|
-
const parsed = JSON.parse(password);
|
|
169
|
-
if (parsed && typeof parsed === 'object' && 'token' in parsed && 'expiry' in parsed && typeof parsed.token === 'string' && typeof parsed.expiry === 'string') {
|
|
170
|
-
return parsed;
|
|
171
|
-
}
|
|
172
|
-
return null;
|
|
173
|
-
} catch {
|
|
174
|
-
// The single report for "stored, but not a token" belongs to getToken, which
|
|
175
|
-
// knows whether anything was stored at all. Logging here too made one event
|
|
176
|
-
// arrive twice, at two different levels.
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Retrieve token and check if expired.
|
|
183
|
-
*/
|
|
184
|
-
export const getToken = async key => {
|
|
185
|
-
if (memo.has(key)) {
|
|
186
|
-
return memo.get(key) ?? null;
|
|
187
|
-
}
|
|
188
|
-
const seen = generation;
|
|
189
|
-
try {
|
|
190
|
-
const credentials = await requireBackend('read a token').getGenericPassword({
|
|
191
|
-
service: key
|
|
192
|
-
});
|
|
193
|
-
// Nothing stored is an ORDINARY state, not a fault: a signed-out user has no
|
|
194
|
-
// token, and every authenticated request, image and download asks for one.
|
|
195
|
-
// Warning here put three info lines and a warning into the log, and into the
|
|
196
|
-
// remote crash sink, on every one of those calls.
|
|
197
|
-
let value = null;
|
|
198
|
-
if (credentials) {
|
|
199
|
-
const parsedData = parseTokenData(credentials);
|
|
200
|
-
if (parsedData) {
|
|
201
|
-
value = {
|
|
202
|
-
token: parsedData.token,
|
|
203
|
-
expiry: parsedData.expiry
|
|
204
|
-
};
|
|
205
|
-
} else {
|
|
206
|
-
// Something IS stored and it is not a token. That is corruption worth
|
|
207
|
-
// hearing about, and it is a different event from having no token.
|
|
208
|
-
Logger.warn(`Stored credentials for key "${key}" are not a valid token; ignoring them.`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (seen === generation) {
|
|
212
|
-
memo.set(key, value);
|
|
213
|
-
}
|
|
214
|
-
return value;
|
|
215
|
-
} catch (error) {
|
|
216
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
-
// Both spellings matter. Android throws "Key permanently invalidated" when
|
|
218
|
-
// the biometric enrolment changes; iOS throws "Secret invalidated" for the
|
|
219
|
-
// equivalent. Either way the stored value can never be decrypted again, so
|
|
220
|
-
// clearing it is the only way out: leaving it makes every later read fail
|
|
221
|
-
// identically, and the user is stuck with no route back to a login. A
|
|
222
|
-
// consumer app handled the iOS spelling and the library did not.
|
|
223
|
-
if (message.includes('Key permanently invalidated') || message.includes('Secret invalidated')) {
|
|
224
|
-
Logger.error(`Stored token for key "${key}" can no longer be decrypted; clearing it.`);
|
|
225
|
-
await removeToken(key);
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
Logger.error(`Error retrieving token for key: ${key}`, error);
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Remove token by key.
|
|
235
|
-
*/
|
|
236
|
-
export const removeToken = async key => {
|
|
237
|
-
forget(key);
|
|
238
|
-
try {
|
|
239
|
-
await requireBackend('remove a token').resetGenericPassword({
|
|
240
|
-
service: key
|
|
241
|
-
});
|
|
242
|
-
Logger.info(`Token removed successfully for key: ${key}`);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
// Clearing a key that was never written is the ordinary logout path, not a
|
|
245
|
-
// failure: only one of the two tokens may ever have been stored. Logging it
|
|
246
|
-
// as an error puts routine traffic in the channel people watch for real
|
|
247
|
-
// problems, which is how an error channel stops being watched.
|
|
248
|
-
if (error instanceof Error && error.message.includes('Item not found')) {
|
|
249
|
-
Logger.info(`No token found to remove for key: ${key}`);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
Logger.error(`Failed to remove token for key: ${key}`, error);
|
|
253
|
-
} finally {
|
|
254
|
-
forget(key);
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
//# sourceMappingURL=authStore.js.map
|
|
1
|
+
"use strict";import*as SecureStore from"expo-secure-store";import{Logger}from"../logger/index.js";import{Storage}from"../storage/index.js";import{getJWTExpiry}from"./jwt.js";export const ACCESS_TOKEN_KEY="accessToken";export const REFRESH_TOKEN_KEY="refreshToken";export const SESSION_USER_KEY="sessionUser";export const LEGACY_REFRESH_TOKEN_LIFETIME_MS=2592e6;let backend=null;const keyFor=e=>(e??"default").replace(/[^A-Za-z0-9._-]/g,"_"),expoSecureStoreBackend={setGenericPassword:async(e,r,o)=>(await SecureStore.setItemAsync(keyFor(o?.service),JSON.stringify({username:e,password:r})),!0),getGenericPassword:async e=>{const r=e?.service,o=await SecureStore.getItemAsync(keyFor(r));if(null===o)return!1;const t=JSON.parse(o);return{username:t.username,password:t.password,service:r??"default"}},resetGenericPassword:async e=>(await SecureStore.deleteItemAsync(keyFor(e?.service)),!0)},resolveBackend=()=>backend??expoSecureStoreBackend,memo=new Map;let generation=0;const forget=e=>{generation+=1,void 0===e?memo.clear():memo.delete(e)},requireBackend=e=>{const r=resolveBackend();if(!r){const r=new Error(`[authStore] no secure-store backend available; cannot ${e}.`);throw Logger.error(r.message),r}return r};export const setKeychainImplementation=e=>{backend=e,forget()};export const storeToken=async(e,r,o)=>{Logger.info(`Storing token for key: ${e}`),forget(e);try{const t=JSON.stringify({token:r,expiry:o});await requireBackend("store a token").setGenericPassword("token",t,{service:e}),forget(e),memo.set(e,{token:r,expiry:o}),Logger.info(`Token stored successfully for key: ${e}`)}catch(r){throw forget(e),Logger.error(`Error storing token for key: ${e}`,r),r}};const parseTokenData=e=>{if(!e)return null;const{password:r}=e;try{const e=JSON.parse(r);return e&&"object"==typeof e&&"token"in e&&"expiry"in e&&"string"==typeof e.token&&"string"==typeof e.expiry?e:null}catch{return null}};export const getToken=async e=>{if(isSessionKey(e)&&legacyKeys&&await migrateLegacySession(),memo.has(e))return memo.get(e)??null;const r=generation;try{const o=await requireBackend("read a token").getGenericPassword({service:e});let t=null;if(o){const r=parseTokenData(o);if(r)t={token:r.token,expiry:r.expiry};else if(Logger.warn(`Stored credentials for key "${e}" are not a valid token; ignoring them.`),isSessionKey(e))return await endSession("corrupt",!0),null}return r===generation&&memo.set(e,t),t}catch(r){const o=r instanceof Error?r.message:String(r);return o.includes("Key permanently invalidated")||o.includes("Secret invalidated")?(Logger.error(`Stored token for key "${e}" can no longer be decrypted; clearing it.`),isSessionKey(e)?await endSession("corrupt",!0):await removeToken(e),null):(Logger.error(`Error retrieving token for key: ${e}`,r),null)}};export const removeToken=async e=>{forget(e);try{await requireBackend("remove a token").resetGenericPassword({service:e}),Logger.info(`Token removed successfully for key: ${e}`)}catch(r){if(r instanceof Error&&r.message.includes("Item not found"))return void Logger.info(`No token found to remove for key: ${e}`);Logger.error(`Failed to remove token for key: ${e}`,r)}finally{forget(e)}};const isSessionKey=e=>"accessToken"===e||"refreshToken"===e;export const storeSessionUser=async e=>{if(!await Storage.setItem("sessionUser",JSON.stringify(e)))throw new Error("[authStore] could not store the session user.")};export const getSessionUser=async()=>{const e=await Storage.getItem("sessionUser");if(null===e)return null;try{return JSON.parse(e)}catch{return Logger.warn("The stored session user is not valid JSON; clearing it."),await removeSessionUser(),null}};export const removeSessionUser=async()=>{await Storage.removeItem("sessionUser")};let legacyKeys=null,legacyCheck=null;export const setLegacyTokenKeys=e=>{legacyKeys=e,legacyCheck=null};const readLegacyToken=e=>{try{const r=JSON.parse(e);if("string"==typeof r)return""===r.trim()?null:{token:r,expiry:null};if(r&&"object"==typeof r&&"string"==typeof r.token&&"string"==typeof r.expiry)return r}catch{}return""===e.trim()?null:{token:e,expiry:null}},expiryOf=e=>3===e.split(".").length?getJWTExpiry(e)?.toISOString()??null:null,readLegacyRaw=async e=>{const r=await resolveBackend().getGenericPassword({service:e});return r?r.password:null},migrateLegacySession=()=>{if(!legacyKeys)return Promise.resolve();if(legacyCheck)return legacyCheck;const e=legacyKeys;return legacyCheck=(async()=>{try{const r=resolveBackend(),o=!await r.getGenericPassword({service:"accessToken"})&&!await r.getGenericPassword({service:"refreshToken"}),t=await readLegacyRaw(e.accessToken),n=await readLegacyRaw(e.refreshToken);if(o&&(null!==t||null!==n)){const o=null===t?null:readLegacyToken(t),s=null===n?null:readLegacyToken(n);if(o){const e=o.expiry??expiryOf(o.token)??(new Date).toISOString();await r.setGenericPassword("token",JSON.stringify({token:o.token,expiry:e}),{service:"accessToken"})}if(s){const e=s.expiry??expiryOf(s.token)??new Date(Date.now()+2592e6).toISOString();await r.setGenericPassword("token",JSON.stringify({token:s.token,expiry:e}),{service:"refreshToken"})}if(e.sessionUser){const r=await Storage.getItem(e.sessionUser);null!==r&&await Storage.setItem("sessionUser",r)}null===o&&null===s||(forget(),Logger.breadcrumb("[authStore] moved a session from the legacy token keys"))}for(const o of[e.accessToken,e.refreshToken])await r.resetGenericPassword({service:o}).catch(()=>{});e.sessionUser&&await Storage.removeItem(e.sessionUser)}catch(e){Logger.error("[authStore] could not move the session from the legacy token keys",e)}})(),legacyCheck},listeners=new Set,emitSessionEnded=(e,r)=>{for(const o of[...listeners])try{o(e,r)}catch(e){Logger.error("[authStore] a session-ended listener threw",e)}};export const addSessionEndedListener=e=>(listeners.add(e),()=>{listeners.delete(e)});const endSession=async(e,r,o=null)=>{const t=r||null!==await getToken("accessToken")||null!==await getToken("refreshToken");await removeToken("accessToken"),await removeToken("refreshToken"),await removeSessionUser(),t&&emitSessionEnded(e,{signOutReason:o})};export const clearSession=async(e="signedOut",r={})=>endSession(e,!1,r.signOutReason??null);
|
package/lib/module/auth/index.js
CHANGED
|
@@ -1,5 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
export { getToken, removeToken, storeToken } from "./authStore.js";
|
|
4
|
-
export { decodeJWT, getJWTExpiry, isJWTExpired } from "./jwt.js";
|
|
5
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
"use strict";export{ACCESS_TOKEN_KEY,addSessionEndedListener,clearSession,getSessionUser,getToken,LEGACY_REFRESH_TOKEN_LIFETIME_MS,REFRESH_TOKEN_KEY,removeSessionUser,removeToken,SESSION_USER_KEY,setLegacyTokenKeys,storeSessionUser,storeToken}from"./authStore.js";export{decodeJWT,getJWTExpiry,isJWTExpired}from"./jwt.js";
|
package/lib/module/auth/jwt.js
CHANGED
|
@@ -1,106 +1 @@
|
|
|
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 BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Pure-JS base64, used only when the runtime has no `atob`.
|
|
18
|
-
*
|
|
19
|
-
* This used to be `require('buffer')`. That was a latent bundling failure for
|
|
20
|
-
* every consumer: `buffer` is a Node builtin that React Native does not
|
|
21
|
-
* provide, it was never declared as a dependency, and Metro resolves requires
|
|
22
|
-
* STATICALLY — so sitting inside an unreached fallback branch did not save it.
|
|
23
|
-
* The app still failed to bundle, with an error naming this file. It only went
|
|
24
|
-
* unnoticed because the repo's own checks never bundle for a device.
|
|
25
|
-
*
|
|
26
|
-
* Decoding four base64 characters yields three bytes; the padding tells us how
|
|
27
|
-
* many of those three are real.
|
|
28
|
-
*/
|
|
29
|
-
const decodeBase64Fallback = input => {
|
|
30
|
-
let out = '';
|
|
31
|
-
for (let i = 0; i < input.length; i += 4) {
|
|
32
|
-
const chunk = [0, 1, 2, 3].map(offset => BASE64_ALPHABET.indexOf(input[i + offset] ?? '='));
|
|
33
|
-
const bits = Math.max(chunk[0], 0) << 18 | Math.max(chunk[1], 0) << 12 | Math.max(chunk[2], 0) << 6 | Math.max(chunk[3], 0);
|
|
34
|
-
out += String.fromCharCode(bits >> 16 & 0xff);
|
|
35
|
-
if (chunk[2] !== -1) {
|
|
36
|
-
out += String.fromCharCode(bits >> 8 & 0xff);
|
|
37
|
-
}
|
|
38
|
-
if (chunk[3] !== -1) {
|
|
39
|
-
out += String.fromCharCode(bits & 0xff);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
};
|
|
44
|
-
const decodeBase64 = input =>
|
|
45
|
-
// Every RN runtime the library supports has `atob` (Hermes 0.71+), so the
|
|
46
|
-
// fallback is a safety net rather than a path anyone is expected to take.
|
|
47
|
-
typeof atob === 'function' ? atob(input) : decodeBase64Fallback(input);
|
|
48
|
-
const utf8FromBinary = binary => {
|
|
49
|
-
// Convert percent-encoded UTF-8 sequence so multi-byte chars survive.
|
|
50
|
-
try {
|
|
51
|
-
return decodeURIComponent(binary.split('').map(c => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')).join(''));
|
|
52
|
-
} catch {
|
|
53
|
-
return binary;
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Decode a JWT's payload claims. Does **not** verify the signature.
|
|
59
|
-
*
|
|
60
|
-
* @returns claims object on success, `null` if the token is malformed.
|
|
61
|
-
*/
|
|
62
|
-
export const decodeJWT = token => {
|
|
63
|
-
if (!token || typeof token !== 'string') {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
const parts = token.split('.');
|
|
67
|
-
if (parts.length !== 3) {
|
|
68
|
-
Logger.warn('[jwt] decodeJWT: token is not a valid JWT (expected 3 segments)');
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
const payload = parts[1];
|
|
73
|
-
const json = utf8FromBinary(decodeBase64(base64UrlToBase64(payload)));
|
|
74
|
-
return JSON.parse(json);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
Logger.warn('[jwt] decodeJWT failed to parse payload', error);
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Returns true when the token is expired (or unparseable / missing `exp`).
|
|
83
|
-
*
|
|
84
|
-
* @param token JWT string
|
|
85
|
-
* @param leewaySeconds amount of clock-skew tolerance to apply (default 30s)
|
|
86
|
-
*/
|
|
87
|
-
export const isJWTExpired = (token, leewaySeconds = 30) => {
|
|
88
|
-
const claims = decodeJWT(token);
|
|
89
|
-
if (!claims || typeof claims.exp !== 'number') {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
93
|
-
return claims.exp <= nowSeconds + leewaySeconds;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Returns the token's expiry as a Date, or `null` when no `exp` claim exists.
|
|
98
|
-
*/
|
|
99
|
-
export const getJWTExpiry = token => {
|
|
100
|
-
const claims = decodeJWT(token);
|
|
101
|
-
if (!claims || typeof claims.exp !== 'number') {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
return new Date(claims.exp * 1000);
|
|
105
|
-
};
|
|
106
|
-
//# sourceMappingURL=jwt.js.map
|
|
1
|
+
"use strict";import{Logger}from"../logger/index.js";const base64UrlToBase64=e=>(e+"=".repeat((4-e.length%4)%4)).replace(/-/g,"+").replace(/_/g,"/"),BASE64_ALPHABET="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",decodeBase64Fallback=e=>{let t="";for(let r=0;r<e.length;r+=4){const o=[0,1,2,3].map(t=>BASE64_ALPHABET.indexOf(e[r+t]??"=")),n=Math.max(o[0],0)<<18|Math.max(o[1],0)<<12|Math.max(o[2],0)<<6|Math.max(o[3],0);t+=String.fromCharCode(n>>16&255),-1!==o[2]&&(t+=String.fromCharCode(n>>8&255)),-1!==o[3]&&(t+=String.fromCharCode(255&n))}return t},decodeBase64=e=>"function"==typeof atob?atob(e):decodeBase64Fallback(e),utf8FromBinary=e=>{try{return decodeURIComponent(e.split("").map(e=>"%"+e.charCodeAt(0).toString(16).padStart(2,"0")).join(""))}catch{return e}};export const decodeJWT=e=>{if(!e||"string"!=typeof e)return null;const t=e.split(".");if(3!==t.length)return Logger.warn("[jwt] decodeJWT: token is not a valid JWT (expected 3 segments)"),null;try{const e=t[1],r=utf8FromBinary(decodeBase64(base64UrlToBase64(e)));return JSON.parse(r)}catch(e){return Logger.warn("[jwt] decodeJWT failed to parse payload",e),null}};export const isJWTExpired=(e,t=30)=>{const r=decodeJWT(e);if(!r||"number"!=typeof r.exp)return!0;const o=Math.floor(Date.now()/1e3);return r.exp<=o+t};export const getJWTExpiry=e=>{const t=decodeJWT(e);return t&&"number"==typeof t.exp?new Date(1e3*t.exp):null};
|