@webority/mobile-core 0.0.24 → 0.0.26

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.
Files changed (34) hide show
  1. package/LICENSE.txt +9 -0
  2. package/README.md +13 -0
  3. package/lib/commonjs/api/multipart.js +21 -0
  4. package/lib/commonjs/api/publicClient.js +6 -0
  5. package/lib/commonjs/api/userClient.js +12 -2
  6. package/lib/commonjs/index.js +7 -0
  7. package/lib/commonjs/logger/index.js +16 -6
  8. package/lib/commonjs/logger/redact.js +50 -0
  9. package/lib/commonjs/push/push.js +3 -2
  10. package/lib/commonjs/types/axios.js +4 -0
  11. package/lib/module/api/multipart.js +16 -0
  12. package/lib/module/api/publicClient.js +6 -0
  13. package/lib/module/api/userClient.js +12 -2
  14. package/lib/module/index.js +1 -0
  15. package/lib/module/logger/index.js +16 -6
  16. package/lib/module/logger/redact.js +44 -0
  17. package/lib/module/push/push.js +3 -2
  18. package/lib/module/types/axios.js +4 -0
  19. package/lib/typescript/commonjs/api/multipart.d.ts +8 -0
  20. package/lib/typescript/commonjs/api/publicClient.d.ts +1 -0
  21. package/lib/typescript/commonjs/api/userClient.d.ts +1 -0
  22. package/lib/typescript/commonjs/index.d.ts +1 -0
  23. package/lib/typescript/commonjs/logger/redact.d.ts +4 -0
  24. package/lib/typescript/commonjs/push/push.d.ts +2 -0
  25. package/lib/typescript/commonjs/types/axios.d.ts +8 -0
  26. package/lib/typescript/module/api/multipart.d.ts +8 -0
  27. package/lib/typescript/module/api/publicClient.d.ts +1 -0
  28. package/lib/typescript/module/api/userClient.d.ts +1 -0
  29. package/lib/typescript/module/index.d.ts +1 -0
  30. package/lib/typescript/module/logger/redact.d.ts +4 -0
  31. package/lib/typescript/module/push/push.d.ts +2 -0
  32. package/lib/typescript/module/types/axios.d.ts +8 -0
  33. package/package.json +2 -2
  34. package/LICENSE +0 -21
package/LICENSE.txt ADDED
@@ -0,0 +1,9 @@
1
+ Copyright (c) Webority Technologies. All rights reserved.
2
+
3
+ This software and its NuGet packages are the proprietary and confidential property
4
+ of Webority Technologies. They are published publicly only for the convenience of
5
+ Webority's own projects and internal consumption.
6
+
7
+ No license or right to use, copy, modify, merge, publish, distribute, sublicense,
8
+ or create derivative works is granted to any third party. Unauthorized use,
9
+ reproduction, or distribution is prohibited.
package/README.md CHANGED
@@ -47,3 +47,16 @@ keeps push on calls `enablePush` at each start.
47
47
 
48
48
  Storage and network status stay on the main barrel but load no package on their own. Wire them at
49
49
  startup with `setStorageImplementation` and `setNetworkStatusImplementation` if you want them.
50
+
51
+ ## Quiet requests and log redaction
52
+
53
+ Both HTTP clients log a failed response through `Logger.error`. For a call whose failure the app
54
+ handles itself, pass `skipErrorLog: true` in the axios config and the clients stay silent:
55
+
56
+ ```ts
57
+ await userClient.get('/status', { skipErrorLog: true });
58
+ ```
59
+
60
+ `Logger` masks secrets in every message and argument before any sink sees them: `Bearer` tokens,
61
+ JWT-shaped strings, and the values of keys named `token`, `accessToken`, `refreshToken`,
62
+ `password` and `authorization` (any case) in objects and JSON strings.
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.sendFormDataAsMultipart = void 0;
7
+ /**
8
+ * The clients default to a JSON content type, and axios then serialises a FormData body to
9
+ * JSON, which breaks every upload. Dropping the header for FormData lets the platform write
10
+ * the multipart header with its boundary.
11
+ */
12
+ const sendFormDataAsMultipart = client => {
13
+ client.interceptors.request.use(config => {
14
+ if (typeof FormData !== 'undefined' && config.data instanceof FormData) {
15
+ config.headers?.delete?.('Content-Type');
16
+ }
17
+ return config;
18
+ });
19
+ };
20
+ exports.sendFormDataAsMultipart = sendFormDataAsMultipart;
21
+ //# sourceMappingURL=multipart.js.map
@@ -7,8 +7,10 @@ exports.default = void 0;
7
7
  var _axios = _interopRequireDefault(require("axios"));
