@arex95/vue-core 3.3.0 → 5.1.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/dist/index.mjs CHANGED
@@ -35,33 +35,47 @@ function getTokenConfig() {
35
35
  }
36
36
 
37
37
  /**
38
- * Converts an `ArrayBuffer` or `Uint8Array` into a hexadecimal string representation.
38
+ * Returns the Web Crypto API instance, validating availability.
39
39
  *
40
- * @param {ArrayBuffer | Uint8Array} buffer - The buffer to convert.
41
- * @returns {string} The resulting hexadecimal string.
40
+ * Compatible with:
41
+ * - Modern browsers (window.crypto.subtle)
42
+ * - Node.js 15+ (globalThis.crypto.subtle — built-in Web Crypto API)
43
+ * - Nitro / Deno / Workers (globalThis.crypto.subtle)
44
+ *
45
+ * Throws a descriptive error on Node.js < 15 instead of failing silently.
46
+ */
47
+ function getWebCrypto() {
48
+ const c = typeof globalThis !== 'undefined'
49
+ ? globalThis.crypto
50
+ : typeof crypto !== 'undefined'
51
+ ? crypto
52
+ : undefined;
53
+ if (!c?.subtle) {
54
+ throw new Error('[arex-core] Web Crypto API (crypto.subtle) is not available. ' +
55
+ 'Requires Node.js 15+, a modern browser, or a runtime that exposes globalThis.crypto.subtle.');
56
+ }
57
+ return c;
58
+ }
59
+ /**
60
+ * Converts an `ArrayBuffer` or `Uint8Array` into a hexadecimal string.
42
61
  */
43
62
  function ab2hex(buffer) {
44
63
  return Array.from(new Uint8Array(buffer))
45
- .map((byte) => byte.toString(16).padStart(2, "0"))
46
- .join("");
64
+ .map((byte) => byte.toString(16).padStart(2, '0'))
65
+ .join('');
47
66
  }
48
67
  /**
49
68
  * Converts a hexadecimal string into a `Uint8Array`.
50
- *
51
- * @param {string} hex - The hexadecimal string to convert.
52
- * @returns {Uint8Array} The resulting `Uint8Array`.
53
- * @throws {TypeError} If the input is not a string.
54
- * @throws {Error} If the hexadecimal string has an invalid format or an odd length.
55
69
  */
