@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.
- package/dist/config/axios/axiosConfig.d.ts +8 -32
- package/dist/config/global/callbacksConfig.d.ts +14 -0
- package/dist/config/global/index.d.ts +1 -0
- package/dist/index.mjs +265 -235
- package/dist/services/credentials.d.ts +10 -29
- package/dist/services/refreshTokens.d.ts +11 -8
- package/dist/types/ArexVueCoreOptions.d.ts +10 -0
- package/dist/types/AxiosServiceOptions.d.ts +6 -0
- package/dist/utils/encryption.d.ts +7 -27
- package/dist/utils/storage.d.ts +29 -18
- package/package.json +1 -1
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import { AxiosInstance } from
|
|
2
|
-
import { AxiosServiceOptions } from
|
|
3
|
-
declare module
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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;
|
package/dist/index.mjs
CHANGED
|
@@ -35,33 +35,47 @@ function getTokenConfig() {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Returns the Web Crypto API instance, validating availability.
|
|
39
39
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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,
|
|
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 !==
|
|
58
|
-
throw new TypeError(
|
|
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(
|
|
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
|
|
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
|
-
* @
|
|
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(
|
|
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
|
|
86
|
-
return
|
|
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
|
|
90
|
-
* A
|
|
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 =
|
|
108
|
+
const iv = wc.getRandomValues(new Uint8Array(16));
|
|
100
109
|
const encodedValue = new TextEncoder().encode(value);
|
|
101
|
-
const ciphertext = await
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
123
|
-
const
|
|
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(`
|
|
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(
|
|
131
|
+
throw new Error('Ciphertext is empty. Nothing to decrypt.');
|
|
131
132
|
}
|
|
132
|
-
const decryptedBuffer = await
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 ??
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
216
|
-
*
|
|
217
|
-
*
|
|
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
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
247
|
-
*
|
|
248
|
-
*
|
|
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
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
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
|
-
|
|
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
|
|
272
|
+
catch {
|
|
265
273
|
return null;
|
|
266
274
|
}
|
|
267
275
|
}
|
|
268
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
301
|
+
catch {
|
|
280
302
|
return null;
|
|
281
303
|
}
|
|
282
304
|
}
|
|
283
305
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1253
|
-
*
|
|
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
|
-
|
|
1297
|
+
const keys = Object.keys(tokensConfig);
|
|
1298
|
+
const tokenItemKeys = keys.map((k) => tokensConfig[k]);
|
|
1299
|
+
const removeCookies = () => {
|
|
1259
1300
|
const cookieStorage = getCookieStorage();
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
|
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
|
|
1339
|
+
return getDecryptedItem(tokensConfig.REFRESH_TOKEN, secretKey, location);
|
|
1300
1340
|
};
|
|
1301
1341
|
/**
|
|
1302
|
-
* Encrypts and stores the access token
|
|
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
|
|
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
|
|
1327
|
-
*
|
|
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
|
-
*
|
|
1359
|
+
* Returns false (without throwing) if the token is missing, malformed, or expired.
|
|
1331
1360
|
*/
|
|
1332
1361
|
const verifyAuth = async () => {
|
|
1333
|
-
const
|
|
1334
|
-
|
|
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 !==
|
|
1349
|
-
|
|
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
|
-
|
|
1374
|
+
handleError('TOKEN_EXPIRED: Token is expired');
|
|
1375
|
+
return false;
|
|
1353
1376
|
}
|
|
1354
1377
|
return true;
|
|
1355
1378
|
}
|
|
1356
|
-
catch
|
|
1357
|
-
|
|
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
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
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
|
-
*
|
|
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
|
|
1789
|
+
const getFetcher = () => fetcher ?? getDefaultAuthFetcher();
|
|
1762
1790
|
try {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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,
|
|
1772
|
-
await storeTokens(accessToken,
|
|
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
|
-
|
|
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:
|
|
1851
|
-
|
|
1876
|
+
Accept: 'application/json',
|
|
1877
|
+
'Content-Type': 'application/json',
|
|
1852
1878
|
...options.headers,
|
|
1853
1879
|
},
|
|
1854
1880
|
withCredentials: options.withCredentials ?? false,
|
|
1855
1881
|
});
|
|
1856
|
-
|
|
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
|
-
|
|
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
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
1
|
+
import { LocationPreference } from '@/types/SessionConfig';
|
|
2
2
|
/**
|
|
3
|
-
* Removes all stored authentication credentials (access
|
|
3
|
+
* Removes all stored authentication credentials (access + refresh tokens)
|
|
4
|
+
* from the specified storage location(s).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
|
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
|
|
46
|
-
*
|
|
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
|
-
*
|
|
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
|
|
1
|
+
import { AuthResponse, Fetcher } from '@/types';
|
|
2
2
|
/**
|
|
3
|
-
* Refreshes the access and refresh tokens
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
28
|
-
* A
|
|
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
|
|
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>;
|
package/dist/utils/storage.d.ts
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
|
-
import { LocationPreference } from
|
|
2
|
-
import { CookieOptions } from
|
|
1
|
+
import { LocationPreference } from '@/types';
|
|
2
|
+
import { CookieOptions } from './ssr';
|
|
3
3
|
/**
|
|
4
|
-
* Encrypts and stores a
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
*
|
|
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>;
|