8
8
  var _index = require("../config/index.js");
9
9
  var _index2 = require("../logger/index.js");
10
+ require("../types/axios.js");
10
11
  var _baseUrl = require("./baseUrl.js");
11
12
  var _curl = require("./curl.js");
13
+ var _multipart = require("./multipart.js");
12
14
  var _retry = require("./retry.js");
13
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
16
  // Define timeout for requests
@@ -30,6 +32,7 @@ const publicClient = _axios.default.create({
30
32
  timeout: REQUEST_TIMEOUT
31
33
  });
32
34
  (0, _baseUrl.applyConfiguredBaseUrl)(publicClient);
35
+ (0, _multipart.sendFormDataAsMultipart)(publicClient);
33
36
 
34
37
  // Registered before the logging interceptors below so they only ever observe
35
38
  // the final outcome of a request, not its intermediate retries. The policy
@@ -56,6 +59,9 @@ publicClient.interceptors.response.use(response => {
56
59
  }, error => {
57
60
  if (_axios.default.isAxiosError(error)) {
58
61
  const originalRequest = error.config;
62
+ if (originalRequest?.skipErrorLog) {
63
+ return Promise.reject(error);
64
+ }
59
65
  if (!error.response) {
60
66
  _index2.Logger.error(`Network error or no response from server: ${error.message}, URL: ${originalRequest?.url}`);
61
67
  } else {
@@ -27,8 +27,10 @@ var _authStore = require("../auth/authStore.js");
27
27
  var _index = require("../config/index.js");
28
28
  var _index2 = require("../logger/index.js");
29
29
  var _networkStatus = require("../network/networkStatus.js");
30
+ require("../types/axios.js");
30
31
  var _baseUrl = require("./baseUrl.js");
31
32
  var _curl = require("./curl.js");
33
+ var _multipart = require("./multipart.js");
32
34
  var _retry = require("./retry.js");
33
35
  var _tokenRefresh = require("./tokenRefresh.js");
34
36
  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); }
@@ -45,6 +47,9 @@ class RequestOfflineError extends Error {
45
47
  constructor(message = 'Request blocked: device is offline') {
46
48
  super(message);
47
49
  this.name = 'RequestOfflineError';
50
+ // Babel's class transform can drop the prototype link, which makes
51
+ // `instanceof RequestOfflineError` false in the transpiled build.
52
+ Object.setPrototypeOf(this, RequestOfflineError.prototype);
48
53
  }
49
54
  }
50
55
 
@@ -135,6 +140,7 @@ const isRefreshTokenRequest = url => {
135
140
  // registered run last. The curl line therefore describes a request that already
136
141
  // has both its baseURL and its Authorization header.
137
142
  (0, _baseUrl.applyConfiguredBaseUrl)(userClient);
143
+ (0, _multipart.sendFormDataAsMultipart)(userClient);
138
144
 
139
145
  // Registered BEFORE the auth interceptor below deliberately. Axios builds its
140
146
  // request chain by unshifting, so the FIRST-registered request interceptor runs
@@ -206,7 +212,9 @@ userClient.interceptors.response.use(response => response, async error => {
206
212
  const axiosError = error;
207
213
  const originalRequest = axiosError.config;
208
214
  if (!axiosError.response) {
209
- _index2.Logger.error(`Network error or no response from server: ${axiosError.message}, URL: ${originalRequest?.url}`);
215
+ if (!originalRequest?.skipErrorLog) {
216
+ _index2.Logger.error(`Network error or no response from server: ${axiosError.message}, URL: ${originalRequest?.url}`);
217
+ }
210
218
  return Promise.reject(error);
211
219
  }
212
220
  if (axiosError.response.status === 401 && originalRequest && !originalRequest._retry) {
@@ -248,7 +256,9 @@ userClient.interceptors.response.use(response => response, async error => {
248
256
  return Promise.reject(err);
249
257
  }
250
258
  }
251
- _index2.Logger.error(`Response error: ${axiosError.response?.data ?? axiosError.message}, URL: ${originalRequest?.url}, Status: ${axiosError.response.status}`);
259
+ if (!originalRequest?.skipErrorLog) {
260
+ _index2.Logger.error(`Response error: ${axiosError.response?.data ?? axiosError.message}, URL: ${originalRequest?.url}, Status: ${axiosError.response.status}`);
261
+ }
252
262
  return Promise.reject(error);
253
263
  });
254
264
  var _default = exports.default = userClient;
@@ -501,6 +501,12 @@ Object.defineProperty(exports, "publicClient", {
501
501
  return _publicClient.default;
502
502
  }
503
503
  });
504
+ Object.defineProperty(exports, "refreshAccessToken", {
505
+ enumerable: true,
506
+ get: function () {
507
+ return _tokenRefresh.refreshAccessToken;
508
+ }
509
+ });
504
510
  Object.defineProperty(exports, "removeToken", {
505
511
  enumerable: true,
506
512
  get: function () {
@@ -612,6 +618,7 @@ Object.defineProperty(exports, "validatePhoneNumberLength", {
612
618
  var _curl = require("./api/curl.js");
613
619
  var _publicClient = _interopRequireDefault(require("./api/publicClient.js"));
614
620
  var _retry = require("./api/retry.js");
621
+ var _tokenRefresh = require("./api/tokenRefresh.js");
615
622
  var _userClient = _interopRequireWildcard(require("./api/userClient.js"));
616
623
  var _authStore = require("./auth/authStore.js");
617
624
  var _jwt = require("./auth/jwt.js");
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.setRemoteLogger = exports.setDeviceContext = exports.getDeviceContext = exports.configureLogger = exports.Logger = void 0;
7
7
  var _index = require("../config/index.js");
8
+ var _redact = require("./redact.js");
8
9
  // ANSI color codes
9
10
  const Colors = {
10
11
  RESET: '\x1b[0m',
@@ -209,21 +210,27 @@ const forwardErrorToRemote = (err, context) => {
209
210
  }
210
211
  safe(() => remote?.recordError?.(err, buildErrorContext(context, suppressedCount)));
211
212
  };
212
- const logInfo = (message, ...optionalParams) => {
213
+ const logInfo = (rawMessage, ...rawParams) => {
214
+ const message = (0, _redact.redactString)(rawMessage);
215
+ const optionalParams = (0, _redact.redact)(rawParams);
213
216
  if (!isEnabled() || !isLogLevelAllowed(LogLevels.INFO.level)) {
214
217
  return;
215
218
  }
216
219
  console.info(`${LogLevels.INFO.color}[INFO]: ${message}${Colors.RESET}`, ...optionalParams);
217
220
  safe(() => remote?.log?.('info', message, optionalParams));
218
221
  };
219
- const logWarning = (message, ...optionalParams) => {
222
+ const logWarning = (rawMessage, ...rawParams) => {
223
+ const message = (0, _redact.redactString)(rawMessage);
224
+ const optionalParams = (0, _redact.redact)(rawParams);
220
225
  if (!isEnabled() || !isLogLevelAllowed(LogLevels.WARN.level)) {
221
226
  return;
222
227
  }
223
228
  console.warn(`${LogLevels.WARN.color}[WARNING]: ${message}${Colors.RESET}`, ...optionalParams);
224
229
  safe(() => remote?.log?.('warn', message, optionalParams));
225
230
  };
226
- const logError = (message, ...optionalParams) => {
231
+ const logError = (rawMessage, ...rawParams) => {
232
+ const message = (0, _redact.redactString)(rawMessage);
233
+ const optionalParams = (0, _redact.redact)(rawParams);
227
234
  if (isEnabled() && isLogLevelAllowed(LogLevels.ERROR.level)) {
228
235
  console.error(`${LogLevels.ERROR.color}[ERROR]: ${message}${Colors.RESET}`, ...optionalParams);
229
236
  }
@@ -239,14 +246,17 @@ const logError = (message, ...optionalParams) => {
239
246
  });
240
247
  }
241
248
  };
242
- const breadcrumb = (message, data) => {
249
+ const breadcrumb = (rawMessage, rawData) => {
250
+ const message = (0, _redact.redactString)(rawMessage);
251
+ const data = (0, _redact.redact)(rawData);
243
252
  if (isEnabled() && isLogLevelAllowed('info')) {
244
253
  console.info(`${Colors.BLUE}[BREADCRUMB]: ${message}${Colors.RESET}`, data ?? '');
245
254
  }
246
255
  safe(() => remote?.breadcrumb?.(message, data));
247
256
  };
248
- const recordError = (error, context) => {
249
- const err = toErrorObject(error);
257
+ const recordError = (error, rawContext) => {
258
+ const err = (0, _redact.redact)(toErrorObject(error));
259
+ const context = (0, _redact.redact)(rawContext);
250
260
  if (isEnabled() && isLogLevelAllowed('error')) {
251
261
  console.error(`${Colors.RED_BOLD}[RECORDED]: ${err.message}${Colors.RESET}`, context ?? '');
252
262
  }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.redactString = exports.redact = void 0;
7
+ const REDACTED = '[REDACTED]';
8
+ const SECRET_KEYS = /^(token|accesstoken|refreshtoken|password|authorization)$/i;
9
+ const BEARER = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
10
+ const JWT = /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
11
+ const KEYED_VALUE = /(["']?(?:token|accessToken|refreshToken|password|authorization)["']?\s*[:=]\s*)(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^\s,;&}]+)/gi;
12
+ const MAX_DEPTH = 6;
13
+ const redactString = input => input.replace(BEARER, `Bearer ${REDACTED}`).replace(JWT, REDACTED).replace(KEYED_VALUE, (_match, prefix) => `${prefix}"${REDACTED}"`);
14
+ exports.redactString = redactString;
15
+ const redactInner = (value, depth, seen) => {
16
+ if (typeof value === 'string') {
17
+ return redactString(value);
18
+ }
19
+ if (value === null || typeof value !== 'object') {
20
+ return value;
21
+ }
22
+ if (value instanceof Error) {
23
+ value.message = redactString(value.message);
24
+ if (value.stack) {
25
+ value.stack = redactString(value.stack);
26
+ }
27
+ return value;
28
+ }
29
+ if (depth >= MAX_DEPTH || seen.has(value)) {
30
+ return value;
31
+ }
32
+ seen.add(value);
33
+ if (Array.isArray(value)) {
34
+ return value.map(item => redactInner(item, depth + 1, seen));
35
+ }
36
+ const proto = Object.getPrototypeOf(value);
37
+ if (proto !== Object.prototype && proto !== null) {
38
+ return value;
39
+ }
40
+ const out = {};
41
+ for (const [key, item] of Object.entries(value)) {
42
+ out[key] = SECRET_KEYS.test(key) ? REDACTED : redactInner(item, depth + 1, seen);
43
+ }
44
+ return out;
45
+ };
46
+
47
+ /** Returns a copy with secrets masked; Errors are masked in place so identity and stack survive. */
48
+ const redact = value => redactInner(value, 0, new WeakSet());
49
+ exports.redact = redact;
50
+ //# sourceMappingURL=redact.js.map
@@ -166,7 +166,7 @@ const initPush = next => {
166
166
  return {
167
167
  shouldShowBanner: show,
168
168
  shouldShowList: show,
169
- shouldPlaySound: show,
169
+ shouldPlaySound: show && (config?.playSoundInForeground?.() ?? true),
170
170
  shouldSetBadge: true
171
171
  };
172
172
  }
@@ -238,12 +238,13 @@ const enablePush = async () => {
238
238
  exports.enablePush = enablePush;
239
239
  const disablePush = async () => {
240
240
  const current = requireConfig();
241
- await current.unregister(await current.getInstallationId());
241
+ // Stop following token changes first, so a failed unregister cannot re-register the phone.
242
242
  tokenSubscription?.remove();
243
243
  tokenSubscription = null;
244
244
  setState({
245
245
  enabled: false
246
246
  });
247
+ await current.unregister(await current.getInstallationId());
247
248
  };
248
249
 
249
250
  /** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ require("axios");
4
+ //# sourceMappingURL=axios.js.map
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The clients default to a JSON content type, and axios then serialises a FormData body to
5
+ * JSON, which breaks every upload. Dropping the header for FormData lets the platform write
6
+ * the multipart header with its boundary.
7
+ */
8
+ export const sendFormDataAsMultipart = client => {
9
+ client.interceptors.request.use(config => {
10
+ if (typeof FormData !== 'undefined' && config.data instanceof FormData) {
11
+ config.headers?.delete?.('Content-Type');
12
+ }
13
+ return config;
14
+ });
15
+ };
16
+ //# sourceMappingURL=multipart.js.map
@@ -3,8 +3,10 @@
3
3
  import axios from 'axios';
4
4
  import { getConfigValue } from "../config/index.js";
5
5
  import { Logger } from "../logger/index.js";
6
+ import "../types/axios.js";
6
7
  import { applyConfiguredBaseUrl } from "./baseUrl.js";
7
8
  import { logCurl } from "./curl.js";
9
+ import { sendFormDataAsMultipart } from "./multipart.js";
8
10
  import { attachRetry } from "./retry.js";
9
11
 
10
12
  // Define timeout for requests
@@ -26,6 +28,7 @@ const publicClient = axios.create({
26
28
  timeout: REQUEST_TIMEOUT
27
29
  });
28
30
  applyConfiguredBaseUrl(publicClient);
31
+ sendFormDataAsMultipart(publicClient);
29
32
 
30
33
  // Registered before the logging interceptors below so they only ever observe
31
34
  // the final outcome of a request, not its intermediate retries. The policy
@@ -52,6 +55,9 @@ publicClient.interceptors.response.use(response => {
52
55
  }, error => {
53
56
  if (axios.isAxiosError(error)) {
54
57
  const originalRequest = error.config;
58
+ if (originalRequest?.skipErrorLog) {
59
+ return Promise.reject(error);
60
+ }
55
61
  if (!error.response) {
56
62
  Logger.error(`Network error or no response from server: ${error.message}, URL: ${originalRequest?.url}`);
57
63
  } else {
@@ -5,8 +5,10 @@ import { ACCESS_TOKEN_KEY, getToken } from "../auth/authStore.js";
5
5
  import { getConfigValue } from "../config/index.js";
6
6
  import { Logger } from "../logger/index.js";
7
7
  import { getNetworkStatus } from "../network/networkStatus.js";
8
+ import "../types/axios.js";
8
9
  import { applyConfiguredBaseUrl } from "./baseUrl.js";
9
10
  import { logCurl } from "./curl.js";
11
+ import { sendFormDataAsMultipart } from "./multipart.js";
10
12
  import { attachRetry } from "./retry.js";
11
13
  import { dispatchLogout, getErrorMessage, getRefreshPath, isRefreshInFlight, queueBehindRefresh, refreshAccessToken, resetRefreshState } from "./tokenRefresh.js";
12
14
  export { injectStore, injectUserAuth, resetRefreshState } from "./tokenRefresh.js";
@@ -24,6 +26,9 @@ export class RequestOfflineError extends Error {
24
26
  constructor(message = 'Request blocked: device is offline') {
25
27
  super(message);
26
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);
27
32
  }
28
33
  }
29
34
 
@@ -113,6 +118,7 @@ const isRefreshTokenRequest = url => {
113
118
  // registered run last. The curl line therefore describes a request that already
114
119
  // has both its baseURL and its Authorization header.
115
120
  applyConfiguredBaseUrl(userClient);
121
+ sendFormDataAsMultipart(userClient);
116
122
 
117
123
  // Registered BEFORE the auth interceptor below deliberately. Axios builds its
118
124
  // request chain by unshifting, so the FIRST-registered request interceptor runs
@@ -184,7 +190,9 @@ userClient.interceptors.response.use(response => response, async error => {
184
190
  const axiosError = error;
185
191
  const originalRequest = axiosError.config;
186
192
  if (!axiosError.response) {
187
- Logger.error(`Network error or no response from server: ${axiosError.message}, URL: ${originalRequest?.url}`);
193
+ if (!originalRequest?.skipErrorLog) {
194
+ Logger.error(`Network error or no response from server: ${axiosError.message}, URL: ${originalRequest?.url}`);
195
+ }
188
196
  return Promise.reject(error);
189
197
  }
190
198
  if (axiosError.response.status === 401 && originalRequest && !originalRequest._retry) {
@@ -226,7 +234,9 @@ userClient.interceptors.response.use(response => response, async error => {
226
234
  return Promise.reject(err);
227
235
  }
228
236
  }
229
- Logger.error(`Response error: ${axiosError.response?.data ?? axiosError.message}, URL: ${originalRequest?.url}, Status: ${axiosError.response.status}`);
237
+ if (!originalRequest?.skipErrorLog) {
238
+ Logger.error(`Response error: ${axiosError.response?.data ?? axiosError.message}, URL: ${originalRequest?.url}, Status: ${axiosError.response.status}`);
239
+ }
230
240
  return Promise.reject(error);
231
241
  });
232
242
  export default userClient;
@@ -27,6 +27,7 @@
27
27
  export { buildCurl, logCurl } from "./api/curl.js";
28
28
  export { default as publicClient } from "./api/publicClient.js";
29
29
  export { attachRetry, isRetryableStatus } from "./api/retry.js";
30
+ export { refreshAccessToken } from "./api/tokenRefresh.js";
30
31
  export { default as userClient, injectStore, injectUserAuth, RequestOfflineError, resetRefreshState, SESSION_TERMINATION_CODES } from "./api/userClient.js";
31
32
  // Auth / token storage
32
33
  export { ACCESS_TOKEN_KEY, getToken, REFRESH_TOKEN_KEY, removeToken, setKeychainImplementation, storeToken } from "./auth/authStore.js";
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import { getConfigValue } from "../config/index.js";
4
+ import { redact, redactString } from "./redact.js";
4
5
 
5
6
  // ANSI color codes
6
7
  const Colors = {
@@ -203,21 +204,27 @@ const forwardErrorToRemote = (err, context) => {
203
204
  }
204
205
  safe(() => remote?.recordError?.(err, buildErrorContext(context, suppressedCount)));
205
206
  };
206
- const logInfo = (message, ...optionalParams) => {
207
+ const logInfo = (rawMessage, ...rawParams) => {
208
+ const message = redactString(rawMessage);
209
+ const optionalParams = redact(rawParams);
207
210
  if (!isEnabled() || !isLogLevelAllowed(LogLevels.INFO.level)) {
208
211
  return;
209
212
  }
210
213
  console.info(`${LogLevels.INFO.color}[INFO]: ${message}${Colors.RESET}`, ...optionalParams);
211
214
  safe(() => remote?.log?.('info', message, optionalParams));
212
215
  };
213
- const logWarning = (message, ...optionalParams) => {
216
+ const logWarning = (rawMessage, ...rawParams) => {
217
+ const message = redactString(rawMessage);
218
+ const optionalParams = redact(rawParams);
214
219
  if (!isEnabled() || !isLogLevelAllowed(LogLevels.WARN.level)) {
215
220
  return;
216
221
  }
217
222
  console.warn(`${LogLevels.WARN.color}[WARNING]: ${message}${Colors.RESET}`, ...optionalParams);
218
223
  safe(() => remote?.log?.('warn', message, optionalParams));
219
224
  };
220
- const logError = (message, ...optionalParams) => {
225
+ const logError = (rawMessage, ...rawParams) => {
226
+ const message = redactString(rawMessage);
227
+ const optionalParams = redact(rawParams);
221
228
  if (isEnabled() && isLogLevelAllowed(LogLevels.ERROR.level)) {
222
229
  console.error(`${LogLevels.ERROR.color}[ERROR]: ${message}${Colors.RESET}`, ...optionalParams);
223
230
  }
@@ -233,14 +240,17 @@ const logError = (message, ...optionalParams) => {
233
240
  });
234
241
  }
235
242
  };
236
- const breadcrumb = (message, data) => {
243
+ const breadcrumb = (rawMessage, rawData) => {
244
+ const message = redactString(rawMessage);
245
+ const data = redact(rawData);
237
246
  if (isEnabled() && isLogLevelAllowed('info')) {
238
247
  console.info(`${Colors.BLUE}[BREADCRUMB]: ${message}${Colors.RESET}`, data ?? '');
239
248
  }
240
249
  safe(() => remote?.breadcrumb?.(message, data));
241
250
  };
242
- const recordError = (error, context) => {
243
- const err = toErrorObject(error);
251
+ const recordError = (error, rawContext) => {
252
+ const err = redact(toErrorObject(error));
253
+ const context = redact(rawContext);
244
254
  if (isEnabled() && isLogLevelAllowed('error')) {
245
255
  console.error(`${Colors.RED_BOLD}[RECORDED]: ${err.message}${Colors.RESET}`, context ?? '');
246
256
  }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ const REDACTED = '[REDACTED]';
4
+ const SECRET_KEYS = /^(token|accesstoken|refreshtoken|password|authorization)$/i;
5
+ const BEARER = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
6
+ const JWT = /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
7
+ const KEYED_VALUE = /(["']?(?:token|accessToken|refreshToken|password|authorization)["']?\s*[:=]\s*)(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^\s,;&}]+)/gi;
8
+ const MAX_DEPTH = 6;
9
+ export const redactString = input => input.replace(BEARER, `Bearer ${REDACTED}`).replace(JWT, REDACTED).replace(KEYED_VALUE, (_match, prefix) => `${prefix}"${REDACTED}"`);
10
+ const redactInner = (value, depth, seen) => {
11
+ if (typeof value === 'string') {
12
+ return redactString(value);
13
+ }
14
+ if (value === null || typeof value !== 'object') {
15
+ return value;
16
+ }
17
+ if (value instanceof Error) {
18
+ value.message = redactString(value.message);
19
+ if (value.stack) {
20
+ value.stack = redactString(value.stack);
21
+ }
22
+ return value;
23
+ }
24
+ if (depth >= MAX_DEPTH || seen.has(value)) {
25
+ return value;
26
+ }
27
+ seen.add(value);
28
+ if (Array.isArray(value)) {
29
+ return value.map(item => redactInner(item, depth + 1, seen));
30
+ }
31
+ const proto = Object.getPrototypeOf(value);
32
+ if (proto !== Object.prototype && proto !== null) {
33
+ return value;
34
+ }
35
+ const out = {};
36
+ for (const [key, item] of Object.entries(value)) {
37
+ out[key] = SECRET_KEYS.test(key) ? REDACTED : redactInner(item, depth + 1, seen);
38
+ }
39
+ return out;
40
+ };
41
+
42
+ /** Returns a copy with secrets masked; Errors are masked in place so identity and stack survive. */
43
+ export const redact = value => redactInner(value, 0, new WeakSet());
44
+ //# sourceMappingURL=redact.js.map
@@ -162,7 +162,7 @@ export const initPush = next => {
162
162
  return {
163
163
  shouldShowBanner: show,
164
164
  shouldShowList: show,
165
- shouldPlaySound: show,
165
+ shouldPlaySound: show && (config?.playSoundInForeground?.() ?? true),
166
166
  shouldSetBadge: true
167
167
  };
168
168
  }
@@ -232,12 +232,13 @@ export const enablePush = async () => {
232
232
  */
233
233
  export const disablePush = async () => {
234
234
  const current = requireConfig();
235
- await current.unregister(await current.getInstallationId());
235
+ // Stop following token changes first, so a failed unregister cannot re-register the phone.
236
236
  tokenSubscription?.remove();
237
237
  tokenSubscription = null;
238
238
  setState({
239
239
  enabled: false
240
240
  });
241
+ await current.unregister(await current.getInstallationId());
241
242
  };
242
243
 
243
244
  /** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ import 'axios';
4
+ //# sourceMappingURL=axios.js.map
@@ -0,0 +1,8 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ /**
3
+ * The clients default to a JSON content type, and axios then serialises a FormData body to
4
+ * JSON, which breaks every upload. Dropping the header for FormData lets the platform write
5
+ * the multipart header with its boundary.
6
+ */
7
+ export declare const sendFormDataAsMultipart: (client: AxiosInstance) => void;
8
+ //# sourceMappingURL=multipart.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { type AxiosInstance } from 'axios';
2
+ import '../types/axios';
2
3
  /**
3
4
  * Public Axios Client
4
5
  * - Base URL: Set your public API base URL.
@@ -1,4 +1,5 @@
1
1
  import { type AxiosInstance } from 'axios';
2
+ import '../types/axios';
2
3
  export type { RefreshableStore } from './tokenRefresh';
3
4
  export { injectStore, injectUserAuth, resetRefreshState } from './tokenRefresh';
4
5
  /**
@@ -25,6 +25,7 @@ export { buildCurl, logCurl } from './api/curl';
25
25
  export { default as publicClient } from './api/publicClient';
26
26
  export type { RetryPolicy } from './api/retry';
27
27
  export { attachRetry, isRetryableStatus } from './api/retry';
28
+ export { refreshAccessToken } from './api/tokenRefresh';
28
29
  export type { RefreshableStore, SessionTerminationCode } from './api/userClient';
29
30
  export { default as userClient, injectStore, injectUserAuth, RequestOfflineError, resetRefreshState, SESSION_TERMINATION_CODES } from './api/userClient';
30
31
  export type { KeychainLike, SecureCredentials, StoredToken } from './auth/authStore';
@@ -0,0 +1,4 @@
1
+ export declare const redactString: (input: string) => string;
2
+ /** Returns a copy with secrets masked; Errors are masked in place so identity and stack survive. */
3
+ export declare const redact: <T>(value: T) => T;
4
+ //# sourceMappingURL=redact.d.ts.map
@@ -23,6 +23,8 @@ export interface PushConfig {
23
23
  androidChannels?: PushAndroidChannel[];
24
24
  /** Whether a notification arriving while the app is open is shown. Defaults to true. */
25
25
  showInForeground?: () => boolean;
26
+ /** Whether a notification shown while the app is open plays its sound. Defaults to true. */
27
+ playSoundInForeground?: () => boolean;
26
28
  }
27
29
  export type PushStatus = 'unknown' | 'granted' | 'denied' | 'unavailable';
28
30
  export interface PushState {
@@ -0,0 +1,8 @@
1
+ import 'axios';
2
+ declare module 'axios' {
3
+ interface AxiosRequestConfig {
4
+ /** When true, the library's response-error interceptors do not log this request's failure. */
5
+ skipErrorLog?: boolean;
6
+ }
7
+ }
8
+ //# sourceMappingURL=axios.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ /**
3
+ * The clients default to a JSON content type, and axios then serialises a FormData body to
4
+ * JSON, which breaks every upload. Dropping the header for FormData lets the platform write
5
+ * the multipart header with its boundary.
6
+ */
7
+ export declare const sendFormDataAsMultipart: (client: AxiosInstance) => void;
8
+ //# sourceMappingURL=multipart.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { type AxiosInstance } from 'axios';
2
+ import '../types/axios.js';
2
3
  /**
3
4
  * Public Axios Client
4
5
  * - Base URL: Set your public API base URL.
@@ -1,4 +1,5 @@
1
1
  import { type AxiosInstance } from 'axios';
2
+ import '../types/axios.js';
2
3
  export type { RefreshableStore } from './tokenRefresh.js';
3
4
  export { injectStore, injectUserAuth, resetRefreshState } from './tokenRefresh.js';
4
5
  /**
@@ -25,6 +25,7 @@ export { buildCurl, logCurl } from './api/curl.js';
25
25
  export { default as publicClient } from './api/publicClient.js';
26
26
  export type { RetryPolicy } from './api/retry.js';
27
27
  export { attachRetry, isRetryableStatus } from './api/retry.js';
28
+ export { refreshAccessToken } from './api/tokenRefresh.js';
28
29
  export type { RefreshableStore, SessionTerminationCode } from './api/userClient.js';
29
30
  export { default as userClient, injectStore, injectUserAuth, RequestOfflineError, resetRefreshState, SESSION_TERMINATION_CODES } from './api/userClient.js';
30
31
  export type { KeychainLike, SecureCredentials, StoredToken } from './auth/authStore.js';
@@ -0,0 +1,4 @@
1
+ export declare const redactString: (input: string) => string;
2
+ /** Returns a copy with secrets masked; Errors are masked in place so identity and stack survive. */
3
+ export declare const redact: <T>(value: T) => T;
4
+ //# sourceMappingURL=redact.d.ts.map
@@ -23,6 +23,8 @@ export interface PushConfig {
23
23
  androidChannels?: PushAndroidChannel[];
24
24
  /** Whether a notification arriving while the app is open is shown. Defaults to true. */
25
25
  showInForeground?: () => boolean;
26
+ /** Whether a notification shown while the app is open plays its sound. Defaults to true. */
27
+ playSoundInForeground?: () => boolean;
26
28
  }
27
29
  export type PushStatus = 'unknown' | 'granted' | 'denied' | 'unavailable';
28
30
  export interface PushState {
@@ -0,0 +1,8 @@
1
+ import 'axios';
2
+ declare module 'axios' {
3
+ interface AxiosRequestConfig {
4
+ /** When true, the library's response-error interceptors do not log this request's failure. */
5
+ skipErrorLog?: boolean;
6
+ }
7
+ }
8
+ //# sourceMappingURL=axios.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-core",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Platform layer for Webority React Native apps: HTTP clients, auth/token storage, config, logging, formatters, validators, network status, permissions, storage and version checks. No UI.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -14,7 +14,7 @@
14
14
  "webority"
15
15
  ],
16
16
  "homepage": "https://www.webority.com",
17
- "license": "MIT",
17
+ "license": "SEE LICENSE IN LICENSE.txt",
18
18
  "author": {
19
19
  "name": "Webority Technologies",
20
20
  "email": "contact@webority.com",
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Webority Technologies
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.