56
70
  function hex2ab(hex) {
57
- if (typeof hex !== "string") {
58
- throw new TypeError("Input must be a string.");
71
+ if (typeof hex !== 'string') {
72
+ throw new TypeError('Input must be a string.');
59
73
  }
60
74
  if (hex.length === 0) {
61
75
  return new Uint8Array();
62
76
  }
63
77
  if (!/^[0-9a-fA-F]*$/.test(hex) || hex.length % 2 !== 0) {
64
- throw new Error("Invalid hexadecimal string format or odd length.");
78
+ throw new Error('Invalid hexadecimal string format or odd length.');
65
79
  }
66
80
  const array = new Uint8Array(hex.length / 2);
67
81
  for (let i = 0; i < hex.length; i += 2) {
@@ -70,81 +84,66 @@ function hex2ab(hex) {
70
84
  return array;
71
85
  }
72
86
  /**
73
- * Derives a `CryptoKey` for AES-CBC encryption from a plain-text secret key.
74
- * It uses SHA-256 to hash the secret key, ensuring a fixed-length key suitable for the Web Crypto API.
87
+ * Derives a CryptoKey for AES-CBC-256 from a plain-text secret using SHA-256.
75
88
  *
76
- * @param {string} secretKey - The plain-text secret key.
77
- * @returns {Promise<CryptoKey>} A promise that resolves with the derived `CryptoKey`.
78
- * @throws {Error} If the `secretKey` is null or empty.
89
+ * @throws {Error} If secretKey is empty or Web Crypto API is unavailable.
79
90
  */
80
91
  async function importKey(secretKey) {
81
92
  if (!secretKey) {
82
- throw new Error("Secret key cannot be null or empty.");
93
+ throw new Error('Secret key cannot be null or empty.');
83
94
  }
95
+ const wc = getWebCrypto();
84
96
  const keyMaterial = new TextEncoder().encode(secretKey);
85
- const digest = await crypto.subtle.digest("SHA-256", keyMaterial);
86
- return crypto.subtle.importKey("raw", digest, { name: "AES-CBC", length: 256 }, false, ["encrypt", "decrypt"]);
97
+ const digest = await wc.subtle.digest('SHA-256', keyMaterial);
98
+ return wc.subtle.importKey('raw', digest, { name: 'AES-CBC', length: 256 }, false, ['encrypt', 'decrypt']);
87
99
  }
88
100
  /**
89
- * Encrypts a plain-text value using AES-CBC with a given secret key.
90
- * A random 16-byte initialization vector (IV) is generated for each encryption.
91
- *
92
- * @param {string} value - The plain-text string to encrypt.
93
- * @param {string} secretKey - The secret key to use for encryption.
94
- * @returns {Promise<string>} A promise that resolves with a concatenated hexadecimal string of the IV and the ciphertext.
95
- * @throws {Error} If the `secretKey` is null or empty.
101
+ * Encrypts a plain-text string using AES-CBC-256.
102
+ * A unique 16-byte IV is generated per call.
103
+ * Output format: IV_hex (32 chars) + ciphertext_hex.
96
104
  */
97
105
  async function encrypt(value, secretKey) {
106
+ const wc = getWebCrypto();
98
107
  const key = await importKey(secretKey);
99
- const iv = crypto.getRandomValues(new Uint8Array(16));
108
+ const iv = wc.getRandomValues(new Uint8Array(16));
100
109
  const encodedValue = new TextEncoder().encode(value);
101
- const ciphertext = await crypto.subtle.encrypt({ name: "AES-CBC", iv: iv }, key, encodedValue);
110
+ const ciphertext = await wc.subtle.encrypt({ name: 'AES-CBC', iv }, key, encodedValue);
102
111
  return ab2hex(iv) + ab2hex(new Uint8Array(ciphertext));
103
112
  }
104
113
  /**
105
- * Decrypts a hexadecimal string (IV + ciphertext) using AES-CBC with a given secret key.
106
- *
107
- * @param {string} encryptedValue - The concatenated hexadecimal string of the IV and ciphertext.
108
- * @param {string} secretKey - The secret key to use for decryption.
109
- * @returns {Promise<string>} A promise that resolves with the decrypted plain-text string.
110
- * @throws {Error} If the encrypted value is null, empty, or too short, or if the `secretKey` is invalid.
114
+ * Decrypts a hex string (IV_hex + ciphertext_hex) produced by `encrypt()`.
111
115
  */
112
116
  async function decrypt(encryptedValue, secretKey) {
113
117
  if (!encryptedValue) {
114
- throw new Error("Encrypted value cannot be null or empty.");
118
+ throw new Error('Encrypted value cannot be null or empty.');
115
119
  }
116
- // For AES-CBC, the IV is ALWAYS 16 bytes.
117
- // 16 bytes * 2 hex characters/byte = 32 hex characters for the IV.
118
120
  if (encryptedValue.length < 32) {
119
- throw new Error("Encrypted value is too short. Expected at least 32 hexadecimal characters for the IV.");
121
+ throw new Error('Encrypted value is too short. Expected at least 32 hex chars for the IV.');
120
122
  }
123
+ const wc = getWebCrypto();
121
124
  const key = await importKey(secretKey);
122
- const ivHex = encryptedValue.substring(0, 32);
123
- const ciphertextHex = encryptedValue.substring(32);
124
- const iv = hex2ab(ivHex);
125
- const ciphertext = hex2ab(ciphertextHex);
125
+ const iv = hex2ab(encryptedValue.substring(0, 32));
126
+ const ciphertext = hex2ab(encryptedValue.substring(32));
126
127
  if (iv.byteLength !== 16) {
127
- throw new Error(`Converted IV has incorrect length: ${iv.byteLength} bytes. Expected 16 bytes.`);
128
+ throw new Error(`IV has incorrect length: ${iv.byteLength} bytes. Expected 16.`);
128
129
  }
129
130
  if (ciphertext.byteLength === 0) {
130
- throw new Error("Ciphertext is empty. No data to decrypt.");
131
+ throw new Error('Ciphertext is empty. Nothing to decrypt.');
131
132
  }
132
- const decryptedBuffer = await crypto.subtle.decrypt({ name: "AES-CBC", iv: iv }, key, ciphertext);
133
+ const decryptedBuffer = await wc.subtle.decrypt({ name: 'AES-CBC', iv }, key, ciphertext);
133
134
  return new TextDecoder().decode(decryptedBuffer);
134
135
  }
135
136
 
136
137
  const isServer = typeof window === 'undefined';
137
138
  const isClient = typeof window !== 'undefined';
138
139
  function getStorage() {
139
- if (isServer) {
140
+ if (isServer)
140
141
  return null;
141
- }
142
142
  return window.localStorage;
143
143
  }
144
144
  function getSessionStorage() {
145
- if (isServer) {
145
+ if (isServer)
146
146
  return null;
147
- }
148
147
  return window.sessionStorage;
149
148
  }
150
149
  function getCookieStorage() {
@@ -154,10 +153,14 @@ function getCookieStorage() {
154
153
  return null;
155
154
  const cookies = document.cookie.split(';');
156
155
  for (const cookie of cookies) {
157
- const [name, value] = cookie.trim().split('=');
158
- if (name === key) {
159
- return decodeURIComponent(value);
160
- }
156
+ // Split on the FIRST '=' only — values may contain '=' (e.g. base64, hex).
157
+ const separatorIndex = cookie.indexOf('=');
158
+ if (separatorIndex === -1)
159
+ continue;
160
+ const name = cookie.substring(0, separatorIndex).trim();
161
+ if (name !== key)
162
+ continue;
163
+ return decodeURIComponent(cookie.substring(separatorIndex + 1));
161
164
  }
162
165
  return null;
163
166
  },
@@ -170,62 +173,57 @@ function getCookieStorage() {
170
173
  date.setTime(date.getTime() + options.expires * 24 * 60 * 60 * 1000);
171
174
  cookie += `; expires=${date.toUTCString()}`;
172
175
  }
173
- cookie += `; path=${options?.path || '/'}`;
176
+ cookie += `; path=${options?.path ?? '/'}`;
174
177
  if (options?.domain) {
175
178
  cookie += `; domain=${options.domain}`;
176
179
  }
177
180
  if (options?.secure !== false) {
178
- const isSecure = options?.secure ?? (typeof window !== 'undefined' && window.location.protocol === 'https:');
179
- if (isSecure) {
181
+ const isSecure = options?.secure ??
182
+ (typeof window !== 'undefined' && window.location.protocol === 'https:');
183
+ if (isSecure)
180
184
  cookie += '; Secure';
181
- }
182
- }
183
- if (options?.sameSite) {
184
- cookie += `; SameSite=${options.sameSite}`;
185
- }
186
- else {
187
- cookie += '; SameSite=Lax';
188
185
  }
186
+ cookie += `; SameSite=${options?.sameSite ?? 'Lax'}`;
189
187
  if (options?.httpOnly) {
190
- console.warn('HttpOnly cookies cannot be set from JavaScript. Use server-side code to set HttpOnly cookies.');
188
+ throw new Error('[arex-core] HttpOnly cookies cannot be set from JavaScript. ' +
189
+ 'Use server-side code (e.g. h3 setCookie) to set HttpOnly cookies.');
191
190
  }
192
191
  document.cookie = cookie;
193
192
  },
194
193
  removeItem: (key, options) => {
195
194
  if (isServer)
196
195
  return;
197
- const path = options?.path || '/';
196
+ const path = options?.path ?? '/';
198
197
  const domain = options?.domain ? `; domain=${options.domain}` : '';
199
198
  document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}${domain}`;
200
199
  },
201
200
  };
202
201
  }
203
202
  function getPreferredStorage() {
204
- if (isServer) {
203
+ if (isServer)
205
204
  return getCookieStorage();
206
- }
207
- const storage = getStorage();
208
- if (storage) {
209
- return storage;
210
- }
211
- return getCookieStorage();
205
+ return getStorage() ?? getCookieStorage();
212
206
  }
213
207
 
214
208
  /**
215
- * Encrypts and stores a key-value pair in either `localStorage`, `sessionStorage`, or cookies.
216
- * Cookies are automatically used in SSR environments and can be explicitly requested.
217
- * Cookies include security options: Secure (HTTPS only), SameSite (CSRF protection), and encryption.
209
+ * Encrypts and stores a value in the requested storage.
210
+ *
211
+ * | location | where it goes | persistence |
212
+ * |-----------|----------------------------|---------------------|
213
+ * | 'cookie' | document.cookie | expires option |
214
+ * | 'local' | localStorage | until explicitly cleared |
215
+ * | 'any' | localStorage | until explicitly cleared |
216
+ * | 'session' | sessionStorage | until tab closes |
218
217
  *
219
- * @param {string} key - The key for the storage item.
220
- * @param {string} value - The string value to encrypt and store.
221
- * @param {string} secretKey - The secret key to use for encryption.
222
- * @param {LocationPreference} location - The storage location: 'local' for `localStorage`, 'session' for `sessionStorage`, 'cookie' for cookies, or 'any' for retrieval.
223
- * @param {CookieOptions} [cookieOptions] - Optional cookie-specific options (only used when location is 'cookie').
224
- * @returns {Promise<void>} A promise that resolves when the item has been stored.
218
+ * Note: 'any' stores in localStorage (same as 'local') for maximum
219
+ * persistence. Previously it stored in sessionStorage that was a bug.
220
+ *
221
+ * In SSR environments cookies are always used regardless of location
222
+ * (localStorage / sessionStorage do not exist on the server).
225
223
  */
226
224
  async function storeEncryptedItem(key, value, secretKey, location, cookieOptions) {
227
225
  const encryptedValue = await encrypt(value, secretKey);
228
- if (location === "cookie" || isServer) {
226
+ if (location === 'cookie' || isServer) {
229
227
  const cookieStorage = getCookieStorage();
230
228
  const defaultCookieOptions = {
231
229
  expires: location === 'local' || isServer ? 365 : undefined,
@@ -237,58 +235,85 @@ async function storeEncryptedItem(key, value, secretKey, location, cookieOptions
237
235
  cookieStorage.setItem(key, encryptedValue, defaultCookieOptions);
238
236
  return;
239
237
  }
240
- const storage = location === "local" ? getStorage() : getSessionStorage();
238
+ // 'local' and 'any' localStorage (persistent)
239
+ // 'session' → sessionStorage (tab-scoped)
240
+ const storage = location === 'local' || location === 'any' ? getStorage() : getSessionStorage();
241
241
  if (storage) {
242
242
  storage.setItem(key, encryptedValue);
243
243
  }
244
244
  }
245
245
  /**
246
- * Retrieves and decrypts an item from `localStorage`, `sessionStorage`, or cookies.
247
- * When location is 'any', checks in order: sessionStorage, localStorage, cookies.
248
- * Cookies are automatically checked in SSR environments.
246
+ * Retrieves and decrypts a value from storage.
247
+ *
248
+ * Search order by location:
249
+ *
250
+ * | location | search order |
251
+ * |-----------|-------------------------------------------|
252
+ * | 'cookie' | cookies only |
253
+ * | 'local' | localStorage only |
254
+ * | 'session' | sessionStorage only |
255
+ * | 'any' | sessionStorage → localStorage → cookies |
249
256
  *
250
- * @param {string} key - The key of the item to retrieve.
251
- * @param {string} secretKey - The secret key to use for decryption.
252
- * @param {LocationPreference} location - The storage location to search: 'local', 'session', 'cookie', or 'any' (checks session, local, cookie in that order).
253
- * @returns {Promise<string | null>} A promise that resolves with the decrypted value, or `null` if the item is not found or decryption fails.
257
+ * In SSR, cookies are always checked first (localStorage / sessionStorage
258
+ * are not available on the server).
259
+ *
260
+ * Returns null if the key is not found or decryption fails.
254
261
  */
255
262
  async function getDecryptedItem(key, secretKey, location) {
256
263
  let encryptedData = null;
257
- if (location === "cookie" || isServer) {
264
+ // ── cookies ─────────────────────────────────────────────────────────────
265
+ if (location === 'cookie' || isServer) {
258
266
  const cookieStorage = getCookieStorage();
259
267
  encryptedData = cookieStorage.getItem(key);
260
268
  if (encryptedData) {
261
269
  try {
262
270
  return await decrypt(encryptedData, secretKey);
263
271
  }
264
- catch (error) {
272
+ catch {
265
273
  return null;
266
274
  }
267
275
  }
268
- if (location === "cookie") {
276
+ // Explicit 'cookie' location stops here — don't fall through
277
+ if (location === 'cookie')
269
278
  return null;
279
+ }
280
+ // ── sessionStorage ───────────────────────────────────────────────────────
281
+ if (location === 'session' || location === 'any') {
282
+ const ss = getSessionStorage();
283
+ encryptedData = ss?.getItem(key) ?? null;
284
+ if (encryptedData) {
285
+ try {
286
+ return await decrypt(encryptedData, secretKey);
287
+ }
288
+ catch {
289
+ return null;
290
+ }
270
291
  }
271
292
  }
272
- if (location === "session" || location === "any") {
273
- const sessionStorage = getSessionStorage();
274
- encryptedData = sessionStorage?.getItem(key) || null;
293
+ // ── localStorage ─────────────────────────────────────────────────────────
294
+ if (location === 'local' || location === 'any') {
295
+ const ls = getStorage();
296
+ encryptedData = ls?.getItem(key) ?? null;
275
297
  if (encryptedData) {
276
298
  try {
277
299
  return await decrypt(encryptedData, secretKey);
278
300
  }
279
- catch (error) {
301
+ catch {
280
302
  return null;
281
303
  }
282
304
  }
283
305
  }
284
- if (location === "local" || location === "any") {
285
- const storage = getStorage();
286
- encryptedData = storage?.getItem(key) || null;
306
+ // ── cookies fallback for 'any' on client ─────────────────────────────────
307
+ // Checked last so that localStorage/sessionStorage take precedence,
308
+ // but cookies stored explicitly with location='cookie' are still found.
309
+ if (location === 'any' && !isServer) {
310
+ const cookieStorage = getCookieStorage();
311
+ encryptedData = cookieStorage.getItem(key);
287
312
  if (encryptedData) {
288
313
  try {
289
314
  return await decrypt(encryptedData, secretKey);
290
315
  }
291
- catch (error) {
316
+ catch {
292
317
  return null;
293
318
  }
294
319
  }
@@ -1261,114 +1286,100 @@ const configCallbacks = (config) => {
1261
1286
  const getCallbacksConfig = () => callbacksConfig;
1262
1287
 
1263
1288
  /**
1264
- * Removes all stored authentication credentials (access and refresh tokens) from the specified storage locations.
1289
+ * Removes all stored authentication credentials (access + refresh tokens)
1290
+ * from the specified storage location(s).
1265
1291
  *
1266
- * @param {LocationPreference} location - The storage location to clear. Can be 'local' for `localStorage`,
1267
- * 'session' for `sessionStorage`, 'cookie' for cookies, or 'any' to clear all.
1268
- * @returns {Promise<void>} A promise that resolves when the credentials have been cleared.
1292
+ * With location='any', ALL storage locations are cleared including cookies.
1293
+ * This prevents orphaned tokens surviving a logout.
1269
1294
  */
1270
1295
  const cleanCredentials = async (location) => {
1271
1296
  const tokensConfig = getTokenConfig();
1272
- if (location === "cookie" || isServer) {
1297
+ const keys = Object.keys(tokensConfig);
1298
+ const tokenItemKeys = keys.map((k) => tokensConfig[k]);
1299
+ const removeCookies = () => {
1273
1300
  const cookieStorage = getCookieStorage();
1274
- Object.keys(tokensConfig).forEach((key) => {
1275
- cookieStorage.removeItem(tokensConfig[key], { path: '/' });
1276
- });
1277
- if (location === "cookie") {
1278
- return;
1279
- }
1301
+ tokenItemKeys.forEach((itemKey) => cookieStorage.removeItem(itemKey, { path: '/' }));
1302
+ };
1303
+ const removeFromStorage = (storage) => {
1304
+ tokenItemKeys.forEach((itemKey) => storage?.removeItem(itemKey));
1305
+ };
1306
+ if (location === 'cookie') {
1307
+ removeCookies();
1308
+ return;
1309
+ }
1310
+ if (isServer) {
1311
+ removeCookies();
1312
+ return;
1313
+ }
1314
+ // Client — clear every requested location
1315
+ if (location === 'local' || location === 'any') {
1316
+ removeFromStorage(getStorage());
1317
+ }
1318
+ if (location === 'session' || location === 'any') {
1319
+ removeFromStorage(getSessionStorage());
1320
+ }
1321
+ // 'any' also clears cookies so that tokens stored explicitly with
1322
+ // location='cookie' don't survive a logout
1323
+ if (location === 'any') {
1324
+ removeCookies();
1280
1325
  }
1281
- Object.keys(tokensConfig).forEach((key) => {
1282
- const itemKey = tokensConfig[key];
1283
- if (location === "local" || location === "any") {
1284
- const storage = getStorage();
1285
- storage?.removeItem(itemKey);
1286
- }
1287
- if (location === "session" || location === "any") {
1288
- const sessionStorage = getSessionStorage();
1289
- sessionStorage?.removeItem(itemKey);
1290
- }
1291
- });
1292
1326
  };
1293
1327
  /**
1294
1328
  * Retrieves and decrypts the access token from the specified storage location.
1295
- *
1296
- * @param {string} secretKey - The secret key to use for decryption.
1297
- * @param {LocationPreference} location - The storage location to search ('local', 'session', 'cookie', or 'any').
1298
- * @returns {Promise<string | null>} A promise that resolves with the decrypted access token, or `null` if it's not found.
1299
1329
  */
1300
1330
  const getAuthToken = async (secretKey, location) => {
1301
1331
  const tokensConfig = getTokenConfig();
1302
- return await getDecryptedItem(tokensConfig.ACCESS_TOKEN, secretKey, location);
1332
+ return getDecryptedItem(tokensConfig.ACCESS_TOKEN, secretKey, location);
1303
1333
  };
1304
1334
  /**
1305
1335
  * Retrieves and decrypts the refresh token from the specified storage location.
1306
- *
1307
- * @param {string} secretKey - The secret key to use for decryption.
1308
- * @param {LocationPreference} location - The storage location to search ('local', 'session', 'cookie', or 'any').
1309
- * @returns {Promise<string | null>} A promise that resolves with the decrypted refresh token, or `null` if it's not found.
1310
1336
  */
1311
1337
  const getAuthRefreshToken = async (secretKey, location) => {
1312
1338
  const tokensConfig = getTokenConfig();
1313
- return await getDecryptedItem(tokensConfig.REFRESH_TOKEN, secretKey, location);
1339
+ return getDecryptedItem(tokensConfig.REFRESH_TOKEN, secretKey, location);
1314
1340
  };
1315
1341
  /**
1316
- * Encrypts and stores the access token in the specified storage location.
1317
- *
1318
- * @param {string} token - The access token to store.
1319
- * @param {string} secretKey - The secret key to use for encryption.
1320
- * @param {LocationPreference} location - The storage location ('local', 'session', or 'cookie').
1321
- * @returns {Promise<void>} A promise that resolves when the token has been stored.
1342
+ * Encrypts and stores the access token.
1322
1343
  */
1323
1344
  const storeAuthToken = async (token, secretKey, location) => {
1324
1345
  const tokensConfig = getTokenConfig();
1325
1346
  await storeEncryptedItem(tokensConfig.ACCESS_TOKEN, token, secretKey, location);
1326
1347
  };
1327
1348
  /**
1328
- * Encrypts and stores the refresh token in the specified storage location.
1329
- *
1330
- * @param {string} token - The refresh token to store.
1331
- * @param {string} secretKey - The secret key to use for encryption.
1332
- * @param {LocationPreference} location - The storage location ('local', 'session', or 'cookie').
1333
- * @returns {Promise<void>} A promise that resolves when the token has been stored.
1349
+ * Encrypts and stores the refresh token.
1334
1350
  */
1335
1351
  const storeAuthRefreshToken = async (token, secretKey, location) => {
1336
1352
  const tokensConfig = getTokenConfig();
1337
1353
  await storeEncryptedItem(tokensConfig.REFRESH_TOKEN, token, secretKey, location);
1338
1354
  };
1339
1355
  /**
1340
- * Verifies the current user's authentication status by checking for a valid, unexpired access token.
1341
- * It searches for the token in all storage locations (sessionStorage, localStorage, cookies).
1342
- * If the token is missing, malformed, or expired, it logs the issue, clears credentials, and returns `false`.
1356
+ * Verifies the current user's authentication status by checking for a
1357
+ * valid, unexpired access token across all storage locations.
1343
1358
  *
1344
- * @returns {Promise<boolean>} A promise that resolves to `true` if the user is authenticated, and `false` otherwise.
1359
+ * Returns false (without throwing) if the token is missing, malformed, or expired.
1345
1360
  */
1346
1361
  const verifyAuth = async () => {
1347
- const sessionPersistence = 'any';
1348
- const handleAuthError = async (message, shouldClean = true) => {
1349
- handleError(message);
1350
- if (shouldClean) {
1351
- await cleanCredentials(sessionPersistence);
1352
- }
1362
+ const token = await getAuthToken(getAppKey(), 'any');
1363
+ if (!token)
1353
1364
  return false;
1354
- };
1355
- const token = await getAuthToken(getAppKey(), sessionPersistence);
1356
- if (!token) {
1357
- return handleAuthError("TOKEN_MISSING: No valid token found", false);
1358
- }
1359
1365
  try {
1360
1366
  const decoded = jwtDecode(token);
1361
1367
  const currentTime = Date.now() / 1000;
1362
- if (typeof decoded.exp !== "number") {
1363
- return handleAuthError("TOKEN_INVALID: Invalid expiration format");
1368
+ if (typeof decoded.exp !== 'number') {
1369
+ handleError('TOKEN_INVALID: Invalid expiration format');
1370
+ await cleanCredentials('any');
1371
+ return false;
1364
1372
  }
1365
1373
  if (decoded.exp <= currentTime) {
1366
- return handleAuthError("TOKEN_EXPIRED: Token is expired", false);
1374
+ handleError('TOKEN_EXPIRED: Token is expired');
1375
+ return false;
1367
1376
  }
1368
1377
  return true;
1369
1378
  }
1370
- catch (error) {
1371
- return handleAuthError("TOKEN_INVALID: Invalid token format");
1379
+ catch {
1380
+ handleError('TOKEN_INVALID: Could not decode token');
1381
+ await cleanCredentials('any');
1382
+ return false;
1372
1383
  }
1373
1384
  };
1374
1385
 
@@ -1758,32 +1769,39 @@ function getDefaultAuthFetcher() {
1758
1769
  }
1759
1770
 
1760
1771
  /**
1761
- * Refreshes the access and refresh tokens by making a POST request to the refresh endpoint.
1762
- * It retrieves the current refresh token from storage, sends it to the refresh endpoint,
1763
- * and then stores the new tokens upon a successful response. If the refresh process fails
1764
- * or no refresh token is found, it clears all credentials and reloads the page.
1772
+ * Refreshes the access and refresh tokens.
1773
+ *
1774
+ * Fixes over the original:
1775
+ * 1. Uses `persistence` (the location where tokens were originally stored)
1776
+ * instead of hardcoding `"any"` — so cookies / localStorage / sessionStorage
1777
+ * are searched in the right place.
1778
+ * 2. Sends the refresh token in the request body using the configured
1779
+ * `refreshTokenPaths.refreshTokenPath` as the body key, so the backend
1780
+ * always receives it regardless of withCredentials or cookie settings.
1765
1781
  *
1766
- * @param {Fetcher} [fetcher] - Optional fetcher function to use for the refresh request. If not provided, uses the default configured fetcher.
1767
- * @returns {Promise<AuthResponse>} A promise that resolves with the new authentication response containing the refreshed tokens.
1768
- * @throws {Error} Throws an error if the refresh token is missing or if the refresh request fails, which is then caught to trigger a logout.
1782
+ * On failure: clears credentials and calls `onRefreshFailed` callback.
1769
1783
  */
1770
1784
  const refreshTokens = async (fetcher) => {
1771
1785
  const tokenPaths = getRefreshTokenPathsConfig();
1772
1786
  const endpoints = getEndpointsConfig();
1773
1787
  const secretKey = getAppKey();
1774
1788
  const persistence = await getSessionPersistence();
1775
- const getFetcher = () => fetcher || getDefaultAuthFetcher();
1789
+ const getFetcher = () => fetcher ?? getDefaultAuthFetcher();
1776
1790
  try {
1777
- const refreshTokenFromStorage = await getAuthRefreshToken(secretKey, "any");
1778
- if (!refreshTokenFromStorage) {
1779
- throw new Error("TOKEN_MISSING: No refresh token found in storage.");
1791
+ // Use persistence (not hardcoded "any") — tokens live where they were stored
1792
+ const refreshToken = await getAuthRefreshToken(secretKey, persistence);
1793
+ if (!refreshToken) {
1794
+ throw new Error('TOKEN_MISSING: No refresh token found in storage.');
1780
1795
  }
1796
+ // Send refresh token in the request body using the configured path key
1797
+ const refreshBodyKey = tokenPaths.refreshTokenPath ?? 'refresh_token';
1781
1798
  const data = await getFetcher()({
1782
1799
  method: 'POST',
1783
1800
  url: endpoints.REFRESH,
1801
+ data: { [refreshBodyKey]: refreshToken },
1784
1802
  });
1785
- const { accessToken, refreshToken } = extractAndValidateTokens(data, tokenPaths, "REFRESH");
1786
- await storeTokens(accessToken, refreshToken, persistence);
1803
+ const { accessToken, refreshToken: newRefreshToken } = extractAndValidateTokens(data, tokenPaths, 'REFRESH');
1804
+ await storeTokens(accessToken, newRefreshToken, persistence);
1787
1805
  return data;
1788
1806
  }
1789
1807
  catch (error) {
@@ -1840,12 +1858,6 @@ function createAxiosFetcher(axiosInstance) {
1840
1858
  };
1841
1859
  }
1842
1860
 
1843
- /**
1844
- * A service class that encapsulates a customizable Axios instance with built-in interceptors
1845
- * for handling authentication, token refreshing, and request cancellation. It is designed to
1846
- * streamline API communication by automatically attaching authorization headers and managing
1847
- * token refresh logic for 401 Unauthorized responses.
1848
- */
1849
1861
  class AxiosService {
1850
1862
  instance;
1851
1863
  cancelTokenSource;
@@ -1853,20 +1865,16 @@ class AxiosService {
1853
1865
  refreshTokenUrl;
1854
1866
  isRefreshing = false;
1855
1867
  failedQueue = [];
1856
- /**
1857
- * Creates an instance of AxiosService.
1858
- * @param {AxiosServiceOptions} options - Configuration options for the Axios instance, such as `baseURL`, `timeout`, and custom `headers`.
1859
- */
1860
1868
  constructor(options) {
1861
1869
  this.cancelTokenSource = axios.CancelToken.source();
1862
1870
  const endpointsConfig = getEndpointsConfig();
1863
1871
  this.refreshTokenUrl = endpointsConfig.REFRESH;
1864
1872
  this.instance = axios.create({
1865
- baseURL: options.baseURL ?? "",
1873
+ baseURL: options.baseURL ?? '',
1866
1874
  timeout: options.timeout ?? 30000,
1867
1875
  headers: {
1868
- Accept: "application/json",
1869
- "Content-Type": "application/json",
1876
+ Accept: 'application/json',
1877
+ 'Content-Type': 'application/json',
1870
1878
  ...options.headers,
1871
1879
  },
1872
1880
  withCredentials: options.withCredentials ?? false,
@@ -1875,6 +1883,12 @@ class AxiosService {
1875
1883
  this.initializeInterceptors();
1876
1884
  }
1877
1885
  }
1886
+ /**
1887
+ * Resolves or rejects all queued promises waiting for a token refresh.
1888
+ *
1889
+ * Fix: if both error and token are null (edge case), the queue is still
1890
+ * cleared to avoid permanently hanging promises.
1891
+ */
1878
1892
  processQueue(error, token = null) {
1879
1893
  this.failedQueue.forEach((prom) => {
1880
1894
  if (error) {
@@ -1883,6 +1897,10 @@ class AxiosService {
1883
1897
  else if (token) {
1884
1898
  prom.resolve(token);
1885
1899
  }
1900
+ else {
1901
+ // Neither error nor token — reject with a clear message rather than hanging
1902
+ prom.reject(new Error('[arex-core] Token refresh completed but no token was produced.'));
1903
+ }
1886
1904
  });
1887
1905
  this.failedQueue = [];
1888
1906
  }
@@ -1892,8 +1910,12 @@ class AxiosService {
1892
1910
  }
1893
1911
  }
1894
1912
  initializeInterceptors() {
1913
+ // ── Request: attach Authorization using the session's storage location ──
1895
1914
  this.instance.interceptors.request.use(async (config) => {
1896
- const token = await getAuthToken(getAppKey(), "any");
1915
+ // Use persistence preference (not hardcoded "any") so the token is
1916
+ // found regardless of whether it was stored in cookies, localStorage, etc.
1917
+ const persistence = await getSessionPersistence();
1918
+ const token = await getAuthToken(getAppKey(), persistence);
1897
1919
  if (token) {
1898
1920
  this.setAuthHeader(config, token);
1899
1921
  }
@@ -1904,11 +1926,13 @@ class AxiosService {
1904
1926
  handleError(error);
1905
1927
  return Promise.reject(error);
1906
1928
  });
1929
+ // ── Response: handle 401 with token refresh ──────────────────────────
1907
1930
  this.instance.interceptors.response.use((response) => {
1908
1931
  this.activeRequests--;
1909
1932
  return response;
1910
1933
  }, async (error) => {
1911
1934
  this.activeRequests--;
1935
+ // Do not attempt refresh in SSR — storage is unavailable
1912
1936
  if (typeof window === 'undefined') {
1913
1937
  return Promise.reject(error);
1914
1938
  }
@@ -1916,14 +1940,11 @@ class AxiosService {
1916
1940
  const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
1917
1941
  const isRefreshCall = originalRequest?.url === this.refreshTokenUrl;
1918
1942
  const isRetry = originalRequest?._retry === true;
1919
- if (!isAuthError || isRefreshCall || isRetry) {
1920
- handleError(error);
1921
- return Promise.reject(error);
1922
- }
1923
- if (!originalRequest) {
1943
+ if (!isAuthError || isRefreshCall || isRetry || !originalRequest) {
1924
1944
  handleError(error);
1925
1945
  return Promise.reject(error);
1926
1946
  }
1947
+ // Queue concurrent requests while a refresh is already in progress
1927
1948
  if (this.isRefreshing) {
1928
1949
  return new Promise((resolve, reject) => {
1929
1950
  this.failedQueue.push({ resolve, reject });
@@ -1932,25 +1953,22 @@ class AxiosService {
1932
1953
  this.setAuthHeader(originalRequest, newToken);
1933
1954
  return this.instance(originalRequest);
1934
1955
  })
1935
- .catch((err) => {
1936
- return Promise.reject(err);
1937
- });
1956
+ .catch((err) => Promise.reject(err));
1938
1957
  }
1939
1958
  this.isRefreshing = true;
1940
1959
  originalRequest._retry = true;
1941
1960
  try {
1942
1961
  const fetcher = createAxiosFetcher(this.instance);
1943
1962
  await refreshTokens(fetcher);
1944
- const newToken = await getAuthToken(getAppKey(), "any");
1945
- if (newToken) {
1946
- this.processQueue(null, newToken);
1947
- this.setAuthHeader(originalRequest, newToken);
1948
- }
1949
- else {
1950
- const refreshError = new Error("New token not found after refresh.");
1963
+ const persistence = await getSessionPersistence();
1964
+ const newToken = await getAuthToken(getAppKey(), persistence);
1965
+ if (!newToken) {
1966
+ const refreshError = new Error('[arex-core] New token not found after refresh.');
1951
1967
  this.processQueue(refreshError, null);
1952
1968
  throw refreshError;
1953
1969
  }
1970
+ this.processQueue(null, newToken);
1971
+ this.setAuthHeader(originalRequest, newToken);
1954
1972
  this.isRefreshing = false;
1955
1973
  return this.instance(originalRequest);
1956
1974
  }
@@ -1962,39 +1980,19 @@ class AxiosService {
1962
1980
  }
1963
1981
  });
1964
1982
  }
1965
- /**
1966
- * Returns the number of active (in-flight) requests.
1967
- * @returns {number} The number of active requests.
1968
- */
1969
1983
  getActiveRequests() {
1970
1984
  return this.activeRequests;
1971
1985
  }
1972
- /**
1973
- * Returns the underlying Axios instance.
1974
- * @returns {AxiosInstance} The Axios instance.
1975
- */
1976
1986
  getAxiosInstance() {
1977
1987
  return this.instance;
1978
1988
  }
1979
- /**
1980
- * Cancels all ongoing requests made by this Axios instance.
1981
- */
1982
1989
  cancelAllRequests() {
1983
- this.cancelTokenSource.cancel("Operation canceled by the user.");
1990
+ this.cancelTokenSource.cancel('Operation canceled by the user.');
1984
1991
  this.cancelTokenSource = axios.CancelToken.source();
1985
1992
  }
1986
- /**
1987
- * Sets a default header for all subsequent requests.
1988
- * @param {string} key - The header key.
1989
- * @param {string} value - The header value.
1990
- */
1991
1993
  setHeader(key, value) {
1992
1994
  this.instance.defaults.headers.common[key] = value;
1993
1995
  }
1994
- /**
1995
- * Removes a default header.
1996
- * @param {string} key - The header key to remove.
1997
- */
1998
1996
  removeHeader(key) {
1999
1997
  delete this.instance.defaults.headers.common[key];
2000
1998
  }