@arex95/vue-core 3.3.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/index.mjs +228 -230
- package/dist/services/credentials.d.ts +10 -29
- package/dist/services/refreshTokens.d.ts +11 -8
- 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
|
}
|
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
|
}
|
|
@@ -1261,114 +1286,100 @@ const configCallbacks = (config) => {
|
|
|
1261
1286
|
const getCallbacksConfig = () => callbacksConfig;
|
|
1262
1287
|
|
|
1263
1288
|
/**
|
|
1264
|
-
* Removes all stored authentication credentials (access
|
|
1289
|
+
* Removes all stored authentication credentials (access + refresh tokens)
|
|
1290
|
+
* from the specified storage location(s).
|
|
1265
1291
|
*
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
* @returns {Promise<void>} A promise that resolves when the credentials have been cleared.
|
|
1292
|
+
* With location='any', ALL storage locations are cleared — including cookies.
|
|
1293
|
+
* This prevents orphaned tokens surviving a logout.
|
|
1269
1294
|
*/
|
|
1270
1295
|
const cleanCredentials = async (location) => {
|
|
1271
1296
|
const tokensConfig = getTokenConfig();
|
|
1272
|
-
|
|
1297
|
+
const keys = Object.keys(tokensConfig);
|
|
1298
|
+
const tokenItemKeys = keys.map((k) => tokensConfig[k]);
|
|
1299
|
+
const removeCookies = () => {
|
|
1273
1300
|
const cookieStorage = getCookieStorage();
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1301
|
+
tokenItemKeys.forEach((itemKey) => cookieStorage.removeItem(itemKey, { path: '/' }));
|
|
1302
|
+
};
|
|
1303
|
+
const removeFromStorage = (storage) => {
|
|
1304
|
+
tokenItemKeys.forEach((itemKey) => storage?.removeItem(itemKey));
|
|
1305
|
+
};
|
|
1306
|
+
if (location === 'cookie') {
|
|
1307
|
+
removeCookies();
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (isServer) {
|
|
1311
|
+
removeCookies();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
// Client — clear every requested location
|
|
1315
|
+
if (location === 'local' || location === 'any') {
|
|
1316
|
+
removeFromStorage(getStorage());
|
|
1317
|
+
}
|
|
1318
|
+
if (location === 'session' || location === 'any') {
|
|
1319
|
+
removeFromStorage(getSessionStorage());
|
|
1320
|
+
}
|
|
1321
|
+
// 'any' also clears cookies so that tokens stored explicitly with
|
|
1322
|
+
// location='cookie' don't survive a logout
|
|
1323
|
+
if (location === 'any') {
|
|
1324
|
+
removeCookies();
|
|
1280
1325
|
}
|
|
1281
|
-
Object.keys(tokensConfig).forEach((key) => {
|
|
1282
|
-
const itemKey = tokensConfig[key];
|
|
1283
|
-
if (location === "local" || location === "any") {
|
|
1284
|
-
const storage = getStorage();
|
|
1285
|
-
storage?.removeItem(itemKey);
|
|
1286
|
-
}
|
|
1287
|
-
if (location === "session" || location === "any") {
|
|
1288
|
-
const sessionStorage = getSessionStorage();
|
|
1289
|
-
sessionStorage?.removeItem(itemKey);
|
|
1290
|
-
}
|
|
1291
|
-
});
|
|
1292
1326
|
};
|
|
1293
1327
|
/**
|
|
1294
1328
|
* Retrieves and decrypts the access token from the specified storage location.
|
|
1295
|
-
*
|
|
1296
|
-
* @param {string} secretKey - The secret key to use for decryption.
|
|
1297
|
-
* @param {LocationPreference} location - The storage location to search ('local', 'session', 'cookie', or 'any').
|
|
1298
|
-
* @returns {Promise<string | null>} A promise that resolves with the decrypted access token, or `null` if it's not found.
|
|
1299
1329
|
*/
|
|
1300
1330
|
const getAuthToken = async (secretKey, location) => {
|
|
1301
1331
|
const tokensConfig = getTokenConfig();
|
|
1302
|
-
return
|
|
1332
|
+
return getDecryptedItem(tokensConfig.ACCESS_TOKEN, secretKey, location);
|
|
1303
1333
|
};
|
|
1304
1334
|
/**
|
|
1305
1335
|
* Retrieves and decrypts the refresh token from the specified storage location.
|
|
1306
|
-
*
|
|
1307
|
-
* @param {string} secretKey - The secret key to use for decryption.
|
|
1308
|
-
* @param {LocationPreference} location - The storage location to search ('local', 'session', 'cookie', or 'any').
|
|
1309
|
-
* @returns {Promise<string | null>} A promise that resolves with the decrypted refresh token, or `null` if it's not found.
|
|
1310
1336
|
*/
|
|
1311
1337
|
const getAuthRefreshToken = async (secretKey, location) => {
|
|
1312
1338
|
const tokensConfig = getTokenConfig();
|
|
1313
|
-
return
|
|
1339
|
+
return getDecryptedItem(tokensConfig.REFRESH_TOKEN, secretKey, location);
|
|
1314
1340
|
};
|
|
1315
1341
|
/**
|
|
1316
|
-
* Encrypts and stores the access token
|
|
1317
|
-
*
|
|
1318
|
-
* @param {string} token - The access token to store.
|
|
1319
|
-
* @param {string} secretKey - The secret key to use for encryption.
|
|
1320
|
-
* @param {LocationPreference} location - The storage location ('local', 'session', or 'cookie').
|
|
1321
|
-
* @returns {Promise<void>} A promise that resolves when the token has been stored.
|
|
1342
|
+
* Encrypts and stores the access token.
|
|
1322
1343
|
*/
|
|
1323
1344
|
const storeAuthToken = async (token, secretKey, location) => {
|
|
1324
1345
|
const tokensConfig = getTokenConfig();
|
|
1325
1346
|
await storeEncryptedItem(tokensConfig.ACCESS_TOKEN, token, secretKey, location);
|
|
1326
1347
|
};
|
|
1327
1348
|
/**
|
|
1328
|
-
* Encrypts and stores the refresh token
|
|
1329
|
-
*
|
|
1330
|
-
* @param {string} token - The refresh token to store.
|
|
1331
|
-
* @param {string} secretKey - The secret key to use for encryption.
|
|
1332
|
-
* @param {LocationPreference} location - The storage location ('local', 'session', or 'cookie').
|
|
1333
|
-
* @returns {Promise<void>} A promise that resolves when the token has been stored.
|
|
1349
|
+
* Encrypts and stores the refresh token.
|
|
1334
1350
|
*/
|
|
1335
1351
|
const storeAuthRefreshToken = async (token, secretKey, location) => {
|
|
1336
1352
|
const tokensConfig = getTokenConfig();
|
|
1337
1353
|
await storeEncryptedItem(tokensConfig.REFRESH_TOKEN, token, secretKey, location);
|
|
1338
1354
|
};
|
|
1339
1355
|
/**
|
|
1340
|
-
* Verifies the current user's authentication status by checking for a
|
|
1341
|
-
*
|
|
1342
|
-
* If the token is missing, malformed, or expired, it logs the issue, clears credentials, and returns `false`.
|
|
1356
|
+
* Verifies the current user's authentication status by checking for a
|
|
1357
|
+
* valid, unexpired access token across all storage locations.
|
|
1343
1358
|
*
|
|
1344
|
-
*
|
|
1359
|
+
* Returns false (without throwing) if the token is missing, malformed, or expired.
|
|
1345
1360
|
*/
|
|
1346
1361
|
const verifyAuth = async () => {
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
handleError(message);
|
|
1350
|
-
if (shouldClean) {
|
|
1351
|
-
await cleanCredentials(sessionPersistence);
|
|
1352
|
-
}
|
|
1362
|
+
const token = await getAuthToken(getAppKey(), 'any');
|
|
1363
|
+
if (!token)
|
|
1353
1364
|
return false;
|
|
1354
|
-
};
|
|
1355
|
-
const token = await getAuthToken(getAppKey(), sessionPersistence);
|
|
1356
|
-
if (!token) {
|
|
1357
|
-
return handleAuthError("TOKEN_MISSING: No valid token found", false);
|
|
1358
|
-
}
|
|
1359
1365
|
try {
|
|
1360
1366
|
const decoded = jwtDecode(token);
|
|
1361
1367
|
const currentTime = Date.now() / 1000;
|
|
1362
|
-
if (typeof decoded.exp !==
|
|
1363
|
-
|
|
1368
|
+
if (typeof decoded.exp !== 'number') {
|
|
1369
|
+
handleError('TOKEN_INVALID: Invalid expiration format');
|
|
1370
|
+
await cleanCredentials('any');
|
|
1371
|
+
return false;
|
|
1364
1372
|
}
|
|
1365
1373
|
if (decoded.exp <= currentTime) {
|
|
1366
|
-
|
|
1374
|
+
handleError('TOKEN_EXPIRED: Token is expired');
|
|
1375
|
+
return false;
|
|
1367
1376
|
}
|
|
1368
1377
|
return true;
|
|
1369
1378
|
}
|
|
1370
|
-
catch
|
|
1371
|
-
|
|
1379
|
+
catch {
|
|
1380
|
+
handleError('TOKEN_INVALID: Could not decode token');
|
|
1381
|
+
await cleanCredentials('any');
|
|
1382
|
+
return false;
|
|
1372
1383
|
}
|
|
1373
1384
|
};
|
|
1374
1385
|
|
|
@@ -1758,32 +1769,39 @@ function getDefaultAuthFetcher() {
|
|
|
1758
1769
|
}
|
|
1759
1770
|
|
|
1760
1771
|
/**
|
|
1761
|
-
* Refreshes the access and refresh tokens
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1772
|
+
* Refreshes the access and refresh tokens.
|
|
1773
|
+
*
|
|
1774
|
+
* Fixes over the original:
|
|
1775
|
+
* 1. Uses `persistence` (the location where tokens were originally stored)
|
|
1776
|
+
* instead of hardcoding `"any"` — so cookies / localStorage / sessionStorage
|
|
1777
|
+
* are searched in the right place.
|
|
1778
|
+
* 2. Sends the refresh token in the request body using the configured
|
|
1779
|
+
* `refreshTokenPaths.refreshTokenPath` as the body key, so the backend
|
|
1780
|
+
* always receives it regardless of withCredentials or cookie settings.
|
|
1765
1781
|
*
|
|
1766
|
-
*
|
|
1767
|
-
* @returns {Promise<AuthResponse>} A promise that resolves with the new authentication response containing the refreshed tokens.
|
|
1768
|
-
* @throws {Error} Throws an error if the refresh token is missing or if the refresh request fails, which is then caught to trigger a logout.
|
|
1782
|
+
* On failure: clears credentials and calls `onRefreshFailed` callback.
|
|
1769
1783
|
*/
|
|
1770
1784
|
const refreshTokens = async (fetcher) => {
|
|
1771
1785
|
const tokenPaths = getRefreshTokenPathsConfig();
|
|
1772
1786
|
const endpoints = getEndpointsConfig();
|
|
1773
1787
|
const secretKey = getAppKey();
|
|
1774
1788
|
const persistence = await getSessionPersistence();
|
|
1775
|
-
const getFetcher = () => fetcher
|
|
1789
|
+
const getFetcher = () => fetcher ?? getDefaultAuthFetcher();
|
|
1776
1790
|
try {
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1791
|
+
// Use persistence (not hardcoded "any") — tokens live where they were stored
|
|
1792
|
+
const refreshToken = await getAuthRefreshToken(secretKey, persistence);
|
|
1793
|
+
if (!refreshToken) {
|
|
1794
|
+
throw new Error('TOKEN_MISSING: No refresh token found in storage.');
|
|
1780
1795
|
}
|
|
1796
|
+
// Send refresh token in the request body using the configured path key
|
|
1797
|
+
const refreshBodyKey = tokenPaths.refreshTokenPath ?? 'refresh_token';
|
|
1781
1798
|
const data = await getFetcher()({
|
|
1782
1799
|
method: 'POST',
|
|
1783
1800
|
url: endpoints.REFRESH,
|
|
1801
|
+
data: { [refreshBodyKey]: refreshToken },
|
|
1784
1802
|
});
|
|
1785
|
-
const { accessToken, refreshToken } = extractAndValidateTokens(data, tokenPaths,
|
|
1786
|
-
await storeTokens(accessToken,
|
|
1803
|
+
const { accessToken, refreshToken: newRefreshToken } = extractAndValidateTokens(data, tokenPaths, 'REFRESH');
|
|
1804
|
+
await storeTokens(accessToken, newRefreshToken, persistence);
|
|
1787
1805
|
return data;
|
|
1788
1806
|
}
|
|
1789
1807
|
catch (error) {
|
|
@@ -1840,12 +1858,6 @@ function createAxiosFetcher(axiosInstance) {
|
|
|
1840
1858
|
};
|
|
1841
1859
|
}
|
|
1842
1860
|
|
|
1843
|
-
/**
|
|
1844
|
-
* A service class that encapsulates a customizable Axios instance with built-in interceptors
|
|
1845
|
-
* for handling authentication, token refreshing, and request cancellation. It is designed to
|
|
1846
|
-
* streamline API communication by automatically attaching authorization headers and managing
|
|
1847
|
-
* token refresh logic for 401 Unauthorized responses.
|
|
1848
|
-
*/
|
|
1849
1861
|
class AxiosService {
|
|
1850
1862
|
instance;
|
|
1851
1863
|
cancelTokenSource;
|
|
@@ -1853,20 +1865,16 @@ class AxiosService {
|
|
|
1853
1865
|
refreshTokenUrl;
|
|
1854
1866
|
isRefreshing = false;
|
|
1855
1867
|
failedQueue = [];
|
|
1856
|
-
/**
|
|
1857
|
-
* Creates an instance of AxiosService.
|
|
1858
|
-
* @param {AxiosServiceOptions} options - Configuration options for the Axios instance, such as `baseURL`, `timeout`, and custom `headers`.
|
|
1859
|
-
*/
|
|
1860
1868
|
constructor(options) {
|
|
1861
1869
|
this.cancelTokenSource = axios.CancelToken.source();
|
|
1862
1870
|
const endpointsConfig = getEndpointsConfig();
|
|
1863
1871
|
this.refreshTokenUrl = endpointsConfig.REFRESH;
|
|
1864
1872
|
this.instance = axios.create({
|
|
1865
|
-
baseURL: options.baseURL ??
|
|
1873
|
+
baseURL: options.baseURL ?? '',
|
|
1866
1874
|
timeout: options.timeout ?? 30000,
|
|
1867
1875
|
headers: {
|
|
1868
|
-
Accept:
|
|
1869
|
-
|
|
1876
|
+
Accept: 'application/json',
|
|
1877
|
+
'Content-Type': 'application/json',
|
|
1870
1878
|
...options.headers,
|
|
1871
1879
|
},
|
|
1872
1880
|
withCredentials: options.withCredentials ?? false,
|
|
@@ -1875,6 +1883,12 @@ class AxiosService {
|
|
|
1875
1883
|
this.initializeInterceptors();
|
|
1876
1884
|
}
|
|
1877
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Resolves or rejects all queued promises waiting for a token refresh.
|
|
1888
|
+
*
|
|
1889
|
+
* Fix: if both error and token are null (edge case), the queue is still
|
|
1890
|
+
* cleared to avoid permanently hanging promises.
|
|
1891
|
+
*/
|
|
1878
1892
|
processQueue(error, token = null) {
|
|
1879
1893
|
this.failedQueue.forEach((prom) => {
|
|
1880
1894
|
if (error) {
|
|
@@ -1883,6 +1897,10 @@ class AxiosService {
|
|
|
1883
1897
|
else if (token) {
|
|
1884
1898
|
prom.resolve(token);
|
|
1885
1899
|
}
|
|
1900
|
+
else {
|
|
1901
|
+
// Neither error nor token — reject with a clear message rather than hanging
|
|
1902
|
+
prom.reject(new Error('[arex-core] Token refresh completed but no token was produced.'));
|
|
1903
|
+
}
|
|
1886
1904
|
});
|
|
1887
1905
|
this.failedQueue = [];
|
|
1888
1906
|
}
|
|
@@ -1892,8 +1910,12 @@ class AxiosService {
|
|
|
1892
1910
|
}
|
|
1893
1911
|
}
|
|
1894
1912
|
initializeInterceptors() {
|
|
1913
|
+
// ── Request: attach Authorization using the session's storage location ──
|
|
1895
1914
|
this.instance.interceptors.request.use(async (config) => {
|
|
1896
|
-
|
|
1915
|
+
// Use persistence preference (not hardcoded "any") so the token is
|
|
1916
|
+
// found regardless of whether it was stored in cookies, localStorage, etc.
|
|
1917
|
+
const persistence = await getSessionPersistence();
|
|
1918
|
+
const token = await getAuthToken(getAppKey(), persistence);
|
|
1897
1919
|
if (token) {
|
|
1898
1920
|
this.setAuthHeader(config, token);
|
|
1899
1921
|
}
|
|
@@ -1904,11 +1926,13 @@ class AxiosService {
|
|
|
1904
1926
|
handleError(error);
|
|
1905
1927
|
return Promise.reject(error);
|
|
1906
1928
|
});
|
|
1929
|
+
// ── Response: handle 401 with token refresh ──────────────────────────
|
|
1907
1930
|
this.instance.interceptors.response.use((response) => {
|
|
1908
1931
|
this.activeRequests--;
|
|
1909
1932
|
return response;
|
|
1910
1933
|
}, async (error) => {
|
|
1911
1934
|
this.activeRequests--;
|
|
1935
|
+
// Do not attempt refresh in SSR — storage is unavailable
|
|
1912
1936
|
if (typeof window === 'undefined') {
|
|
1913
1937
|
return Promise.reject(error);
|
|
1914
1938
|
}
|
|
@@ -1916,14 +1940,11 @@ class AxiosService {
|
|
|
1916
1940
|
const isAuthError = axios.isAxiosError(error) && error.response?.status === 401;
|
|
1917
1941
|
const isRefreshCall = originalRequest?.url === this.refreshTokenUrl;
|
|
1918
1942
|
const isRetry = originalRequest?._retry === true;
|
|
1919
|
-
if (!isAuthError || isRefreshCall || isRetry) {
|
|
1920
|
-
handleError(error);
|
|
1921
|
-
return Promise.reject(error);
|
|
1922
|
-
}
|
|
1923
|
-
if (!originalRequest) {
|
|
1943
|
+
if (!isAuthError || isRefreshCall || isRetry || !originalRequest) {
|
|
1924
1944
|
handleError(error);
|
|
1925
1945
|
return Promise.reject(error);
|
|
1926
1946
|
}
|
|
1947
|
+
// Queue concurrent requests while a refresh is already in progress
|
|
1927
1948
|
if (this.isRefreshing) {
|
|
1928
1949
|
return new Promise((resolve, reject) => {
|
|
1929
1950
|
this.failedQueue.push({ resolve, reject });
|
|
@@ -1932,25 +1953,22 @@ class AxiosService {
|
|
|
1932
1953
|
this.setAuthHeader(originalRequest, newToken);
|
|
1933
1954
|
return this.instance(originalRequest);
|
|
1934
1955
|
})
|
|
1935
|
-
.catch((err) =>
|
|
1936
|
-
return Promise.reject(err);
|
|
1937
|
-
});
|
|
1956
|
+
.catch((err) => Promise.reject(err));
|
|
1938
1957
|
}
|
|
1939
1958
|
this.isRefreshing = true;
|
|
1940
1959
|
originalRequest._retry = true;
|
|
1941
1960
|
try {
|
|
1942
1961
|
const fetcher = createAxiosFetcher(this.instance);
|
|
1943
1962
|
await refreshTokens(fetcher);
|
|
1944
|
-
const
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
}
|
|
1949
|
-
else {
|
|
1950
|
-
const refreshError = new Error("New token not found after refresh.");
|
|
1963
|
+
const persistence = await getSessionPersistence();
|
|
1964
|
+
const newToken = await getAuthToken(getAppKey(), persistence);
|
|
1965
|
+
if (!newToken) {
|
|
1966
|
+
const refreshError = new Error('[arex-core] New token not found after refresh.');
|
|
1951
1967
|
this.processQueue(refreshError, null);
|
|
1952
1968
|
throw refreshError;
|
|
1953
1969
|
}
|
|
1970
|
+
this.processQueue(null, newToken);
|
|
1971
|
+
this.setAuthHeader(originalRequest, newToken);
|
|
1954
1972
|
this.isRefreshing = false;
|
|
1955
1973
|
return this.instance(originalRequest);
|
|
1956
1974
|
}
|
|
@@ -1962,39 +1980,19 @@ class AxiosService {
|
|
|
1962
1980
|
}
|
|
1963
1981
|
});
|
|
1964
1982
|
}
|
|
1965
|
-
/**
|
|
1966
|
-
* Returns the number of active (in-flight) requests.
|
|
1967
|
-
* @returns {number} The number of active requests.
|
|
1968
|
-
*/
|
|
1969
1983
|
getActiveRequests() {
|
|
1970
1984
|
return this.activeRequests;
|
|
1971
1985
|
}
|
|
1972
|
-
/**
|
|
1973
|
-
* Returns the underlying Axios instance.
|
|
1974
|
-
* @returns {AxiosInstance} The Axios instance.
|
|
1975
|
-
*/
|
|
1976
1986
|
getAxiosInstance() {
|
|
1977
1987
|
return this.instance;
|
|
1978
1988
|
}
|
|
1979
|
-
/**
|
|
1980
|
-
* Cancels all ongoing requests made by this Axios instance.
|
|
1981
|
-
*/
|
|
1982
1989
|
cancelAllRequests() {
|
|
1983
|
-
this.cancelTokenSource.cancel(
|
|
1990
|
+
this.cancelTokenSource.cancel('Operation canceled by the user.');
|
|
1984
1991
|
this.cancelTokenSource = axios.CancelToken.source();
|
|
1985
1992
|
}
|
|
1986
|
-
/**
|
|
1987
|
-
* Sets a default header for all subsequent requests.
|
|
1988
|
-
* @param {string} key - The header key.
|
|
1989
|
-
* @param {string} value - The header value.
|
|
1990
|
-
*/
|
|
1991
1993
|
setHeader(key, value) {
|
|
1992
1994
|
this.instance.defaults.headers.common[key] = value;
|
|
1993
1995
|
}
|
|
1994
|
-
/**
|
|
1995
|
-
* Removes a default header.
|
|
1996
|
-
* @param {string} key - The header key to remove.
|
|
1997
|
-
*/
|
|
1998
1996
|
removeHeader(key) {
|
|
1999
1997
|
delete this.instance.defaults.headers.common[key];
|
|
2000
1998
|
}
|
|
@@ -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>;
|
|
@@ -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>;
|