@webority-technologies/mobile-core 0.0.1 → 0.0.3
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 +22 -1
- package/lib/commonjs/api/baseUrl.js +34 -0
- package/lib/commonjs/api/curl.js +109 -0
- package/lib/commonjs/api/publicClient.js +11 -20
- package/lib/commonjs/api/retry.js +75 -0
- package/lib/commonjs/api/userClient.js +46 -22
- package/lib/commonjs/auth/authStore.js +101 -33
- package/lib/commonjs/auth/jwt.js +32 -8
- package/lib/commonjs/config/index.js +2 -1
- package/lib/commonjs/deepLink/index.js +286 -0
- package/lib/commonjs/download/adapters/expoFs.js +100 -0
- package/lib/commonjs/download/adapters/shared.js +13 -0
- package/lib/commonjs/download/index.js +272 -0
- package/lib/commonjs/formatters/date.js +1 -1
- package/lib/commonjs/formatters/index.js +24 -0
- package/lib/commonjs/formatters/phone.js +132 -15
- package/lib/commonjs/geolocation/adapters/expoLocation.js +126 -0
- package/lib/commonjs/geolocation/index.js +339 -0
- package/lib/commonjs/index.js +151 -48
- package/lib/commonjs/initSDK.js +23 -2
- package/lib/commonjs/initUserAuth.js +2 -1
- package/lib/commonjs/logger/index.js +159 -5
- package/lib/commonjs/network/index.js +6 -0
- package/lib/commonjs/network/networkStatus.js +50 -15
- package/lib/commonjs/permissions/index.js +24 -1
- package/lib/commonjs/sqlite/adapters/expo.js +53 -0
- package/lib/commonjs/sqlite/adapters/op.js +43 -0
- package/lib/commonjs/sqlite/adapters/rows.js +33 -0
- package/lib/commonjs/sqlite/adapters/storage.js +50 -0
- package/lib/commonjs/sqlite/adapters/sync.js +45 -0
- package/lib/commonjs/sqlite/index.js +610 -0
- package/lib/commonjs/sqlite/nitro.js +17 -0
- package/lib/commonjs/sqlite/op.js +18 -0
- package/lib/commonjs/sqlite/quick.js +14 -0
- package/lib/commonjs/sqlite/storage.js +14 -0
- package/lib/commonjs/storage/index.js +41 -20
- package/lib/commonjs/types/globals.d.js +6 -0
- package/lib/commonjs/utils/imageCompression.js +51 -14
- package/lib/commonjs/utils/index.js +6 -0
- package/lib/commonjs/versionCheck/index.js +28 -4
- package/lib/module/api/baseUrl.js +30 -0
- package/lib/module/api/curl.js +104 -0
- package/lib/module/api/publicClient.js +11 -20
- package/lib/module/api/retry.js +69 -0
- package/lib/module/api/userClient.js +45 -21
- package/lib/module/auth/authStore.js +99 -32
- package/lib/module/auth/jwt.js +32 -8
- package/lib/module/config/index.js +2 -1
- package/lib/module/deepLink/index.js +280 -0
- package/lib/module/download/adapters/expoFs.js +95 -0
- package/lib/module/download/adapters/shared.js +8 -0
- package/lib/module/download/index.js +264 -0
- package/lib/module/formatters/date.js +1 -1
- package/lib/module/formatters/index.js +1 -1
- package/lib/module/formatters/phone.js +110 -13
- package/lib/module/geolocation/adapters/expoLocation.js +121 -0
- package/lib/module/geolocation/index.js +332 -0
- package/lib/module/index.js +28 -11
- package/lib/module/initSDK.js +23 -2
- package/lib/module/initUserAuth.js +2 -1
- package/lib/module/logger/index.js +156 -4
- package/lib/module/network/index.js +1 -1
- package/lib/module/network/networkStatus.js +47 -14
- package/lib/module/permissions/index.js +22 -0
- package/lib/module/sqlite/adapters/expo.js +48 -0
- package/lib/module/sqlite/adapters/op.js +38 -0
- package/lib/module/sqlite/adapters/rows.js +28 -0
- package/lib/module/sqlite/adapters/storage.js +46 -0
- package/lib/module/sqlite/adapters/sync.js +41 -0
- package/lib/module/sqlite/index.js +600 -0
- package/lib/module/sqlite/nitro.js +12 -0
- package/lib/module/sqlite/op.js +13 -0
- package/lib/module/sqlite/quick.js +9 -0
- package/lib/module/sqlite/storage.js +9 -0
- package/lib/module/storage/index.js +38 -19
- package/lib/module/types/globals.d.js +10 -0
- package/lib/module/utils/imageCompression.js +49 -13
- package/lib/module/utils/index.js +1 -1
- package/lib/module/versionCheck/index.js +27 -4
- 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/retry.d.ts +27 -0
- package/lib/typescript/commonjs/api/userClient.d.ts +11 -0
- package/lib/typescript/commonjs/auth/authStore.d.ts +29 -6
- package/lib/typescript/commonjs/config/index.d.ts +6 -0
- package/lib/typescript/commonjs/deepLink/index.d.ts +54 -0
- package/lib/typescript/commonjs/download/adapters/expoFs.d.ts +45 -0
- package/lib/typescript/commonjs/download/adapters/shared.d.ts +3 -0
- package/lib/typescript/commonjs/download/index.d.ts +76 -0
- package/lib/typescript/commonjs/formatters/index.d.ts +2 -1
- package/lib/typescript/commonjs/formatters/phone.d.ts +27 -1
- package/lib/typescript/commonjs/geolocation/adapters/expoLocation.d.ts +42 -0
- package/lib/typescript/commonjs/geolocation/index.d.ts +89 -0
- package/lib/typescript/commonjs/index.d.ts +30 -13
- package/lib/typescript/commonjs/initSDK.d.ts +32 -0
- package/lib/typescript/commonjs/logger/index.d.ts +39 -0
- package/lib/typescript/commonjs/network/index.d.ts +2 -2
- package/lib/typescript/commonjs/network/networkStatus.d.ts +16 -0
- package/lib/typescript/commonjs/permissions/index.d.ts +35 -0
- package/lib/typescript/commonjs/sqlite/adapters/expo.d.ts +30 -0
- package/lib/typescript/commonjs/sqlite/adapters/op.d.ts +27 -0
- package/lib/typescript/commonjs/sqlite/adapters/rows.d.ts +7 -0
- package/lib/typescript/commonjs/sqlite/adapters/storage.d.ts +30 -0
- package/lib/typescript/commonjs/sqlite/adapters/sync.d.ts +31 -0
- package/lib/typescript/commonjs/sqlite/index.d.ts +121 -0
- package/lib/typescript/commonjs/sqlite/nitro.d.ts +7 -0
- package/lib/typescript/commonjs/sqlite/op.d.ts +8 -0
- package/lib/typescript/commonjs/sqlite/quick.d.ts +4 -0
- package/lib/typescript/commonjs/sqlite/storage.d.ts +4 -0
- package/lib/typescript/commonjs/storage/index.d.ts +25 -3
- package/lib/typescript/commonjs/utils/imageCompression.d.ts +17 -0
- package/lib/typescript/commonjs/utils/index.d.ts +2 -2
- 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/retry.d.ts +27 -0
- package/lib/typescript/module/api/userClient.d.ts +11 -0
- package/lib/typescript/module/auth/authStore.d.ts +29 -6
- package/lib/typescript/module/config/index.d.ts +6 -0
- package/lib/typescript/module/deepLink/index.d.ts +54 -0
- package/lib/typescript/module/download/adapters/expoFs.d.ts +45 -0
- package/lib/typescript/module/download/adapters/shared.d.ts +3 -0
- package/lib/typescript/module/download/index.d.ts +76 -0
- package/lib/typescript/module/formatters/index.d.ts +2 -1
- package/lib/typescript/module/formatters/phone.d.ts +27 -1
- package/lib/typescript/module/geolocation/adapters/expoLocation.d.ts +42 -0
- package/lib/typescript/module/geolocation/index.d.ts +89 -0
- package/lib/typescript/module/index.d.ts +30 -13
- package/lib/typescript/module/initSDK.d.ts +32 -0
- package/lib/typescript/module/logger/index.d.ts +39 -0
- package/lib/typescript/module/network/index.d.ts +2 -2
- package/lib/typescript/module/network/networkStatus.d.ts +16 -0
- package/lib/typescript/module/permissions/index.d.ts +35 -0
- package/lib/typescript/module/sqlite/adapters/expo.d.ts +30 -0
- package/lib/typescript/module/sqlite/adapters/op.d.ts +27 -0
- package/lib/typescript/module/sqlite/adapters/rows.d.ts +7 -0
- package/lib/typescript/module/sqlite/adapters/storage.d.ts +30 -0
- package/lib/typescript/module/sqlite/adapters/sync.d.ts +31 -0
- package/lib/typescript/module/sqlite/index.d.ts +121 -0
- package/lib/typescript/module/sqlite/nitro.d.ts +7 -0
- package/lib/typescript/module/sqlite/op.d.ts +8 -0
- package/lib/typescript/module/sqlite/quick.d.ts +4 -0
- package/lib/typescript/module/sqlite/storage.d.ts +4 -0
- package/lib/typescript/module/storage/index.d.ts +25 -3
- package/lib/typescript/module/utils/imageCompression.d.ts +17 -0
- package/lib/typescript/module/utils/index.d.ts +2 -2
- package/package.json +102 -13
package/README.md
CHANGED
|
@@ -9,7 +9,28 @@ version checks. **No UI** — components live in
|
|
|
9
9
|
npm install @webority-technologies/mobile-core
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
External runtime
|
|
12
|
+
External runtime dependency: `axios`, and nothing else. `react-native-keychain` is a REQUIRED
|
|
13
|
+
peer, because the HTTP client reads the bearer token. Every other native peer is optional and lazily
|
|
13
14
|
required, so the package installs and runs without any of them.
|
|
14
15
|
|
|
15
16
|
Full documentation lives in the repository's `docs/` directory.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Import paths
|
|
20
|
+
|
|
21
|
+
Anything needing a native package lives behind its own path, so an app only bundles what it
|
|
22
|
+
imports:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Permissions } from '@webority-technologies/mobile-core/permissions';
|
|
26
|
+
import { Geolocation } from '@webority-technologies/mobile-core/geolocation';
|
|
27
|
+
import { openDatabase } from '@webority-technologies/mobile-core/sqlite';
|
|
28
|
+
import { downloadFile } from '@webority-technologies/mobile-core/download';
|
|
29
|
+
import { compressImage } from '@webority-technologies/mobile-core/compressor';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Metro resolves requires statically, so anything reachable from the main barrel is bundled for
|
|
33
|
+
every consumer. Keeping these off it is what makes their peers genuinely optional.
|
|
34
|
+
|
|
35
|
+
Storage and network status stay on the main barrel but load no package on their own. Wire them at
|
|
36
|
+
startup with `setStorageImplementation` and `setNetworkStatusImplementation` if you want them.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.applyConfiguredBaseUrl = void 0;
|
|
7
|
+
var _index = require("../config/index.js");
|
|
8
|
+
/**
|
|
9
|
+
* Resolves `baseURL` PER REQUEST rather than at module evaluation.
|
|
10
|
+
*
|
|
11
|
+
* `axios.create({ baseURL: getConfigValue('baseUrl') })` reads the value once,
|
|
12
|
+
* when the module is first imported — which happens the moment anything touches
|
|
13
|
+
* the package barrel, long before an app's startup code can call `setConfig`.
|
|
14
|
+
* The result was a client permanently pinned to `undefined` and a `setConfig`
|
|
15
|
+
* that appeared to do nothing, with no error anywhere: requests just went to a
|
|
16
|
+
* relative URL.
|
|
17
|
+
*
|
|
18
|
+
* A consumer cannot fix that by ordering its own imports, because the import
|
|
19
|
+
* that loses the race is usually one it never wrote. So the clients stop
|
|
20
|
+
* capturing the value and ask for it per request instead.
|
|
21
|
+
*
|
|
22
|
+
* An explicit per-request `baseURL` still wins, which is what keeps a one-off
|
|
23
|
+
* call to a different host possible.
|
|
24
|
+
*/
|
|
25
|
+
const applyConfiguredBaseUrl = client => {
|
|
26
|
+
client.interceptors.request.use(config => {
|
|
27
|
+
if (!config.baseURL) {
|
|
28
|
+
config.baseURL = (0, _index.getConfigValue)('baseUrl') ?? undefined;
|
|
29
|
+
}
|
|
30
|
+
return config;
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
exports.applyConfiguredBaseUrl = applyConfiguredBaseUrl;
|
|
34
|
+
//# sourceMappingURL=baseUrl.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.logCurl = exports.buildCurl = void 0;
|
|
7
|
+
var _index = require("../logger/index.js");
|
|
8
|
+
/**
|
|
9
|
+
* Header names whose VALUE is a credential. Matched case-insensitively against
|
|
10
|
+
* the whole name, since `Authorization` and `authorization` are the same header
|
|
11
|
+
* and axios does not normalise the case of headers a caller sets by hand.
|
|
12
|
+
*/
|
|
13
|
+
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']);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Body fields whose value is a credential. Matched on the key alone, so this
|
|
17
|
+
* catches `password`, `newPassword` and `password_confirmation` alike.
|
|
18
|
+
*/
|
|
19
|
+
const SECRET_BODY_PATTERN = /pass(word|code)|secret|token|otp|pin|cvv|authoriz/i;
|
|
20
|
+
|
|
21
|
+
/** The axios per-method header buckets, which are config plumbing, not headers. */
|
|
22
|
+
const AXIOS_METHOD_KEYS = new Set(['common', 'delete', 'get', 'head', 'post', 'put', 'patch']);
|
|
23
|
+
const REDACTED = '<redacted>';
|
|
24
|
+
const redactBody = (value, seen, depth = 0) => {
|
|
25
|
+
if (value === null || typeof value !== 'object') {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
// Both guards are needed and they catch different shapes. `seen` breaks a
|
|
29
|
+
// true cycle, which would otherwise recurse forever AND make the eventual
|
|
30
|
+
// JSON.stringify throw. The depth bound catches a body that is merely
|
|
31
|
+
// enormous. A curl line is a debugging aid, and it must never be able to
|
|
32
|
+
// hang or fail the request it is describing.
|
|
33
|
+
if (seen.has(value)) {
|
|
34
|
+
return '<circular>';
|
|
35
|
+
}
|
|
36
|
+
if (depth > 6) {
|
|
37
|
+
return '<truncated>';
|
|
38
|
+
}
|
|
39
|
+
seen.add(value);
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map(entry => redactBody(entry, seen, depth + 1));
|
|
42
|
+
}
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
45
|
+
out[key] = SECRET_BODY_PATTERN.test(key) ? REDACTED : redactBody(entry, seen, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
};
|
|
49
|
+
const buildUrl = config => {
|
|
50
|
+
const url = config.url ?? '';
|
|
51
|
+
const base = config.baseURL ? config.baseURL.replace(/\/$/, '') : '';
|
|
52
|
+
let complete = url.startsWith('http') ? url : `${base}/${url.replace(/^\//, '')}`;
|
|
53
|
+
if (config.params) {
|
|
54
|
+
const query = new URLSearchParams(config.params).toString();
|
|
55
|
+
if (query) {
|
|
56
|
+
complete += (complete.includes('?') ? '&' : '?') + query;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return complete;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds a copy-pasteable curl command for a request, with every credential
|
|
64
|
+
* replaced by a placeholder.
|
|
65
|
+
*
|
|
66
|
+
* The redaction is the point, not a nicety. An unredacted version of this in a
|
|
67
|
+
* consumer app put `Authorization: Bearer <token>` and the login request's
|
|
68
|
+
* plaintext password into the device console of a *release* staging build.
|
|
69
|
+
* A curl line is worth having; a curl line that has to be trusted not to be
|
|
70
|
+
* enabled in the wrong build is not.
|
|
71
|
+
*/
|
|
72
|
+
const buildCurl = config => {
|
|
73
|
+
const method = (config.method ?? 'get').toUpperCase();
|
|
74
|
+
let command = `curl -X ${method} "${buildUrl(config)}"`;
|
|
75
|
+
const headers = config.headers;
|
|
76
|
+
if (headers) {
|
|
77
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
78
|
+
if (AXIOS_METHOD_KEYS.has(key)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const shown = SECRET_HEADERS.has(key.toLowerCase()) ? REDACTED : value;
|
|
85
|
+
command += ` -H "${key}: ${shown}"`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (config.data !== undefined && config.data !== null) {
|
|
89
|
+
const redacted = typeof config.data === 'object' ? redactBody(config.data, new WeakSet()) : config.data;
|
|
90
|
+
const body = typeof redacted === 'object' ? JSON.stringify(redacted) : String(redacted);
|
|
91
|
+
command += ` -d '${body.replace(/'/g, "'\\''")}'`;
|
|
92
|
+
}
|
|
93
|
+
return command;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Logs the curl equivalent of a request. Never throws: a failure to describe a
|
|
98
|
+
* request must not be able to fail the request itself.
|
|
99
|
+
*/
|
|
100
|
+
exports.buildCurl = buildCurl;
|
|
101
|
+
const logCurl = config => {
|
|
102
|
+
try {
|
|
103
|
+
_index.Logger.info('CURL Command:', buildCurl(config));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
_index.Logger.warn('[api] Failed to build a curl command for the request', error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
exports.logCurl = logCurl;
|
|
109
|
+
//# sourceMappingURL=curl.js.map
|
|
@@ -5,9 +5,11 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _axios = _interopRequireDefault(require("axios"));
|
|
8
|
-
var _axiosRetry = _interopRequireDefault(require("axios-retry"));
|
|
9
8
|
var _index = require("../config/index.js");
|
|
10
9
|
var _index2 = require("../logger/index.js");
|
|
10
|
+
var _baseUrl = require("./baseUrl.js");
|
|
11
|
+
var _curl = require("./curl.js");
|
|
12
|
+
var _retry = require("./retry.js");
|
|
11
13
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
14
|
// Define timeout for requests
|
|
13
15
|
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
@@ -20,7 +22,6 @@ const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
|
20
22
|
* - Handles specific HTTP status codes.
|
|
21
23
|
*/
|
|
22
24
|
const publicClient = _axios.default.create({
|
|
23
|
-
baseURL: (0, _index.getConfigValue)('baseUrl') ?? undefined,
|
|
24
25
|
headers: {
|
|
25
26
|
'Content-Type': 'application/json',
|
|
26
27
|
Accept: 'application/json',
|
|
@@ -28,29 +29,19 @@ const publicClient = _axios.default.create({
|
|
|
28
29
|
},
|
|
29
30
|
timeout: REQUEST_TIMEOUT
|
|
30
31
|
});
|
|
32
|
+
(0, _baseUrl.applyConfiguredBaseUrl)(publicClient);
|
|
31
33
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Immediate retry for the first attempt
|
|
37
|
-
retryCondition: error => {
|
|
38
|
-
// Status-based ONLY, deliberately: a bare network error must never be
|
|
39
|
-
// retried here. This client carries login and OTP, which are not
|
|
40
|
-
// idempotent — the request may well have reached the server before the
|
|
41
|
-
// connection broke, so a retry can create a second session the user never
|
|
42
|
-
// sees. axios-retry's isNetworkOrIdempotentRequestError does NOT protect
|
|
43
|
-
// against this: its isNetworkError branch never looks at the HTTP method,
|
|
44
|
-
// so it retried POSTs too.
|
|
45
|
-
const status = error.response?.status;
|
|
46
|
-
return typeof status === 'number' && (status === 429 || status >= 500 && status <= 599);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
(0, _axiosRetry.default)(publicClient, retryConfig);
|
|
34
|
+
// Registered before the logging interceptors below so they only ever observe
|
|
35
|
+
// the final outcome of a request, not its intermediate retries. The policy
|
|
36
|
+
// itself, and why a bare network error is never retried, lives in ./retry.
|
|
37
|
+
(0, _retry.attachRetry)(publicClient);
|
|
50
38
|
|
|
51
39
|
// Add request interceptor for logging
|
|
52
40
|
publicClient.interceptors.request.use(config => {
|
|
53
41
|
_index2.Logger.info(`Request: ${config.method?.toUpperCase()} ${config.url}`);
|
|
42
|
+
if ((0, _index.getConfigValue)('logCurl')) {
|
|
43
|
+
(0, _curl.logCurl)(config);
|
|
44
|
+
}
|
|
54
45
|
return config;
|
|
55
46
|
}, error => {
|
|
56
47
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.isRetryableStatus = exports.attachRetry = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Status-based ONLY, deliberately: a bare network error (no `error.response`)
|
|
9
|
+
* is never retryable. Both clients carry non-idempotent requests, login and OTP
|
|
10
|
+
* on the public one and every write on the user one, and a broken connection
|
|
11
|
+
* says nothing about whether the request reached the server. Retrying can
|
|
12
|
+
* therefore create a second session, or a duplicate record, that the user never
|
|
13
|
+
* sees. A generic network-error retry rule is not a substitute: it decides on
|
|
14
|
+
* the transport alone and never looks at the HTTP method, so it retries POSTs
|
|
15
|
+
* as readily as GETs.
|
|
16
|
+
*/
|
|
17
|
+
const isRetryableStatus = status => typeof status === 'number' && (status === 429 || status >= 500 && status <= 599);
|
|
18
|
+
exports.isRetryableStatus = isRetryableStatus;
|
|
19
|
+
const DEFAULT_POLICY = {
|
|
20
|
+
retries: 3,
|
|
21
|
+
retryDelayMs: attempt => attempt <= 1 ? 0 : 2 ** attempt * 1000,
|
|
22
|
+
shouldRetry: error => isRetryableStatus(error.response?.status),
|
|
23
|
+
onRetry: () => undefined
|
|
24
|
+
};
|
|
25
|
+
const waitOrAbort = (delay, signal) => new Promise(resolve => {
|
|
26
|
+
const onAbort = () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
resolve(false);
|
|
29
|
+
};
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
32
|
+
resolve(true);
|
|
33
|
+
}, delay);
|
|
34
|
+
signal?.addEventListener?.('abort', onAbort, {
|
|
35
|
+
once: true
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const attachRetry = (client, policy = {}) => {
|
|
39
|
+
client.interceptors.response.use(undefined, async error => {
|
|
40
|
+
const axiosError = error;
|
|
41
|
+
const config = axiosError?.config;
|
|
42
|
+
if (!config || config.retry === false) {
|
|
43
|
+
return Promise.reject(error);
|
|
44
|
+
}
|
|
45
|
+
const perRequest = typeof config.retry === 'object' ? config.retry : {};
|
|
46
|
+
const effective = {
|
|
47
|
+
...DEFAULT_POLICY,
|
|
48
|
+
...policy,
|
|
49
|
+
...perRequest
|
|
50
|
+
};
|
|
51
|
+
const attempt = (config.__retryAttempt ?? 0) + 1;
|
|
52
|
+
if (attempt > effective.retries || !effective.shouldRetry(axiosError)) {
|
|
53
|
+
return Promise.reject(error);
|
|
54
|
+
}
|
|
55
|
+
if (config.signal?.aborted) {
|
|
56
|
+
return Promise.reject(error);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The attempt count rides on the config, which axios copies onto the config
|
|
60
|
+
// of the retried request, so nested attempts continue the same count and
|
|
61
|
+
// separate requests never share one.
|
|
62
|
+
config.__retryAttempt = attempt;
|
|
63
|
+
// The body was serialised by the first pass; transforming it again would
|
|
64
|
+
// JSON-encode the already-encoded string.
|
|
65
|
+
config.transformRequest = [data => data];
|
|
66
|
+
effective.onRetry(attempt, axiosError);
|
|
67
|
+
const proceed = await waitOrAbort(effective.retryDelayMs(attempt), config.signal);
|
|
68
|
+
if (!proceed) {
|
|
69
|
+
return Promise.reject(error);
|
|
70
|
+
}
|
|
71
|
+
return client.request(config);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
exports.attachRetry = attachRetry;
|
|
75
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.injectStore = exports.injectAuthActions = exports.default = exports.SESSION_TERMINATION_CODES = exports.SESSION_INVALID_HEADER = exports.RequestOfflineError = void 0;
|
|
6
|
+
exports.resetRefreshState = exports.injectStore = exports.injectAuthActions = exports.default = exports.SESSION_TERMINATION_CODES = exports.SESSION_INVALID_HEADER = exports.RequestOfflineError = void 0;
|
|
7
7
|
var _axios = _interopRequireWildcard(require("axios"));
|
|
8
|
-
var _axiosRetry = _interopRequireDefault(require("axios-retry"));
|
|
9
8
|
var _authStore = require("../auth/authStore.js");
|
|
10
9
|
var _index = require("../config/index.js");
|
|
11
10
|
var _index2 = require("../logger/index.js");
|
|
12
11
|
var _networkStatus = require("../network/networkStatus.js");
|
|
13
|
-
|
|
12
|
+
var _baseUrl = require("./baseUrl.js");
|
|
13
|
+
var _curl = require("./curl.js");
|
|
14
|
+
var _retry = require("./retry.js");
|
|
14
15
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
15
16
|
// Define timeout for requests
|
|
16
17
|
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
@@ -85,7 +86,6 @@ const getSessionTerminationCode = data => {
|
|
|
85
86
|
*/
|
|
86
87
|
|
|
87
88
|
const userClient = _axios.default.create({
|
|
88
|
-
baseURL: (0, _index.getConfigValue)('baseUrl') ?? undefined,
|
|
89
89
|
headers: {
|
|
90
90
|
'Content-Type': 'application/json',
|
|
91
91
|
'Accept-Encoding': 'gzip, deflate, br'
|
|
@@ -93,19 +93,10 @@ const userClient = _axios.default.create({
|
|
|
93
93
|
timeout: REQUEST_TIMEOUT // Set timeout for all requests
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// Immediate retry for the first attempt
|
|
101
|
-
retryCondition: error => {
|
|
102
|
-
// Status-based ONLY — see publicClient for the reasoning. A bare network
|
|
103
|
-
// error carries no response, so it is not retried: the write may already
|
|
104
|
-
// have landed server-side, and this client issues non-idempotent requests.
|
|
105
|
-
const status = error.response?.status;
|
|
106
|
-
return typeof status === 'number' && (status === 429 || status >= 500 && status <= 599);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
96
|
+
// Registered before the auth interceptors below, so a retryable 5xx is retried
|
|
97
|
+
// before anything else sees it while a 401 still falls through to the refresh
|
|
98
|
+
// flow. Policy and reasoning live in ./retry.
|
|
99
|
+
(0, _retry.attachRetry)(userClient);
|
|
109
100
|
let store;
|
|
110
101
|
let authActions = {
|
|
111
102
|
logout: () => undefined,
|
|
@@ -145,6 +136,22 @@ const processQueue = (error, token = null) => {
|
|
|
145
136
|
});
|
|
146
137
|
failedQueue = [];
|
|
147
138
|
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear any in-flight token refresh and reject everything queued behind it.
|
|
142
|
+
* Call this on logout.
|
|
143
|
+
*
|
|
144
|
+
* Without it, a refresh that was already in flight when the user logged out
|
|
145
|
+
* resolves afterwards and replays every queued request with a token for a
|
|
146
|
+
* session that has ended. The queue is module state, so it survives the logout
|
|
147
|
+
* entirely: nothing else clears it. A consumer app hit this and hand-wrote the
|
|
148
|
+
* same function locally, which is how it was found.
|
|
149
|
+
*/
|
|
150
|
+
const resetRefreshState = () => {
|
|
151
|
+
refreshTokenPromise = null;
|
|
152
|
+
processQueue(new Error('[@webority-technologies/mobile-core] Session ended'));
|
|
153
|
+
};
|
|
154
|
+
exports.resetRefreshState = resetRefreshState;
|
|
148
155
|
const getErrorMessage = error => {
|
|
149
156
|
if (error instanceof Error) {
|
|
150
157
|
return error.message;
|
|
@@ -175,6 +182,23 @@ const requireStore = () => {
|
|
|
175
182
|
/**
|
|
176
183
|
* Attach Authorization Token Before Requests
|
|
177
184
|
*/
|
|
185
|
+
// Registered after the curl logger and before the auth interceptor, so that it
|
|
186
|
+
// RUNS between them: axios unshifts its request chain, making the first
|
|
187
|
+
// registered run last. The curl line therefore describes a request that already
|
|
188
|
+
// has both its baseURL and its Authorization header.
|
|
189
|
+
(0, _baseUrl.applyConfiguredBaseUrl)(userClient);
|
|
190
|
+
|
|
191
|
+
// Registered BEFORE the auth interceptor below deliberately. Axios builds its
|
|
192
|
+
// request chain by unshifting, so the FIRST-registered request interceptor runs
|
|
193
|
+
// LAST — closest to dispatch. This one therefore observes the final config,
|
|
194
|
+
// with the Authorization header already attached, which is the whole point of
|
|
195
|
+
// a curl line. Registering it after would log the request before it is signed.
|
|
196
|
+
userClient.interceptors.request.use(config => {
|
|
197
|
+
if ((0, _index.getConfigValue)('logCurl')) {
|
|
198
|
+
(0, _curl.logCurl)(config);
|
|
199
|
+
}
|
|
200
|
+
return config;
|
|
201
|
+
});
|
|
178
202
|
userClient.interceptors.request.use(async config => {
|
|
179
203
|
// Reject before any network call when the device is known to be offline.
|
|
180
204
|
// Unknown connectivity (netinfo not installed, or undetermined) is treated
|
|
@@ -189,7 +213,7 @@ userClient.interceptors.request.use(async config => {
|
|
|
189
213
|
if (config.url?.includes('/Account/RefreshToken')) {
|
|
190
214
|
return config;
|
|
191
215
|
}
|
|
192
|
-
const token = await (0, _authStore.getToken)(
|
|
216
|
+
const token = await (0, _authStore.getToken)(_authStore.ACCESS_TOKEN_KEY);
|
|
193
217
|
|
|
194
218
|
// Check if token exists
|
|
195
219
|
if (!token?.token) {
|
|
@@ -221,7 +245,7 @@ userClient.interceptors.request.use(async config => {
|
|
|
221
245
|
// Start a new token refresh process
|
|
222
246
|
refreshTokenPromise = (async () => {
|
|
223
247
|
try {
|
|
224
|
-
const refreshToken = await (0, _authStore.getToken)(
|
|
248
|
+
const refreshToken = await (0, _authStore.getToken)(_authStore.REFRESH_TOKEN_KEY);
|
|
225
249
|
if (!refreshToken?.token) {
|
|
226
250
|
_index2.Logger.error('No refresh token available');
|
|
227
251
|
throw new Error('No refresh token available');
|
|
@@ -239,7 +263,7 @@ userClient.interceptors.request.use(async config => {
|
|
|
239
263
|
if (refreshResult.error) {
|
|
240
264
|
throw new Error(`Token refresh failed: ${refreshResult.error.message || 'Unknown error'}`);
|
|
241
265
|
}
|
|
242
|
-
const newAccessToken = await (0, _authStore.getToken)(
|
|
266
|
+
const newAccessToken = await (0, _authStore.getToken)(_authStore.ACCESS_TOKEN_KEY);
|
|
243
267
|
if (!newAccessToken?.token) {
|
|
244
268
|
throw new Error('Failed to retrieve new access token after refresh');
|
|
245
269
|
}
|
|
@@ -322,7 +346,7 @@ userClient.interceptors.response.use(response => response, async error => {
|
|
|
322
346
|
refreshTokenPromise = (async () => {
|
|
323
347
|
try {
|
|
324
348
|
_index2.Logger.info('User Client: Refreshing token...');
|
|
325
|
-
const refreshToken = await (0, _authStore.getToken)(
|
|
349
|
+
const refreshToken = await (0, _authStore.getToken)(_authStore.REFRESH_TOKEN_KEY);
|
|
326
350
|
if (!refreshToken || !refreshToken.token) {
|
|
327
351
|
_index2.Logger.error('No refresh token available or token is invalid');
|
|
328
352
|
throw new Error('No refresh token available');
|
|
@@ -335,7 +359,7 @@ userClient.interceptors.response.use(response => response, async error => {
|
|
|
335
359
|
throw new Error(`Token refresh failed: ${refreshResult.error.message || 'Unknown error'}`);
|
|
336
360
|
}
|
|
337
361
|
_index2.Logger.info('Getting new access token after refresh');
|
|
338
|
-
const newAccessToken = await (0, _authStore.getToken)(
|
|
362
|
+
const newAccessToken = await (0, _authStore.getToken)(_authStore.ACCESS_TOKEN_KEY);
|
|
339
363
|
if (!newAccessToken?.token) {
|
|
340
364
|
throw new Error('Failed to retrieve new access token');
|
|
341
365
|
}
|
|
@@ -3,39 +3,85 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.storeToken = exports.setKeychainImplementation = exports.removeToken = exports.getToken = void 0;
|
|
6
|
+
exports.storeToken = exports.setKeychainImplementation = exports.removeToken = exports.getToken = exports.REFRESH_TOKEN_KEY = exports.ACCESS_TOKEN_KEY = void 0;
|
|
7
|
+
var SecureStore = _interopRequireWildcard(require("expo-secure-store"));
|
|
7
8
|
var _index = require("../logger/index.js");
|
|
9
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
8
10
|
/**
|
|
9
11
|
* Token data persisted in the keychain.
|
|
10
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* The two keys the library itself reads. Everything in the library that needs
|
|
15
|
+
* the bearer token, the HTTP client, the download module and the authenticated
|
|
16
|
+
* image cache, resolves it through these rather than repeating the literal, so
|
|
17
|
+
* one of them cannot silently drift and start reading a key nothing writes.
|
|
18
|
+
* Apps are free to store their own tokens under any other key.
|
|
19
|
+
*/
|
|
20
|
+
const ACCESS_TOKEN_KEY = exports.ACCESS_TOKEN_KEY = 'accessToken';
|
|
21
|
+
const REFRESH_TOKEN_KEY = exports.REFRESH_TOKEN_KEY = 'refreshToken';
|
|
11
22
|
|
|
12
23
|
/**
|
|
13
24
|
* Minimal keychain shape we depend on.
|
|
14
25
|
* Lets us avoid a hard import on `react-native-keychain`, so an app that never
|
|
15
26
|
* stores a token does not have to install it.
|
|
16
27
|
*/
|
|
28
|
+
/**
|
|
29
|
+
* The GENERIC password API, not the internet-credentials one.
|
|
30
|
+
*
|
|
31
|
+
* `kSecClassInternetPassword` exists for credentials bound to a server, protocol
|
|
32
|
+
* and port, and it integrates with Safari and AutoFill. A bearer token keyed by
|
|
33
|
+
* a name like "accessToken" is none of those things, and the library was only
|
|
34
|
+
* satisfying that API's required `server` argument by passing the key twice,
|
|
35
|
+
* once as the server and once as the service. That is misuse of the class.
|
|
36
|
+
*
|
|
37
|
+
* `kSecClassGenericPassword` is the class for app-local secrets, which is what a
|
|
38
|
+
* token is. It is what expo-secure-store uses underneath, and what the consumer
|
|
39
|
+
* app being migrated had already chosen for itself.
|
|
40
|
+
*/
|
|
17
41
|
|
|
42
|
+
// Only what a test or the showcase injected; null means use expo-secure-store.
|
|
18
43
|
let backend = null;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* expo-secure-store, adapted to the generic-password shape this module speaks.
|
|
47
|
+
*
|
|
48
|
+
* It is a flat key/value store, so the credential shape is reconstructed around
|
|
49
|
+
* it. The key is sanitised because expo rejects anything outside
|
|
50
|
+
* [A-Za-z0-9._-] at runtime, which would otherwise surface as a throw from deep
|
|
51
|
+
* inside the store rather than as a bad key.
|
|
52
|
+
*/
|
|
53
|
+
const keyFor = service => (service ?? 'default').replace(/[^A-Za-z0-9._-]/g, '_');
|
|
54
|
+
const expoSecureStoreBackend = {
|
|
55
|
+
setGenericPassword: async (username, password, options) => {
|
|
56
|
+
await SecureStore.setItemAsync(keyFor(options?.service), JSON.stringify({
|
|
57
|
+
username,
|
|
58
|
+
password
|
|
59
|
+
}));
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
getGenericPassword: async options => {
|
|
63
|
+
const service = options?.service;
|
|
64
|
+
const raw = await SecureStore.getItemAsync(keyFor(service));
|
|
65
|
+
if (raw === null) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
return {
|
|
70
|
+
username: parsed.username,
|
|
71
|
+
password: parsed.password,
|
|
72
|
+
service: service ?? 'default'
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
resetGenericPassword: async options => {
|
|
76
|
+
await SecureStore.deleteItemAsync(keyFor(options?.service));
|
|
77
|
+
return true;
|
|
32
78
|
}
|
|
33
|
-
return backend;
|
|
34
79
|
};
|
|
80
|
+
const resolveBackend = () => backend ?? expoSecureStoreBackend;
|
|
35
81
|
const requireBackend = op => {
|
|
36
82
|
const b = resolveBackend();
|
|
37
83
|
if (!b) {
|
|
38
|
-
const err = new Error(`[authStore]
|
|
84
|
+
const err = new Error(`[authStore] no secure-store backend available; cannot ${op}.`);
|
|
39
85
|
_index.Logger.error(err.message);
|
|
40
86
|
throw err;
|
|
41
87
|
}
|
|
@@ -43,12 +89,12 @@ const requireBackend = op => {
|
|
|
43
89
|
};
|
|
44
90
|
|
|
45
91
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
92
|
+
* Replace the secure-store backend. Intended for TESTS and for the showcase,
|
|
93
|
+
* which injects a failing store to demo the write-failure path; it is not how
|
|
94
|
+
* an app configures the library. Pass `null` to restore expo-secure-store.
|
|
48
95
|
*/
|
|
49
96
|
const setKeychainImplementation = impl => {
|
|
50
97
|
backend = impl;
|
|
51
|
-
resolved = impl !== null;
|
|
52
98
|
};
|
|
53
99
|
|
|
54
100
|
/**
|
|
@@ -64,9 +110,9 @@ const storeToken = async (key, token, expiry) => {
|
|
|
64
110
|
token,
|
|
65
111
|
expiry
|
|
66
112
|
});
|
|
67
|
-
await requireBackend('store a token').
|
|
113
|
+
await requireBackend('store a token').setGenericPassword('token', data, {
|
|
68
114
|
service: key
|
|
69
|
-
});
|
|
115
|
+
});
|
|
70
116
|
_index.Logger.info(`Token stored successfully for key: ${key}`);
|
|
71
117
|
} catch (error) {
|
|
72
118
|
// Log AND rethrow. Swallowing this told the caller the token was persisted
|
|
@@ -95,8 +141,10 @@ const parseTokenData = credentials => {
|
|
|
95
141
|
return parsed;
|
|
96
142
|
}
|
|
97
143
|
return null;
|
|
98
|
-
} catch
|
|
99
|
-
|
|
144
|
+
} catch {
|
|
145
|
+
// The single report for "stored, but not a token" belongs to getToken, which
|
|
146
|
+
// knows whether anything was stored at all. Logging here too made one event
|
|
147
|
+
// arrive twice, at two different levels.
|
|
100
148
|
return null;
|
|
101
149
|
}
|
|
102
150
|
};
|
|
@@ -105,31 +153,43 @@ const parseTokenData = credentials => {
|
|
|
105
153
|
* Retrieve token and check if expired.
|
|
106
154
|
*/
|
|
107
155
|
const getToken = async key => {
|
|
108
|
-
_index.Logger.info(`Retrieving token for key: ${key}`);
|
|
109
156
|
try {
|
|
110
|
-
const credentials = await requireBackend('read a token').
|
|
157
|
+
const credentials = await requireBackend('read a token').getGenericPassword({
|
|
111
158
|
service: key
|
|
112
|
-
});
|
|
159
|
+
});
|
|
160
|
+
// Nothing stored is an ORDINARY state, not a fault: a signed-out user has no
|
|
161
|
+
// token, and every authenticated request, image and download asks for one.
|
|
162
|
+
// Warning here put three info lines and a warning into the log, and into the
|
|
163
|
+
// remote crash sink, on every one of those calls.
|
|
164
|
+
if (!credentials) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
113
167
|
const parsedData = parseTokenData(credentials);
|
|
114
|
-
_index.Logger.info(`Parsed token data for key: ${key}: ${parsedData ? 'present' : 'absent'}`);
|
|
115
168
|
if (!parsedData) {
|
|
116
|
-
|
|
169
|
+
// Something IS stored and it is not a token. That is corruption worth
|
|
170
|
+
// hearing about, and it is a different event from having no token.
|
|
171
|
+
_index.Logger.warn(`Stored credentials for key "${key}" are not a valid token; ignoring them.`);
|
|
117
172
|
return null;
|
|
118
173
|
}
|
|
119
174
|
const {
|
|
120
175
|
token,
|
|
121
176
|
expiry
|
|
122
177
|
} = parsedData;
|
|
123
|
-
_index.Logger.info(`Token retrieved for key: ${key}`);
|
|
124
178
|
return {
|
|
125
179
|
token,
|
|
126
180
|
expiry
|
|
127
181
|
};
|
|
128
182
|
} catch (error) {
|
|
129
183
|
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
184
|
+
// Both spellings matter. Android throws "Key permanently invalidated" when
|
|
185
|
+
// the biometric enrolment changes; iOS throws "Secret invalidated" for the
|
|
186
|
+
// equivalent. Either way the stored value can never be decrypted again, so
|
|
187
|
+
// clearing it is the only way out: leaving it makes every later read fail
|
|
188
|
+
// identically, and the user is stuck with no route back to a login. A
|
|
189
|
+
// consumer app handled the iOS spelling and the library did not.
|
|
190
|
+
if (message.includes('Key permanently invalidated') || message.includes('Secret invalidated')) {
|
|
191
|
+
_index.Logger.error(`Stored token for key "${key}" can no longer be decrypted; clearing it.`);
|
|
192
|
+
await removeToken(key);
|
|
133
193
|
return null;
|
|
134
194
|
}
|
|
135
195
|
_index.Logger.error(`Error retrieving token for key: ${key}`, error);
|
|
@@ -143,11 +203,19 @@ const getToken = async key => {
|
|
|
143
203
|
exports.getToken = getToken;
|
|
144
204
|
const removeToken = async key => {
|
|
145
205
|
try {
|
|
146
|
-
await requireBackend('remove a token').
|
|
206
|
+
await requireBackend('remove a token').resetGenericPassword({
|
|
147
207
|
service: key
|
|
148
208
|
});
|
|
149
209
|
_index.Logger.info(`Token removed successfully for key: ${key}`);
|
|
150
210
|
} catch (error) {
|
|
211
|
+
// Clearing a key that was never written is the ordinary logout path, not a
|
|
212
|
+
// failure: only one of the two tokens may ever have been stored. Logging it
|
|
213
|
+
// as an error puts routine traffic in the channel people watch for real
|
|
214
|
+
// problems, which is how an error channel stops being watched.
|
|
215
|
+
if (error instanceof Error && error.message.includes('Item not found')) {
|
|
216
|
+
_index.Logger.info(`No token found to remove for key: ${key}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
151
219
|
_index.Logger.error(`Failed to remove token for key: ${key}`, error);
|
|
152
220
|
}
|
|
153
221
|
};
|