@arex95/vue-core 3.1.0 → 5.0.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.
@@ -1,16 +1,10 @@
1
- import { AxiosInstance } from "axios";
2
- import { AxiosServiceOptions } from "@/types/AxiosServiceOptions";
3
- declare module "axios" {
1
+ import { AxiosInstance } from 'axios';
2
+ import { AxiosServiceOptions } from '@/types/AxiosServiceOptions';
3
+ declare module 'axios' {
4
4
  interface InternalAxiosRequestConfig {
5
5
  _retry?: boolean;
6
6
  }
7
7
  }
8
- /**
9
- * A service class that encapsulates a customizable Axios instance with built-in interceptors
10
- * for handling authentication, token refreshing, and request cancellation. It is designed to
11
- * streamline API communication by automatically attaching authorization headers and managing
12
- * token refresh logic for 401 Unauthorized responses.
13
- */
14
8
  export declare class AxiosService {
15
9
  private readonly instance;
16
10
  private cancelTokenSource;
@@ -18,37 +12,19 @@ export declare class AxiosService {
18
12
  private readonly refreshTokenUrl;
19
13
  private isRefreshing;
20
14
  private failedQueue;
15
+ constructor(options: AxiosServiceOptions);
21
16
  /**
22
- * Creates an instance of AxiosService.
23
- * @param {AxiosServiceOptions} options - Configuration options for the Axios instance, such as `baseURL`, `timeout`, and custom `headers`.
17
+ * Resolves or rejects all queued promises waiting for a token refresh.
18
+ *
19
+ * Fix: if both error and token are null (edge case), the queue is still
20
+ * cleared to avoid permanently hanging promises.
24
21
  */
25
- constructor(options: AxiosServiceOptions);
26
22
  private processQueue;
27
23
  private setAuthHeader;
28
24
  private initializeInterceptors;
29
- /**
30
- * Returns the number of active (in-flight) requests.
31
- * @returns {number} The number of active requests.
32
- */
33
25
  getActiveRequests(): number;
34
- /**
35
- * Returns the underlying Axios instance.
36
- * @returns {AxiosInstance} The Axios instance.
37
- */
38
26
  getAxiosInstance(): AxiosInstance;
39
- /**
40
- * Cancels all ongoing requests made by this Axios instance.
41
- */
42
27
  cancelAllRequests(): void;
43
- /**
44
- * Sets a default header for all subsequent requests.
45
- * @param {string} key - The header key.
46
- * @param {string} value - The header value.
47
- */
48
28
  setHeader(key: string, value: string): void;
49
- /**
50
- * Removes a default header.
51
- * @param {string} key - The header key to remove.
52
- */
53
29
  removeHeader(key: string): void;
54
30
  }
@@ -0,0 +1,14 @@
1
+ export interface CallbacksConfig {
2
+ onRefreshFailed?: () => void;
3
+ onLogout?: () => void;
4
+ }
5
+ /**
6
+ * Configures the lifecycle callbacks for auth events.
7
+ * @param {CallbacksConfig} config - Callbacks to invoke on refresh failure or logout.
8
+ */
9
+ export declare const configCallbacks: (config: CallbacksConfig) => void;
10
+ /**
11
+ * Returns the configured lifecycle callbacks.
12
+ * @returns {CallbacksConfig}
13
+ */
14
+ export declare const getCallbacksConfig: () => CallbacksConfig;
@@ -3,3 +3,4 @@ export * from './endpointsConfig';
3
3
  export * from './sessionConfig';
4
4
  export * from './keyConfig';
5
5
  export * from './tokenPathsConfig';
6
+ export * from './callbacksConfig';
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
  }
@@ -1246,115 +1271,115 @@ function getRefreshTokenPathsConfig() {
1246
1271
  return refreshTokenPathsConfig;
1247
1272
  }
1248
1273
 
1274
+ let callbacksConfig = {};
1275
+ /**
1276
+ * Configures the lifecycle callbacks for auth events.
1277
+ * @param {CallbacksConfig} config - Callbacks to invoke on refresh failure or logout.
1278
+ */
1279
+ const configCallbacks = (config) => {
1280
+ callbacksConfig = { ...config };
1281
+ };
1249
1282
  /**
1250
- * Removes all stored authentication credentials (access and refresh tokens) from the specified storage locations.
1283
+ * Returns the configured lifecycle callbacks.
1284
+ * @returns {CallbacksConfig}
1285
+ */
1286
+ const getCallbacksConfig = () => callbacksConfig;
1287
+
1288
+ /**
1289
+ * Removes all stored authentication credentials (access + refresh tokens)
1290
+ * from the specified storage location(s).
1251
1291
  *
1252
- * @param {LocationPreference} location - The storage location to clear. Can be 'local' for `localStorage`,
1253
- * 'session' for `sessionStorage`, 'cookie' for cookies, or 'any' to clear all.
1254
- * @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.
1255
1294
  */
1256
1295
  const cleanCredentials = async (location) => {
1257
1296
  const tokensConfig = getTokenConfig();
1258
- if (location === "cookie" || isServer) {
1297
+ const keys = Object.keys(tokensConfig);
1298
+ const tokenItemKeys = keys.map((k) => tokensConfig[k]);
1299
+ const removeCookies = () => {
1259
1300
  const cookieStorage = getCookieStorage();
1260
- Object.keys(tokensConfig).forEach((key) => {
1261
- cookieStorage.removeItem(tokensConfig[key], { path: '/' });
1262
- });
1263
- if (location === "cookie") {
1264
- return;
1265
- }
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();
1266
1325
  }
1267
- Object.keys(tokensConfig).forEach((key) => {
1268
- const itemKey = tokensConfig[key];
1269
- if (location === "local" || location === "any") {
1270
- const storage = getStorage();
1271
- storage?.removeItem(itemKey);
1272
- }
1273
- if (location === "session" || location === "any") {
1274
- const sessionStorage = getSessionStorage();
1275
- sessionStorage?.removeItem(itemKey);
1276
- }
1277
- });
1278
1326
  };
1279
1327
  /**
1280
1328
  * Retrieves and decrypts the access token from the specified storage location.
1281
- *
1282
- * @param {string} secretKey - The secret key to use for decryption.
1283
- * @param {LocationPreference} location - The storage location to search ('local', 'session', 'cookie', or 'any').
1284
- * @returns {Promise<string | null>} A promise that resolves with the decrypted access token, or `null` if it's not found.
1285
1329
  */
1286
1330
  const getAuthToken = async (secretKey, location) => {
1287
1331
  const tokensConfig = getTokenConfig();
1288
- return await getDecryptedItem(tokensConfig.ACCESS_TOKEN, secretKey, location);
1332
+ return getDecryptedItem(tokensConfig.ACCESS_TOKEN, secretKey, location);
1289
1333
  };
1290
1334
  /**
1291
1335
  * Retrieves and decrypts the refresh token from the specified storage location.
1292
- *
1293
- * @param {string} secretKey - The secret key to use for decryption.
1294
- * @param {LocationPreference} location - The storage location to search ('local', 'session', 'cookie', or 'any').
1295
- * @returns {Promise<string | null>} A promise that resolves with the decrypted refresh token, or `null` if it's not found.
1296
1336
  */
1297
1337
  const getAuthRefreshToken = async (secretKey, location) => {
1298
1338
  const tokensConfig = getTokenConfig();
1299
- return await getDecryptedItem(tokensConfig.REFRESH_TOKEN, secretKey, location);
1339
+ return getDecryptedItem(tokensConfig.REFRESH_TOKEN, secretKey, location);
1300
1340
  };
1301
1341
  /**
1302
- * Encrypts and stores the access token in the specified storage location.
1303
- *
1304
- * @param {string} token - The access token to store.
1305
- * @param {string} secretKey - The secret key to use for encryption.
1306
- * @param {LocationPreference} location - The storage location ('local', 'session', or 'cookie').
1307
- * @returns {Promise<void>} A promise that resolves when the token has been stored.
1342
+ * Encrypts and stores the access token.
1308
1343
  */
1309
1344
  const storeAuthToken = async (token, secretKey, location) => {
1310
1345
  const tokensConfig = getTokenConfig();
1311
1346
  await storeEncryptedItem(tokensConfig.ACCESS_TOKEN, token, secretKey, location);
1312
1347
  };
1313
1348
  /**
1314
- * Encrypts and stores the refresh token in the specified storage location.
1315
- *
1316
- * @param {string} token - The refresh token to store.
1317
- * @param {string} secretKey - The secret key to use for encryption.
1318
- * @param {LocationPreference} location - The storage location ('local', 'session', or 'cookie').
1319
- * @returns {Promise<void>} A promise that resolves when the token has been stored.
1349
+ * Encrypts and stores the refresh token.
1320
1350
  */
1321
1351
  const storeAuthRefreshToken = async (token, secretKey, location) => {
1322
1352
  const tokensConfig = getTokenConfig();
1323
1353
  await storeEncryptedItem(tokensConfig.REFRESH_TOKEN, token, secretKey, location);
1324
1354
  };
1325
1355
  /**
1326
- * Verifies the current user's authentication status by checking for a valid, unexpired access token.
1327
- * It searches for the token in all storage locations (sessionStorage, localStorage, cookies).
1328
- * 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.
1329
1358
  *
1330
- * @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.
1331
1360
  */
1332
1361
  const verifyAuth = async () => {
1333
- const sessionPersistence = 'any';
1334
- const handleAuthError = async (message, shouldClean = true) => {
1335
- handleError(message);
1336
- if (shouldClean) {
1337
- await cleanCredentials(sessionPersistence);
1338
- }
1362
+ const token = await getAuthToken(getAppKey(), 'any');
1363
+ if (!token)
1339
1364
  return false;
1340
- };
1341
- const token = await getAuthToken(getAppKey(), sessionPersistence);
1342
- if (!token) {
1343
- return handleAuthError("TOKEN_MISSING: No valid token found");
1344
- }
1345
1365
  try {
1346
1366
  const decoded = jwtDecode(token);
1347
1367
  const currentTime = Date.now() / 1000;
1348
- if (typeof decoded.exp !== "number") {
1349
- 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;
1350
1372
  }
1351
1373
  if (decoded.exp <= currentTime) {
1352
- return handleAuthError("TOKEN_EXPIRED: Token is expired");
1374
+ handleError('TOKEN_EXPIRED: Token is expired');
1375
+ return false;
1353
1376
  }
1354
1377
  return true;
1355
1378
  }
1356
- catch (error) {
1357
- return handleAuthError("TOKEN_INVALID: Invalid token format");
1379
+ catch {
1380
+ handleError('TOKEN_INVALID: Could not decode token');
1381
+ await cleanCredentials('any');
1382
+ return false;
1358
1383
  }
1359
1384
  };
1360
1385
 
@@ -1744,38 +1769,49 @@ function getDefaultAuthFetcher() {
1744
1769
  }
1745
1770
 
1746
1771
  /**
1747
- * Refreshes the access and refresh tokens by making a POST request to the refresh endpoint.
1748
- * It retrieves the current refresh token from storage, sends it to the refresh endpoint,
1749
- * and then stores the new tokens upon a successful response. If the refresh process fails
1750
- * 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.
1751
1781
  *
1752
- * @param {Fetcher} [fetcher] - Optional fetcher function to use for the refresh request. If not provided, uses the default configured fetcher.
1753
- * @returns {Promise<AuthResponse>} A promise that resolves with the new authentication response containing the refreshed tokens.
1754
- * @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.
1755
1783
  */
1756
1784
  const refreshTokens = async (fetcher) => {
1757
1785
  const tokenPaths = getRefreshTokenPathsConfig();
1758
1786
  const endpoints = getEndpointsConfig();
1759
1787
  const secretKey = getAppKey();
1760
1788
  const persistence = await getSessionPersistence();
1761
- const getFetcher = () => fetcher || getDefaultAuthFetcher();
1789
+ const getFetcher = () => fetcher ?? getDefaultAuthFetcher();
1762
1790
  try {
1763
- const refreshTokenFromStorage = await getAuthRefreshToken(secretKey, "any");
1764
- if (!refreshTokenFromStorage) {
1765
- 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.');
1766
1795
  }
1796
+ // Send refresh token in the request body using the configured path key
1797
+ const refreshBodyKey = tokenPaths.refreshTokenPath ?? 'refresh_token';
1767
1798
  const data = await getFetcher()({
1768
1799
  method: 'POST',
1769
1800
  url: endpoints.REFRESH,
1801
+ data: { [refreshBodyKey]: refreshToken },
1770
1802
  });
1771
- const { accessToken, refreshToken } = extractAndValidateTokens(data, tokenPaths, "REFRESH");
1772
- await storeTokens(accessToken, refreshToken, persistence);
1803
+ const { accessToken, refreshToken: newRefreshToken } = extractAndValidateTokens(data, tokenPaths, 'REFRESH');
1804
+ await storeTokens(accessToken, newRefreshToken, persistence);
1773
1805
  return data;
1774
1806
  }
1775
1807
  catch (error) {
1776
1808
  handleError(error);
1777
1809
  await cleanCredentials(persistence);
1778
- if (typeof window !== 'undefined') {
1810
+ const { onRefreshFailed } = getCallbacksConfig();
1811
+ if (onRefreshFailed) {
1812
+ onRefreshFailed();
1813
+ }
1814
+ else if (typeof window !== 'undefined') {
1779
1815
  window.location.reload();
1780
1816
  }
1781
1817
  throw error;
@@ -1822,12 +1858,6 @@ function createAxiosFetcher(axiosInstance) {
1822
1858
  };
1823
1859
  }
1824
1860
 
1825
- /**
1826
- * A service class that encapsulates a customizable Axios instance with built-in interceptors
1827
- * for handling authentication, token refreshing, and request cancellation. It is designed to
1828
- * streamline API communication by automatically attaching authorization headers and managing
1829
- * token refresh logic for 401 Unauthorized responses.
1830
- */
1831
1861
  class AxiosService {
1832
1862
  instance;
1833
1863
  cancelTokenSource;
@@ -1835,26 +1865,30 @@ class AxiosService {
1835
1865
  refreshTokenUrl;
1836
1866
  isRefreshing = false;
1837
1867
  failedQueue = [];
1838
- /**
1839
- * Creates an instance of AxiosService.
1840
- * @param {AxiosServiceOptions} options - Configuration options for the Axios instance, such as `baseURL`, `timeout`, and custom `headers`.
1841
- */
1842
1868
  constructor(options) {
1843
1869
  this.cancelTokenSource = axios.CancelToken.source();
1844
1870
  const endpointsConfig = getEndpointsConfig();
1845
1871
  this.refreshTokenUrl = endpointsConfig.REFRESH;
1846
1872
  this.instance = axios.create({
1847
- baseURL: options.baseURL ?? "",
1873
+ baseURL: options.baseURL ?? '',
1848
1874
  timeout: options.timeout ?? 30000,
1849
1875
  headers: {
1850
- Accept: "application/json",
1851
- "Content-Type": "application/json",
1876
+ Accept: 'application/json',
1877
+ 'Content-Type': 'application/json',
1852
1878
  ...options.headers,
1853
1879
  },
1854
1880
  withCredentials: options.withCredentials ?? false,
1855
1881
  });
1856
- this.initializeInterceptors();
1882
+ if (options.setupAuthInterceptors !== false) {
1883
+ this.initializeInterceptors();
1884
+ }
1857
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
+ */
1858
1892
  processQueue(error, token = null) {
1859
1893
  this.failedQueue.forEach((prom) => {
1860
1894
  if (error) {
@@ -1863,6 +1897,10 @@ class AxiosService {
1863
1897
  else if (token) {
1864
1898
  prom.resolve(token);
1865
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
+ }
1866
1904
  });
1867
1905
  this.failedQueue = [];
1868
1906
  }
@@ -1872,8 +1910,12 @@ class AxiosService {
1872
1910
  }
1873
1911
  }
1874
1912
  initializeInterceptors() {
1913
+ // ── Request: attach Authorization using the session's storage location ──
1875
1914
  this.instance.interceptors.request.use(async (config) => {
1876
- 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);
1877
1919
  if (token) {
1878
1920
  this.setAuthHeader(config, token);
1879
1921
  }
@@ -1884,23 +1926,25 @@ class AxiosService {
1884
1926
  handleError(error);
1885
1927
  return Promise.reject(error);
1886
1928
  });
1929
+ // ── Response: handle 401 with token refresh ──────────────────────────
1887
1930
  this.instance.interceptors.response.use((response) => {
1888
1931
  this.activeRequests--;
1889
1932
  return response;
1890
1933
  }, async (error) => {
1891
1934
  this.activeRequests--;
1935
+ // Do not attempt refresh in SSR — storage is unavailable
1936
+ if (typeof window === 'undefined') {
1937
+ return Promise.reject(error);
1938
+ }
1892
1939
  const originalRequest = error.config;
1893
1940
  const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
1894
1941
  const isRefreshCall = originalRequest?.url === this.refreshTokenUrl;
1895
1942
  const isRetry = originalRequest?._retry === true;
1896
- if (!isAuthError || isRefreshCall || isRetry) {
1897
- handleError(error);
1898
- return Promise.reject(error);
1899
- }
1900
- if (!originalRequest) {
1943
+ if (!isAuthError || isRefreshCall || isRetry || !originalRequest) {
1901
1944
  handleError(error);
1902
1945
  return Promise.reject(error);
1903
1946
  }
1947
+ // Queue concurrent requests while a refresh is already in progress
1904
1948
  if (this.isRefreshing) {
1905
1949
  return new Promise((resolve, reject) => {
1906
1950
  this.failedQueue.push({ resolve, reject });
@@ -1909,25 +1953,22 @@ class AxiosService {
1909
1953
  this.setAuthHeader(originalRequest, newToken);
1910
1954
  return this.instance(originalRequest);
1911
1955
  })
1912
- .catch((err) => {
1913
- return Promise.reject(err);
1914
- });
1956
+ .catch((err) => Promise.reject(err));
1915
1957
  }
1916
1958
  this.isRefreshing = true;
1917
1959
  originalRequest._retry = true;
1918
1960
  try {
1919
1961
  const fetcher = createAxiosFetcher(this.instance);
1920
1962
  await refreshTokens(fetcher);
1921
- const newToken = await getAuthToken(getAppKey(), "any");
1922
- if (newToken) {
1923
- this.processQueue(null, newToken);
1924
- this.setAuthHeader(originalRequest, newToken);
1925
- }
1926
- else {
1927
- 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.');
1928
1967
  this.processQueue(refreshError, null);
1929
1968
  throw refreshError;
1930
1969
  }
1970
+ this.processQueue(null, newToken);
1971
+ this.setAuthHeader(originalRequest, newToken);
1931
1972
  this.isRefreshing = false;
1932
1973
  return this.instance(originalRequest);
1933
1974
  }
@@ -1939,39 +1980,19 @@ class AxiosService {
1939
1980
  }
1940
1981
  });
1941
1982
  }
1942
- /**
1943
- * Returns the number of active (in-flight) requests.
1944
- * @returns {number} The number of active requests.
1945
- */
1946
1983
  getActiveRequests() {
1947
1984
  return this.activeRequests;
1948
1985
  }
1949
- /**
1950
- * Returns the underlying Axios instance.
1951
- * @returns {AxiosInstance} The Axios instance.
1952
- */
1953
1986
  getAxiosInstance() {
1954
1987
  return this.instance;
1955
1988
  }
1956
- /**
1957
- * Cancels all ongoing requests made by this Axios instance.
1958
- */
1959
1989
  cancelAllRequests() {
1960
- this.cancelTokenSource.cancel("Operation canceled by the user.");
1990
+ this.cancelTokenSource.cancel('Operation canceled by the user.');
1961
1991
  this.cancelTokenSource = axios.CancelToken.source();
1962
1992
  }
1963
- /**
1964
- * Sets a default header for all subsequent requests.
1965
- * @param {string} key - The header key.
1966
- * @param {string} value - The header value.
1967
- */
1968
1993
  setHeader(key, value) {
1969
1994
  this.instance.defaults.headers.common[key] = value;
1970
1995
  }
1971
- /**
1972
- * Removes a default header.
1973
- * @param {string} key - The header key to remove.
1974
- */
1975
1996
  removeHeader(key) {
1976
1997
  delete this.instance.defaults.headers.common[key];
1977
1998
  }
@@ -4279,7 +4300,11 @@ function useAuth(fetcher) {
4279
4300
  }
4280
4301
  finally {
4281
4302
  await cleanCredentials(await getSessionPersistence());
4282
- if (typeof window !== 'undefined') {
4303
+ const { onLogout } = getCallbacksConfig();
4304
+ if (onLogout) {
4305
+ onLogout();
4306
+ }
4307
+ else if (typeof window !== 'undefined') {
4283
4308
  window.location.reload();
4284
4309
  }
4285
4310
  }
@@ -4406,9 +4431,14 @@ const ArexVueCore = {
4406
4431
  baseURL: options.axios.baseURL,
4407
4432
  headers: options.axios.headers,
4408
4433
  timeout: options.axios.timeout,
4409
- withCredentials: options.axios.withCredentials
4434
+ withCredentials: options.axios.withCredentials,
4435
+ setupAuthInterceptors: options.axios.setupAuthInterceptors,
4436
+ });
4437
+ configCallbacks({
4438
+ onRefreshFailed: options.onRefreshFailed,
4439
+ onLogout: options.onLogout,
4410
4440
  });
4411
4441
  },
4412
4442
  };
4413
4443
 
4414
- export { AppTypes, ArchiveTypes, ArexVueCore, AudioTypes, AuthError, AxiosService, BaseError, ContentTypeEnum, DocumentTypes, ERROR_MESSAGES, ERROR_STYLES, ErrorEnum, ErrorMessages, ErrorStyles, ExceptionEnum, FontTypes, ImageTypes, KeyCodeEnum, NetworkError, OtherTypes, RestStd, ScreenBreakpoint, ScreenSize, ServerError, StorageKeyEnum, StorageTypeEnum, TextTypes, ValidationError, VideoTypes, ab2hex, addCustomKeyboardShortcut, addDays, addDoubleClickListener, addKeyListener, ageAtDate, axiosFetch, blobToFormData, bufferToBlob, calculateAge, cleanCredentials, clickOutside, compareObject, configAppKey, configAuthFetcher, configAxios, configEndpoints, configRefreshTokenPaths, configSession, configTokenKeys, configTokenPaths, copyToClipboard, countWords, createAxiosFetcher, createKeyMap, createOfetchFetcher, customShortcut, daysBetween, daysToNextBirthday, debounce, debounceAsync, debounceAsyncValidator, debounceAsyncWithImmediate, debounceLeading, debounceLeadingTrailing, debounceTrailing, decrypt, deepClone, deepEqual, deepMerge, detectKeyHold, disableCopy, disableF12Key, disableMouseButtons, disableRightClick, disableSpecificKeys, downloadBlob, enableMouseButtons, enableRightClick, enableSpecificKeys, encrypt, exportToCSV, exportToExcel, exportToJSON, exportToText, exportToXML, extractAndValidateTokens, filterObjectByKeys, flattenObject, formDataToObject, formatDate, generateRandomString, getAppKey, getAuthRefreshToken, getAuthToken, getConfiguredAxiosInstance, getCookieStorage, getDecryptedItem, getDefaultAuthFetcher, getEndOfMonth, getEndpointsConfig, getObjectDifferences, getObjectKeys, getPreferredStorage, getQueryParam, getRefreshTokenPathsConfig, getSessionConfig, getSessionId, getSessionPersistence, getSessionStorage, getStartOfMonth, getStorage, getTokenConfig, getTokenPathsConfig, handleError, hasNestedProperties, hex2ab, importKey, isClient, isEmptyObject, isLeapYear, isServer, isStrongPassword, isValidAge, isValidCreditCard, isValidDate, isValidEmail, isValidExpiryDate, isValidHexColor, isValidHexColorAlpha, isValidHexNumber, isValidIP, isValidPhoneNumber, isValidSSN, isValidTime, isValidURL, isValidUsername, isValidZIP, lowerFirst, objectToFormData, objectToFormDataEnhanced, objectToQueryString, openWindow, parseDate, proxyToPlainObject, readFileAsDataURL, readFileAsText, refreshTokens, registerKeyboardShortcuts, removeAccent, removeClickOutside, removeCustomKeyboardShortcut, removeCustomShortcuts, removeDoubleClickListener, removeEmptyProperties, removeKeyListeners, replaceAll, retryWithBackoff, reverseString, safeGet, screenMap, scrollToTop, setDefaultAuthFetcherFactory, simulateKeyPress, stopDetectingKeyHold, storeAuthRefreshToken, storeAuthToken, storeEncryptedItem, storeTokens, stringToBlob, subtractDays, throttle, toCamelCase, toKebabCase, toggleTabNavigation, truncateString, unregisterKeyboardShortcuts, upperFirst, useAuth, useBreakpoint, useFetch, useFilter, usePagination, useSorter, validateAlphanumeric, validateLetters, validateNumbers, verifyAuth };
4444
+ export { AppTypes, ArchiveTypes, ArexVueCore, AudioTypes, AuthError, AxiosService, BaseError, ContentTypeEnum, DocumentTypes, ERROR_MESSAGES, ERROR_STYLES, ErrorEnum, ErrorMessages, ErrorStyles, ExceptionEnum, FontTypes, ImageTypes, KeyCodeEnum, NetworkError, OtherTypes, RestStd, ScreenBreakpoint, ScreenSize, ServerError, StorageKeyEnum, StorageTypeEnum, TextTypes, ValidationError, VideoTypes, ab2hex, addCustomKeyboardShortcut, addDays, addDoubleClickListener, addKeyListener, ageAtDate, axiosFetch, blobToFormData, bufferToBlob, calculateAge, cleanCredentials, clickOutside, compareObject, configAppKey, configAuthFetcher, configAxios, configCallbacks, configEndpoints, configRefreshTokenPaths, configSession, configTokenKeys, configTokenPaths, copyToClipboard, countWords, createAxiosFetcher, createKeyMap, createOfetchFetcher, customShortcut, daysBetween, daysToNextBirthday, debounce, debounceAsync, debounceAsyncValidator, debounceAsyncWithImmediate, debounceLeading, debounceLeadingTrailing, debounceTrailing, decrypt, deepClone, deepEqual, deepMerge, detectKeyHold, disableCopy, disableF12Key, disableMouseButtons, disableRightClick, disableSpecificKeys, downloadBlob, enableMouseButtons, enableRightClick, enableSpecificKeys, encrypt, exportToCSV, exportToExcel, exportToJSON, exportToText, exportToXML, extractAndValidateTokens, filterObjectByKeys, flattenObject, formDataToObject, formatDate, generateRandomString, getAppKey, getAuthRefreshToken, getAuthToken, getCallbacksConfig, getConfiguredAxiosInstance, getCookieStorage, getDecryptedItem, getDefaultAuthFetcher, getEndOfMonth, getEndpointsConfig, getObjectDifferences, getObjectKeys, getPreferredStorage, getQueryParam, getRefreshTokenPathsConfig, getSessionConfig, getSessionId, getSessionPersistence, getSessionStorage, getStartOfMonth, getStorage, getTokenConfig, getTokenPathsConfig, handleError, hasNestedProperties, hex2ab, importKey, isClient, isEmptyObject, isLeapYear, isServer, isStrongPassword, isValidAge, isValidCreditCard, isValidDate, isValidEmail, isValidExpiryDate, isValidHexColor, isValidHexColorAlpha, isValidHexNumber, isValidIP, isValidPhoneNumber, isValidSSN, isValidTime, isValidURL, isValidUsername, isValidZIP, lowerFirst, objectToFormData, objectToFormDataEnhanced, objectToQueryString, openWindow, parseDate, proxyToPlainObject, readFileAsDataURL, readFileAsText, refreshTokens, registerKeyboardShortcuts, removeAccent, removeClickOutside, removeCustomKeyboardShortcut, removeCustomShortcuts, removeDoubleClickListener, removeEmptyProperties, removeKeyListeners, replaceAll, retryWithBackoff, reverseString, safeGet, screenMap, scrollToTop, setDefaultAuthFetcherFactory, simulateKeyPress, stopDetectingKeyHold, storeAuthRefreshToken, storeAuthToken, storeEncryptedItem, storeTokens, stringToBlob, subtractDays, throttle, toCamelCase, toKebabCase, toggleTabNavigation, truncateString, unregisterKeyboardShortcuts, upperFirst, useAuth, useBreakpoint, useFetch, useFilter, usePagination, useSorter, validateAlphanumeric, validateLetters, validateNumbers, verifyAuth };
@@ -1,51 +1,32 @@
1
- import { LocationPreference } from "@/types/SessionConfig";
1
+ import { LocationPreference } from '@/types/SessionConfig';
2
2
  /**
3
- * Removes all stored authentication credentials (access and refresh tokens) from the specified storage locations.
3
+ * Removes all stored authentication credentials (access + refresh tokens)
4
+ * from the specified storage location(s).
4
5
  *
5
- * @param {LocationPreference} location - The storage location to clear. Can be 'local' for `localStorage`,
6
- * 'session' for `sessionStorage`, 'cookie' for cookies, or 'any' to clear all.
7
- * @returns {Promise<void>} A promise that resolves when the credentials have been cleared.
6
+ * With location='any', ALL storage locations are cleared including cookies.
7
+ * This prevents orphaned tokens surviving a logout.
8
8
  */
9
9
  export declare const cleanCredentials: (location: LocationPreference) => Promise<void>;
10
10
  /**
11
11
  * Retrieves and decrypts the access token from the specified storage location.
12
- *
13
- * @param {string} secretKey - The secret key to use for decryption.
14
- * @param {LocationPreference} location - The storage location to search ('local', 'session', 'cookie', or 'any').
15
- * @returns {Promise<string | null>} A promise that resolves with the decrypted access token, or `null` if it's not found.
16
12
  */
17
13
  export declare const getAuthToken: (secretKey: string, location: LocationPreference) => Promise<string | null>;
18
14
  /**
19
15
  * Retrieves and decrypts the refresh token from the specified storage location.
20
- *
21
- * @param {string} secretKey - The secret key to use for decryption.
22
- * @param {LocationPreference} location - The storage location to search ('local', 'session', 'cookie', or 'any').
23
- * @returns {Promise<string | null>} A promise that resolves with the decrypted refresh token, or `null` if it's not found.
24
16
  */
25
17
  export declare const getAuthRefreshToken: (secretKey: string, location: LocationPreference) => Promise<string | null>;
26
18
  /**
27
- * Encrypts and stores the access token in the specified storage location.
28
- *
29
- * @param {string} token - The access token to store.
30
- * @param {string} secretKey - The secret key to use for encryption.
31
- * @param {LocationPreference} location - The storage location ('local', 'session', or 'cookie').
32
- * @returns {Promise<void>} A promise that resolves when the token has been stored.
19
+ * Encrypts and stores the access token.
33
20
  */
34
21
  export declare const storeAuthToken: (token: string, secretKey: string, location: LocationPreference) => Promise<void>;
35
22
  /**
36
- * Encrypts and stores the refresh token in the specified storage location.
37
- *
38
- * @param {string} token - The refresh token to store.
39
- * @param {string} secretKey - The secret key to use for encryption.
40
- * @param {LocationPreference} location - The storage location ('local', 'session', or 'cookie').
41
- * @returns {Promise<void>} A promise that resolves when the token has been stored.
23
+ * Encrypts and stores the refresh token.
42
24
  */
43
25
  export declare const storeAuthRefreshToken: (token: string, secretKey: string, location: LocationPreference) => Promise<void>;
44
26
  /**
45
- * Verifies the current user's authentication status by checking for a valid, unexpired access token.
46
- * It searches for the token in all storage locations (sessionStorage, localStorage, cookies).
47
- * If the token is missing, malformed, or expired, it logs the issue, clears credentials, and returns `false`.
27
+ * Verifies the current user's authentication status by checking for a
28
+ * valid, unexpired access token across all storage locations.
48
29
  *
49
- * @returns {Promise<boolean>} A promise that resolves to `true` if the user is authenticated, and `false` otherwise.
30
+ * Returns false (without throwing) if the token is missing, malformed, or expired.
50
31
  */
51
32
  export declare const verifyAuth: () => Promise<boolean>;
@@ -1,12 +1,15 @@
1
- import { AuthResponse, Fetcher } from "@/types";
1
+ import { AuthResponse, Fetcher } from '@/types';
2
2
  /**
3
- * Refreshes the access and refresh tokens by making a POST request to the refresh endpoint.
4
- * It retrieves the current refresh token from storage, sends it to the refresh endpoint,
5
- * and then stores the new tokens upon a successful response. If the refresh process fails
6
- * or no refresh token is found, it clears all credentials and reloads the page.
3
+ * Refreshes the access and refresh tokens.
7
4
  *
8
- * @param {Fetcher} [fetcher] - Optional fetcher function to use for the refresh request. If not provided, uses the default configured fetcher.
9
- * @returns {Promise<AuthResponse>} A promise that resolves with the new authentication response containing the refreshed tokens.
10
- * @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.
5
+ * Fixes over the original:
6
+ * 1. Uses `persistence` (the location where tokens were originally stored)
7
+ * instead of hardcoding `"any"` so cookies / localStorage / sessionStorage
8
+ * are searched in the right place.
9
+ * 2. Sends the refresh token in the request body using the configured
10
+ * `refreshTokenPaths.refreshTokenPath` as the body key, so the backend
11
+ * always receives it regardless of withCredentials or cookie settings.
12
+ *
13
+ * On failure: clears credentials and calls `onRefreshFailed` callback.
11
14
  */
12
15
  export declare const refreshTokens: (fetcher?: Fetcher) => Promise<AuthResponse>;
@@ -39,4 +39,14 @@ export interface ArexVueCoreOptions {
39
39
  };
40
40
  /** The configuration options for the underlying Axios instance. */
41
41
  axios: AxiosServiceOptions;
42
+ /**
43
+ * Called when a token refresh attempt fails (e.g., to redirect to login via Vue Router).
44
+ * Falls back to `window.location.reload()` if not provided.
45
+ */
46
+ onRefreshFailed?: () => void;
47
+ /**
48
+ * Called after a successful logout (e.g., to redirect to login via Vue Router).
49
+ * Falls back to `window.location.reload()` if not provided.
50
+ */
51
+ onLogout?: () => void;
42
52
  }
@@ -12,4 +12,10 @@ export interface AxiosServiceOptions {
12
12
  timeout?: number;
13
13
  /** A boolean indicating whether cross-site Access-Control requests should be made using credentials. */
14
14
  withCredentials?: boolean;
15
+ /**
16
+ * Whether to mount the authentication interceptors (token attachment + 401 refresh).
17
+ * Set to `false` in SSR environments where browser storage is unavailable.
18
+ * Defaults to `true`.
19
+ */
20
+ setupAuthInterceptors?: boolean;
15
21
  }
@@ -1,44 +1,24 @@
1
1
  /**
2
- * Converts an `ArrayBuffer` or `Uint8Array` into a hexadecimal string representation.
3
- *
4
- * @param {ArrayBuffer | Uint8Array} buffer - The buffer to convert.
5
- * @returns {string} The resulting hexadecimal string.
2
+ * Converts an `ArrayBuffer` or `Uint8Array` into a hexadecimal string.
6
3
  */
7
4
  export declare function ab2hex(buffer: ArrayBuffer | Uint8Array): string;
8
5
  /**
9
6
  * Converts a hexadecimal string into a `Uint8Array`.
10
- *
11
- * @param {string} hex - The hexadecimal string to convert.
12
- * @returns {Uint8Array} The resulting `Uint8Array`.
13
- * @throws {TypeError} If the input is not a string.
14
- * @throws {Error} If the hexadecimal string has an invalid format or an odd length.
15
7
  */
16
8
  export declare function hex2ab(hex: string): Uint8Array;
17
9
  /**
18
- * Derives a `CryptoKey` for AES-CBC encryption from a plain-text secret key.
19
- * It uses SHA-256 to hash the secret key, ensuring a fixed-length key suitable for the Web Crypto API.
10
+ * Derives a CryptoKey for AES-CBC-256 from a plain-text secret using SHA-256.
20
11
  *
21
- * @param {string} secretKey - The plain-text secret key.
22
- * @returns {Promise<CryptoKey>} A promise that resolves with the derived `CryptoKey`.
23
- * @throws {Error} If the `secretKey` is null or empty.
12
+ * @throws {Error} If secretKey is empty or Web Crypto API is unavailable.
24
13
  */
25
14
  export declare function importKey(secretKey: string): Promise<CryptoKey>;
26
15
  /**
27
- * Encrypts a plain-text value using AES-CBC with a given secret key.
28
- * A random 16-byte initialization vector (IV) is generated for each encryption.
29
- *
30
- * @param {string} value - The plain-text string to encrypt.
31
- * @param {string} secretKey - The secret key to use for encryption.
32
- * @returns {Promise<string>} A promise that resolves with a concatenated hexadecimal string of the IV and the ciphertext.
33
- * @throws {Error} If the `secretKey` is null or empty.
16
+ * Encrypts a plain-text string using AES-CBC-256.
17
+ * A unique 16-byte IV is generated per call.
18
+ * Output format: IV_hex (32 chars) + ciphertext_hex.
34
19
  */
35
20
  export declare function encrypt(value: string, secretKey: string): Promise<string>;
36
21
  /**
37
- * Decrypts a hexadecimal string (IV + ciphertext) using AES-CBC with a given secret key.
38
- *
39
- * @param {string} encryptedValue - The concatenated hexadecimal string of the IV and ciphertext.
40
- * @param {string} secretKey - The secret key to use for decryption.
41
- * @returns {Promise<string>} A promise that resolves with the decrypted plain-text string.
42
- * @throws {Error} If the encrypted value is null, empty, or too short, or if the `secretKey` is invalid.
22
+ * Decrypts a hex string (IV_hex + ciphertext_hex) produced by `encrypt()`.
43
23
  */
44
24
  export declare function decrypt(encryptedValue: string, secretKey: string): Promise<string>;
@@ -1,26 +1,37 @@
1
- import { LocationPreference } from "@/types";
2
- import { CookieOptions } from "./ssr";
1
+ import { LocationPreference } from '@/types';
2
+ import { CookieOptions } from './ssr';
3
3
  /**
4
- * Encrypts and stores a key-value pair in either `localStorage`, `sessionStorage`, or cookies.
5
- * Cookies are automatically used in SSR environments and can be explicitly requested.
6
- * Cookies include security options: Secure (HTTPS only), SameSite (CSRF protection), and encryption.
4
+ * Encrypts and stores a value in the requested storage.
7
5
  *
8
- * @param {string} key - The key for the storage item.
9
- * @param {string} value - The string value to encrypt and store.
10
- * @param {string} secretKey - The secret key to use for encryption.
11
- * @param {LocationPreference} location - The storage location: 'local' for `localStorage`, 'session' for `sessionStorage`, 'cookie' for cookies, or 'any' for retrieval.
12
- * @param {CookieOptions} [cookieOptions] - Optional cookie-specific options (only used when location is 'cookie').
13
- * @returns {Promise<void>} A promise that resolves when the item has been stored.
6
+ * | location | where it goes | persistence |
7
+ * |-----------|----------------------------|---------------------|
8
+ * | 'cookie' | document.cookie | expires option |
9
+ * | 'local' | localStorage | until explicitly cleared |
10
+ * | 'any' | localStorage | until explicitly cleared |
11
+ * | 'session' | sessionStorage | until tab closes |
12
+ *
13
+ * Note: 'any' stores in localStorage (same as 'local') for maximum
14
+ * persistence. Previously it stored in sessionStorage — that was a bug.
15
+ *
16
+ * In SSR environments cookies are always used regardless of location
17
+ * (localStorage / sessionStorage do not exist on the server).
14
18
  */
15
19
  export declare function storeEncryptedItem(key: string, value: string, secretKey: string, location: LocationPreference, cookieOptions?: CookieOptions): Promise<void>;
16
20
  /**
17
- * Retrieves and decrypts an item from `localStorage`, `sessionStorage`, or cookies.
18
- * When location is 'any', checks in order: sessionStorage, localStorage, cookies.
19
- * Cookies are automatically checked in SSR environments.
21
+ * Retrieves and decrypts a value from storage.
22
+ *
23
+ * Search order by location:
24
+ *
25
+ * | location | search order |
26
+ * |-----------|-------------------------------------------|
27
+ * | 'cookie' | cookies only |
28
+ * | 'local' | localStorage only |
29
+ * | 'session' | sessionStorage only |
30
+ * | 'any' | sessionStorage → localStorage → cookies |
31
+ *
32
+ * In SSR, cookies are always checked first (localStorage / sessionStorage
33
+ * are not available on the server).
20
34
  *
21
- * @param {string} key - The key of the item to retrieve.
22
- * @param {string} secretKey - The secret key to use for decryption.
23
- * @param {LocationPreference} location - The storage location to search: 'local', 'session', 'cookie', or 'any' (checks session, local, cookie in that order).
24
- * @returns {Promise<string | null>} A promise that resolves with the decrypted value, or `null` if the item is not found or decryption fails.
35
+ * Returns null if the key is not found or decryption fails.
25
36
  */
26
37
  export declare function getDecryptedItem(key: string, secretKey: string, location: LocationPreference): Promise<string | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arex95/vue-core",
3
- "version": "3.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "Opinionated Vue Core",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",