@arex95/vue-core 1.1.35 → 1.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +498 -504
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,11 +1,182 @@
1
1
  import axios from 'axios';
2
+ import { jwtDecode } from 'jwt-decode';
2
3
  import { useRouter } from 'vue-router';
3
4
  import { v4 } from 'uuid';
4
5
  import { useTimeoutFn, useBreakpoints, breakpointsTailwind, useWindowSize } from '@vueuse/core';
5
- import { jwtDecode } from 'jwt-decode';
6
6
  import { useQuery } from '@tanstack/vue-query';
7
7
  import { ref, watch, onServerPrefetch, onMounted, computed } from 'vue';
8
8
 
9
+ let tokensConfig = Object.freeze({
10
+ ACCESS_TOKEN: "access_token",
11
+ REFRESH_TOKEN: "refresh_token",
12
+ });
13
+ /**
14
+ * Configures the global keys for access and refresh tokens.
15
+ * Once set, they cannot be modified.
16
+ *
17
+ * @param {TokenKeyConfig} config - An object containing the token keys.
18
+ * @param {string} config.accessTokenKey - The name of the key for the access token.
19
+ * @param {string} config.refreshTokenKey - The name of the key for the refresh token.
20
+ *
21
+ * @returns {void} Does not return anything, but freezes the token configuration object.
22
+ */
23
+ function configTokenKeys(config) {
24
+ tokensConfig = Object.freeze({
25
+ ACCESS_TOKEN: config.accessTokenKey,
26
+ REFRESH_TOKEN: config.refreshTokenKey,
27
+ });
28
+ }
29
+ /**
30
+ * Retrieves the current token configuration.
31
+ *
32
+ * @returns {TokensConfig} The configuration of the access and refresh token keys.
33
+ */
34
+ function getTokenConfig() {
35
+ return tokensConfig;
36
+ }
37
+
38
+ /**
39
+ * Converts an ArrayBuffer or Uint8Array to a hexadecimal string.
40
+ * @param buffer The ArrayBuffer or Uint8Array to convert.
41
+ * @returns The hexadecimal string.
42
+ */
43
+ function ab2hex(buffer) {
44
+ return Array.from(new Uint8Array(buffer))
45
+ .map((byte) => byte.toString(16).padStart(2, "0"))
46
+ .join("");
47
+ }
48
+ /**
49
+ * Converts a hexadecimal string to a Uint8Array.
50
+ * @param hex The hexadecimal string to convert.
51
+ * @returns The Uint8Array.
52
+ * @throws {TypeError} If the input is not a string.
53
+ * @throws {Error} If the hexadecimal string format is invalid or has an odd length.
54
+ */
55
+ function hex2ab(hex) {
56
+ if (typeof hex !== "string") {
57
+ throw new TypeError("Input must be a string.");
58
+ }
59
+ if (hex.length === 0) {
60
+ return new Uint8Array();
61
+ }
62
+ if (!/^[0-9a-fA-F]*$/.test(hex) || hex.length % 2 !== 0) {
63
+ throw new Error("Invalid hexadecimal string format or odd length.");
64
+ }
65
+ const array = new Uint8Array(hex.length / 2);
66
+ for (let i = 0; i < hex.length; i += 2) {
67
+ array[i / 2] = parseInt(hex.substring(i, i + 2), 16);
68
+ }
69
+ return array;
70
+ }
71
+ /**
72
+ * Derives an encryption key from a secret key.
73
+ * @param secretKey The secret key in plain text.
74
+ * @returns A promise that resolves with the derived CryptoKey.
75
+ * @throws {Error} If the secretKey is null or empty.
76
+ */
77
+ async function importKey(secretKey) {
78
+ if (!secretKey) {
79
+ throw new Error("Secret key cannot be null or empty.");
80
+ }
81
+ const keyMaterial = new TextEncoder().encode(secretKey);
82
+ const digest = await crypto.subtle.digest("SHA-256", keyMaterial);
83
+ return crypto.subtle.importKey("raw", digest, { name: "AES-CBC", length: 256 }, false, ["encrypt", "decrypt"]);
84
+ }
85
+ /**
86
+ * Encrypts a value with the provided secret key.
87
+ * @param value The value to encrypt.
88
+ * @param secretKey The secret key for encryption.
89
+ * @returns A promise that resolves with the IV (hex) + ciphertext (hex) string.
90
+ * @throws {Error} If the secretKey is null or empty (via importKey).
91
+ */
92
+ async function encrypt(value, secretKey) {
93
+ const key = await importKey(secretKey);
94
+ const iv = crypto.getRandomValues(new Uint8Array(16));
95
+ const encodedValue = new TextEncoder().encode(value);
96
+ const ciphertext = await crypto.subtle.encrypt({ name: "AES-CBC", iv: iv }, key, encodedValue);
97
+ return ab2hex(iv) + ab2hex(new Uint8Array(ciphertext));
98
+ }
99
+ /**
100
+ * Decrypts an encrypted value.
101
+ * @param encryptedValue The encrypted string (IV_hex + ciphertext_hex).
102
+ * @param secretKey The secret key for decryption.
103
+ * @returns A promise that resolves with the decrypted value.
104
+ * @throws {Error} If encryptedValue is null or empty, too short,
105
+ * or if the IV/ciphertext have incorrect lengths after conversion.
106
+ * @throws {Error} If the secretKey is null or empty (via importKey).
107
+ * @throws {TypeError} If hex2ab receives an invalid input type.
108
+ */
109
+ async function decrypt(encryptedValue, secretKey) {
110
+ if (!encryptedValue) {
111
+ throw new Error("Encrypted value cannot be null or empty.");
112
+ }
113
+ // For AES-CBC, the IV is ALWAYS 16 bytes.
114
+ // 16 bytes * 2 hex characters/byte = 32 hex characters for the IV.
115
+ if (encryptedValue.length < 32) {
116
+ throw new Error("Encrypted value is too short. Expected at least 32 hexadecimal characters for the IV.");
117
+ }
118
+ const key = await importKey(secretKey);
119
+ const ivHex = encryptedValue.substring(0, 32);
120
+ const ciphertextHex = encryptedValue.substring(32);
121
+ const iv = hex2ab(ivHex);
122
+ const ciphertext = hex2ab(ciphertextHex);
123
+ if (iv.byteLength !== 16) {
124
+ throw new Error(`Converted IV has incorrect length: ${iv.byteLength} bytes. Expected 16 bytes.`);
125
+ }
126
+ if (ciphertext.byteLength === 0) {
127
+ throw new Error("Ciphertext is empty. No data to decrypt.");
128
+ }
129
+ const decryptedBuffer = await crypto.subtle.decrypt({ name: "AES-CBC", iv: iv }, key, ciphertext);
130
+ return new TextDecoder().decode(decryptedBuffer);
131
+ }
132
+
133
+ /**
134
+ * Encrypts and stores an item in local or session storage.
135
+ * Assumes the `window` environment is available.
136
+ * @param key The key under which to store the value.
137
+ * @param value The value to encrypt and store.
138
+ * @param secretKey The secret key for encryption.
139
+ * @param location Determines where the item is stored: 'local' for localStorage, 'session' for sessionStorage.
140
+ * @returns A promise that resolves when the item is stored. Throws an error if it fails.
141
+ */
142
+ async function storeEncryptedItem(key, value, secretKey, location) {
143
+ if (typeof window === "undefined") {
144
+ throw new Error("Cannot access storage: window is not defined.");
145
+ }
146
+ const storage = location === "local" ? window.localStorage : window.sessionStorage;
147
+ const encryptedValue = await encrypt(value, secretKey);
148
+ storage.setItem(key, encryptedValue);
149
+ }
150
+ /**
151
+ * Retrieves and decrypts a value from local or session storage.
152
+ * Assumes the `window` environment is available.
153
+ * @param key The key of the item to retrieve.
154
+ * @param secretKey The secret key for decryption.
155
+ * @param location Specifies where to search for the item: 'local' for localStorage, 'session' for sessionStorage, or 'any' to check both (session first).
156
+ * @returns A promise that resolves with the decrypted value or null if not found or decryption fails.
157
+ */
158
+ async function getDecryptedItem(key, secretKey, location) {
159
+ if (typeof window === "undefined") {
160
+ return null;
161
+ }
162
+ let encryptedData = null;
163
+ if (location === "session" || location === "any") {
164
+ encryptedData = window.sessionStorage.getItem(key);
165
+ }
166
+ if (!encryptedData && (location === "local" || location === "any")) {
167
+ encryptedData = window.localStorage.getItem(key);
168
+ }
169
+ if (!encryptedData) {
170
+ return null;
171
+ }
172
+ try {
173
+ return await decrypt(encryptedData, secretKey);
174
+ }
175
+ catch (error) {
176
+ return null;
177
+ }
178
+ }
179
+
9
180
  /**
10
181
  * Enum defining available screen sizes.
11
182
  */
@@ -538,68 +709,288 @@ function inferErrorType(error) {
538
709
  return 'error';
539
710
  }
540
711
 
541
- let tokensConfig = Object.freeze({
542
- ACCESS_TOKEN: "access_token",
543
- REFRESH_TOKEN: "refresh_token",
544
- });
545
712
  /**
546
- * Configures the global keys for access and refresh tokens.
547
- * Once set, they cannot be modified.
548
- *
549
- * @param {TokenKeyConfig} config - An object containing the token keys.
550
- * @param {string} config.accessTokenKey - The name of the key for the access token.
551
- * @param {string} config.refreshTokenKey - The name of the key for the refresh token.
713
+ * Clears all stored authentication data (access and refresh tokens)
714
+ * from either sessionStorage, localStorage, or both based on the provided location preference.
552
715
  *
553
- * @returns {void} Does not return anything, but freezes the token configuration object.
716
+ * @param {LocationPreference} location - The storage preference ('local' for localStorage, 'session' for sessionStorage, 'any' for both).
717
+ * @returns {Promise<void>} A promise that resolves when all relevant storage items are removed.
554
718
  */
555
- function configTokenKeys(config) {
556
- tokensConfig = Object.freeze({
557
- ACCESS_TOKEN: config.accessTokenKey,
558
- REFRESH_TOKEN: config.refreshTokenKey,
719
+ const cleanCredentials = async (location) => {
720
+ const tokensConfig = getTokenConfig();
721
+ Object.keys(tokensConfig).forEach((key) => {
722
+ const itemKey = tokensConfig[key];
723
+ if (location === "local" || location === "any") {
724
+ localStorage.removeItem(itemKey);
725
+ }
726
+ if (location === "session" || location === "any") {
727
+ sessionStorage.removeItem(itemKey);
728
+ }
559
729
  });
560
- }
730
+ };
561
731
  /**
562
- * Retrieves the current token configuration.
732
+ * Retrieves the authentication token (access token) from storage, decrypting it
733
+ * using the provided secret key and based on the specified session preference.
563
734
  *
564
- * @returns {TokensConfig} The configuration of the access and refresh token keys.
735
+ * @param {string} secretKey - The secret key used for decryption.
736
+ * @param {SessionPreference} preference - The storage preference ('local' for localStorage, 'session' for sessionStorage).
737
+ * @returns {Promise<string | null>} A promise that resolves with the decrypted access token, or null if not found.
565
738
  */
566
- function getTokenConfig() {
567
- return tokensConfig;
568
- }
569
-
570
- let endpointsConfig = {
571
- LOGIN: "/login",
572
- REFRESH: "/refresh",
573
- LOGOUT: "/logout",
739
+ const getAuthToken = async (secretKey, location) => {
740
+ const tokensConfig = getTokenConfig();
741
+ return await getDecryptedItem(tokensConfig.ACCESS_TOKEN, secretKey, location);
574
742
  };
575
743
  /**
576
- * Configures authentication endpoint URLs globally.
577
- * This function freezes the object to prevent further modifications.
578
- *
579
- * @param {EndpointConfig} config - An object containing the authentication endpoint URLs.
580
- * @param {string} config.loginEndpoint - URL of the login endpoint.
581
- * @param {string} config.refreshEndpoint - URL of the refresh token endpoint.
582
- * @param {string} config.logoutEndpoint - URL of the logout endpoint.
744
+ * Retrieves the authentication refresh token from storage, decrypting it
745
+ * using the provided secret key and based on the specified session preference.
583
746
  *
584
- * @returns {void} Does not return anything but freezes the endpoint configuration object.
747
+ * @param {string} secretKey - The secret key used for decryption.
748
+ * @param {SessionPreference} preference - The storage preference ('local' for localStorage, 'session' for sessionStorage).
749
+ * @returns {Promise<string | null>} A promise that resolves with the decrypted refresh token, or null if not found.
585
750
  */
586
- function configEndpoints(config) {
587
- endpointsConfig = Object.freeze({
588
- LOGIN: config.loginEndpoint,
589
- REFRESH: config.refreshEndpoint,
590
- LOGOUT: config.logoutEndpoint,
591
- });
592
- }
751
+ const getAuthRefreshToken = async (secretKey, location) => {
752
+ const tokensConfig = getTokenConfig();
753
+ return await getDecryptedItem(tokensConfig.REFRESH_TOKEN, secretKey, location);
754
+ };
593
755
  /**
594
- * Retrieves the configured authentication endpoint URLs.
756
+ * Stores the authentication token (access token) in storage after encrypting it,
757
+ * based on the specified session preference.
595
758
  *
596
- * @returns {EndpointsConfig} An object containing the configured authentication endpoints.
597
- * @property {string} LOGIN - URL of the login endpoint.
598
- * @property {string} REFRESH - URL of the refresh token endpoint.
599
- * @property {string} LOGOUT - URL of the logout endpoint.
600
- */
601
- function getEndpointsConfig() {
602
- return endpointsConfig;
759
+ * @param {string} token - The access token to store.
760
+ * @param {string} secretKey - The secret key used for encryption.
761
+ * @param {SessionPreference} preference - The storage preference ('local' for localStorage, 'session' for sessionStorage).
762
+ * @returns {Promise<void>} A promise that resolves when the token is successfully stored.
763
+ */
764
+ const storeAuthToken = async (token, secretKey, location) => {
765
+ const tokensConfig = getTokenConfig();
766
+ await storeEncryptedItem(tokensConfig.ACCESS_TOKEN, token, secretKey, location);
767
+ };
768
+ /**
769
+ * Stores the authentication refresh token in storage after encrypting it,
770
+ * based on the specified session preference.
771
+ *
772
+ * @param {string} token - The refresh token to store.
773
+ * @param {string} secretKey - The secret key used for encryption.
774
+ * @param {SessionPreference} preference - The storage preference ('local' for localStorage, 'session' for sessionStorage).
775
+ * @returns {Promise<void>} A promise that resolves when the token is successfully stored.
776
+ */
777
+ const storeAuthRefreshToken = async (token, secretKey, location) => {
778
+ const tokensConfig = getTokenConfig();
779
+ await storeEncryptedItem(tokensConfig.REFRESH_TOKEN, token, secretKey, location);
780
+ };
781
+ /**
782
+ * Verifies the validity and expiration of the current authentication token.
783
+ * If the token is missing, invalid, or expired, appropriate errors are thrown and credentials are cleaned.
784
+ *
785
+ * @returns {Promise<boolean>} True if the token is valid and unexpired.
786
+ * @throws {Error} "TOKEN_MISSING" if no token is found, "TOKEN_EXPIRED" if the token has expired,
787
+ * "TOKEN_INVALID" if the token format is invalid.
788
+ */
789
+ const verifyAuth = async () => {
790
+ const sessionPersistence = 'any';
791
+ const handleAuthError = async (message, shouldClean = true) => {
792
+ handleError(message, false);
793
+ if (shouldClean) {
794
+ await cleanCredentials(sessionPersistence);
795
+ }
796
+ return false;
797
+ };
798
+ try {
799
+ const token = await getAuthToken(getAppKey(), sessionPersistence);
800
+ if (!token) {
801
+ return await handleAuthError("TOKEN_MISSING: No valid token found");
802
+ }
803
+ let decoded;
804
+ try {
805
+ decoded = jwtDecode(token);
806
+ }
807
+ catch (decodeError) {
808
+ return await handleAuthError("TOKEN_INVALID: Invalid token format");
809
+ }
810
+ const currentTime = Date.now() / 1000;
811
+ if (typeof decoded.exp !== "number") {
812
+ return await handleAuthError("TOKEN_INVALID: Invalid expiration format");
813
+ }
814
+ if (decoded.exp <= currentTime) {
815
+ return await handleAuthError("TOKEN_EXPIRED: Token is expired");
816
+ }
817
+ return true;
818
+ }
819
+ catch (error) {
820
+ return await handleAuthError("AUTH_ERROR: An unexpected error occurred", true);
821
+ }
822
+ };
823
+
824
+ let appKey = null;
825
+ /**
826
+ * Sets the main application encryption key.
827
+ * This key is expected to be used for encryption purposes within the application.
828
+ *
829
+ * @param {AppKeyConfig} config - An object containing the application encryption key.
830
+ * @param {string} config.key - The new application encryption key.
831
+ *
832
+ * @returns {void} Does not return anything, but updates the application key.
833
+ * @throws {Error} If the provided key is null, undefined, or an empty string.
834
+ */
835
+ function configAppKey(config) {
836
+ if (!config || !config.appKey || config.appKey.trim() === "") {
837
+ throw new Error("The application encryption key cannot be null or empty.");
838
+ }
839
+ appKey = config.appKey;
840
+ }
841
+ /**
842
+ * Retrieves the current application encryption key.
843
+ * Throws an error if the application key has not been configured.
844
+ *
845
+ * @returns {string} The configured application encryption key.
846
+ * @throws {Error} If the application encryption key has not been set.
847
+ */
848
+ function getAppKey() {
849
+ if (appKey === null) {
850
+ throw new Error("The application encryption key has not been configured. Please call 'configAppKey()' before attempting to access it.");
851
+ }
852
+ return appKey;
853
+ }
854
+
855
+ const SESSION_KEY = "session_config_";
856
+ const internalSessionState = {
857
+ sessionId: v4(),
858
+ persistencePreference: "session",
859
+ };
860
+ let _sessionConfig = Object.freeze({
861
+ SESSION_ID: internalSessionState.sessionId,
862
+ PERSISTENCE: internalSessionState.persistencePreference,
863
+ });
864
+ /**
865
+ * Updates the immutable `_sessionConfig` object with the current state of `internalSessionState`
866
+ * and freezes it.
867
+ */
868
+ function updateSessionConfig() {
869
+ _sessionConfig = Object.freeze({
870
+ SESSION_ID: internalSessionState.sessionId,
871
+ PERSISTENCE: internalSessionState.persistencePreference,
872
+ });
873
+ }
874
+ /**
875
+ * Saves the current session configuration to local or session storage.
876
+ * @returns {Promise<void>} A promise that resolves when the session configuration has been saved.
877
+ */
878
+ async function saveSessionConfig() {
879
+ const location = internalSessionState.persistencePreference;
880
+ try {
881
+ await storeEncryptedItem(SESSION_KEY, JSON.stringify(_sessionConfig), getAppKey(), location);
882
+ }
883
+ catch (error) {
884
+ console.error("Error saving session configuration to storage:", error);
885
+ }
886
+ }
887
+ /**
888
+ * Attempts to load the configuration from storage.
889
+ * If it fails or is not found, `internalSessionState` will retain its current values (initial or last configured).
890
+ * Then, it updates `_sessionConfig`.
891
+ *
892
+ * @returns {Promise<void>} A promise that resolves when the state has been loaded and updated.
893
+ */
894
+ async function loadSessionConfig() {
895
+ const location = internalSessionState.persistencePreference;
896
+ try {
897
+ const storedConfig = await getDecryptedItem(SESSION_KEY, getAppKey(), location);
898
+ if (storedConfig) {
899
+ const parsedConfig = JSON.parse(storedConfig);
900
+ internalSessionState.sessionId = parsedConfig.SESSION_ID;
901
+ internalSessionState.persistencePreference = parsedConfig.PERSISTENCE;
902
+ updateSessionConfig();
903
+ }
904
+ }
905
+ catch (error) {
906
+ console.warn(`Error loading or parsing from storage`, error);
907
+ }
908
+ }
909
+ /**
910
+ * Configures the session identifier and/or data persistence preference
911
+ * for the active browser session.
912
+ *
913
+ * This function is asynchronous because it will always attempt to load the current configuration
914
+ * before applying changes and then saving them.
915
+ *
916
+ * @param {SessionConfigObject} config - An object containing the unique session identifier and/or
917
+ * the persistence preference.
918
+ * @returns {Promise<void>} A promise that resolves when the session has been configured and saved.
919
+ */
920
+ async function configSession(config) {
921
+ if (config.sessionId) {
922
+ internalSessionState.sessionId = config.sessionId;
923
+ }
924
+ if (config.persistencePreference) {
925
+ internalSessionState.persistencePreference = config.persistencePreference;
926
+ }
927
+ updateSessionConfig();
928
+ await saveSessionConfig();
929
+ }
930
+ /**
931
+ * Retrieves the current session identifier.
932
+ * Always attempts to load the configuration from storage. If it fails, it uses the internal state.
933
+ *
934
+ * @returns {Promise<string>} A promise that resolves with the unique session identifier.
935
+ */
936
+ async function getSessionId() {
937
+ await loadSessionConfig();
938
+ return _sessionConfig.SESSION_ID;
939
+ }
940
+ /**
941
+ * Retrieves the current data persistence preference.
942
+ * Always attempts to load the configuration from storage. If it fails, it uses the internal state.
943
+ *
944
+ * @returns {Promise<SessionPreference>} A promise that resolves with the configured persistence preference ('local' or 'session').
945
+ */
946
+ async function getSessionPersistence() {
947
+ await loadSessionConfig();
948
+ return _sessionConfig.PERSISTENCE;
949
+ }
950
+ /**
951
+ * Retrieves the complete session configuration.
952
+ * Always attempts to load the configuration from storage. If it fails, it uses the internal state.
953
+ *
954
+ * @returns {Promise<SessionConfig>} A promise that resolves with the session configuration object.
955
+ */
956
+ async function getSessionConfig() {
957
+ await loadSessionConfig();
958
+ return _sessionConfig;
959
+ }
960
+
961
+ let endpointsConfig = {
962
+ LOGIN: "/login",
963
+ REFRESH: "/refresh",
964
+ LOGOUT: "/logout",
965
+ };
966
+ /**
967
+ * Configures authentication endpoint URLs globally.
968
+ * This function freezes the object to prevent further modifications.
969
+ *
970
+ * @param {EndpointConfig} config - An object containing the authentication endpoint URLs.
971
+ * @param {string} config.loginEndpoint - URL of the login endpoint.
972
+ * @param {string} config.refreshEndpoint - URL of the refresh token endpoint.
973
+ * @param {string} config.logoutEndpoint - URL of the logout endpoint.
974
+ *
975
+ * @returns {void} Does not return anything but freezes the endpoint configuration object.
976
+ */
977
+ function configEndpoints(config) {
978
+ endpointsConfig = Object.freeze({
979
+ LOGIN: config.loginEndpoint,
980
+ REFRESH: config.refreshEndpoint,
981
+ LOGOUT: config.logoutEndpoint,
982
+ });
983
+ }
984
+ /**
985
+ * Retrieves the configured authentication endpoint URLs.
986
+ *
987
+ * @returns {EndpointsConfig} An object containing the configured authentication endpoints.
988
+ * @property {string} LOGIN - URL of the login endpoint.
989
+ * @property {string} REFRESH - URL of the refresh token endpoint.
990
+ * @property {string} LOGOUT - URL of the logout endpoint.
991
+ */
992
+ function getEndpointsConfig() {
993
+ return endpointsConfig;
603
994
  }
604
995
 
605
996
  /**
@@ -635,9 +1026,8 @@ class AxiosService {
635
1026
  * Initializes request and response interceptors for the Axios instance.
636
1027
  */
637
1028
  initializeInterceptors() {
638
- const { ACCESS_TOKEN, REFRESH_TOKEN } = getTokenConfig();
639
- this.instance.interceptors.request.use((config) => {
640
- const token = typeof window !== 'undefined' ? localStorage.getItem(ACCESS_TOKEN) : null;
1029
+ this.instance.interceptors.request.use(async (config) => {
1030
+ const token = await getAuthToken(getAppKey(), 'any');
641
1031
  if (token && config.headers) {
642
1032
  config.headers.Authorization = `Bearer ${token}`;
643
1033
  }
@@ -654,19 +1044,15 @@ class AxiosService {
654
1044
  return response;
655
1045
  }, async (error) => {
656
1046
  this.activeRequests--;
657
- // Check for unauthorized access (401)
658
1047
  if (axios.isAxiosError(error) && error.response?.status === 401 && !this.refreshTokenInProgress) {
659
- const refreshToken = localStorage.getItem(REFRESH_TOKEN);
1048
+ const refreshToken = await getAuthRefreshToken(getAppKey(), "any");
660
1049
  if (refreshToken) {
661
1050
  this.refreshTokenInProgress = true;
662
1051
  try {
663
- // Attempt to refresh the token
664
1052
  const refreshResponse = await axios.post(this.refreshTokenUrl, { refreshToken });
665
1053
  const { token: newAccessToken } = refreshResponse.data;
666
1054
  if (newAccessToken) {
667
- // Save the new access token
668
- localStorage.setItem(ACCESS_TOKEN, newAccessToken);
669
- // Retry the original request with the new token
1055
+ storeAuthToken(newAccessToken, getAppKey(), await getSessionPersistence());
670
1056
  if (error.config && error.config.headers) {
671
1057
  error.config.headers['Authorization'] = `Bearer ${newAccessToken}`;
672
1058
  }
@@ -679,8 +1065,7 @@ class AxiosService {
679
1065
  catch (refreshError) {
680
1066
  handleError(refreshError, false);
681
1067
  this.refreshTokenInProgress = false;
682
- localStorage.removeItem(ACCESS_TOKEN);
683
- localStorage.removeItem(REFRESH_TOKEN);
1068
+ cleanCredentials('any');
684
1069
  }
685
1070
  }
686
1071
  }
@@ -692,354 +1077,75 @@ class AxiosService {
692
1077
  * Returns the number of active requests.
693
1078
  */
694
1079
  getActiveRequests() {
695
- return this.activeRequests;
696
- }
697
- /**
698
- * Returns the Axios instance with the configured settings and interceptors.
699
- * @returns {AxiosInstance} The configured Axios instance.
700
- */
701
- getAxiosInstance() {
702
- return this.instance;
703
- }
704
- /**
705
- * Cancels all ongoing requests.
706
- */
707
- cancelAllRequests() {
708
- this.cancelTokenSource.cancel('Operation canceled by the user.');
709
- this.cancelTokenSource = axios.CancelToken.source();
710
- }
711
- /**
712
- * Sets a new header for the Axios instance.
713
- * @param {string} key - The header key.
714
- * @param {string} value - The header value.
715
- */
716
- setHeader(key, value) {
717
- this.instance.defaults.headers.common[key] = value;
718
- }
719
- /**
720
- * Removes a header from the Axios instance.
721
- * @param {string} key - The header key to remove.
722
- */
723
- removeHeader(key) {
724
- delete this.instance.defaults.headers.common[key];
725
- }
726
- }
727
-
728
- let axiosInstance;
729
- /**
730
- * Configures the global Axios instance with a base URL.
731
- *
732
- * @param {AxiosConfig} config - An object containing the base URL for the Axios instance.
733
- * @param {string} config.baseURL - The base URL for the Axios instance.
734
- *
735
- * @returns {void}
736
- */
737
- const configAxios = (config) => {
738
- axiosInstance = new AxiosService(config.baseURL);
739
- };
740
- /**
741
- * Retrieves the configured Axios instance.
742
- *
743
- * @returns {AxiosService} The configured Axios instance.
744
- * @throws Will throw an error if the Axios instance is not configured.
745
- */
746
- const getAxiosInstance = () => {
747
- if (!axiosInstance) {
748
- throw new Error("Axios instance not configured. Call configAxios first.");
749
- }
750
- return axiosInstance.getAxiosInstance();
751
- };
752
- /**
753
- * Creates a new AxiosService instance with custom headers.
754
- *
755
- * @param {CustomAxiosConfig} config - An object containing the base URL and optional custom headers.
756
- * @param {string} config.baseURL - The base URL for the Axios instance.
757
- * @param {Record<string, string>} [config.headers] - Custom headers to set (optional).
758
- *
759
- * @returns {AxiosService} The new AxiosService instance.
760
- */
761
- const createCustomAxiosInstance = (config) => {
762
- return new AxiosService(config.baseURL, config.headers).getAxiosInstance();
763
- };
764
-
765
- /**
766
- * Converts an ArrayBuffer or Uint8Array to a hexadecimal string.
767
- * @param buffer The ArrayBuffer or Uint8Array to convert.
768
- * @returns The hexadecimal string.
769
- */
770
- function ab2hex(buffer) {
771
- return Array.from(new Uint8Array(buffer))
772
- .map((byte) => byte.toString(16).padStart(2, "0"))
773
- .join("");
774
- }
775
- /**
776
- * Converts a hexadecimal string to a Uint8Array.
777
- * @param hex The hexadecimal string to convert.
778
- * @returns The Uint8Array.
779
- * @throws {TypeError} If the input is not a string.
780
- * @throws {Error} If the hexadecimal string format is invalid or has an odd length.
781
- */
782
- function hex2ab(hex) {
783
- if (typeof hex !== "string") {
784
- throw new TypeError("Input must be a string.");
785
- }
786
- if (hex.length === 0) {
787
- return new Uint8Array();
788
- }
789
- if (!/^[0-9a-fA-F]*$/.test(hex) || hex.length % 2 !== 0) {
790
- throw new Error("Invalid hexadecimal string format or odd length.");
791
- }
792
- const array = new Uint8Array(hex.length / 2);
793
- for (let i = 0; i < hex.length; i += 2) {
794
- array[i / 2] = parseInt(hex.substring(i, i + 2), 16);
795
- }
796
- return array;
797
- }
798
- /**
799
- * Derives an encryption key from a secret key.
800
- * @param secretKey The secret key in plain text.
801
- * @returns A promise that resolves with the derived CryptoKey.
802
- * @throws {Error} If the secretKey is null or empty.
803
- */
804
- async function importKey(secretKey) {
805
- if (!secretKey) {
806
- throw new Error("Secret key cannot be null or empty.");
807
- }
808
- const keyMaterial = new TextEncoder().encode(secretKey);
809
- const digest = await crypto.subtle.digest("SHA-256", keyMaterial);
810
- return crypto.subtle.importKey("raw", digest, { name: "AES-CBC", length: 256 }, false, ["encrypt", "decrypt"]);
811
- }
812
- /**
813
- * Encrypts a value with the provided secret key.
814
- * @param value The value to encrypt.
815
- * @param secretKey The secret key for encryption.
816
- * @returns A promise that resolves with the IV (hex) + ciphertext (hex) string.
817
- * @throws {Error} If the secretKey is null or empty (via importKey).
818
- */
819
- async function encrypt(value, secretKey) {
820
- const key = await importKey(secretKey);
821
- const iv = crypto.getRandomValues(new Uint8Array(16));
822
- const encodedValue = new TextEncoder().encode(value);
823
- const ciphertext = await crypto.subtle.encrypt({ name: "AES-CBC", iv: iv }, key, encodedValue);
824
- return ab2hex(iv) + ab2hex(new Uint8Array(ciphertext));
825
- }
826
- /**
827
- * Decrypts an encrypted value.
828
- * @param encryptedValue The encrypted string (IV_hex + ciphertext_hex).
829
- * @param secretKey The secret key for decryption.
830
- * @returns A promise that resolves with the decrypted value.
831
- * @throws {Error} If encryptedValue is null or empty, too short,
832
- * or if the IV/ciphertext have incorrect lengths after conversion.
833
- * @throws {Error} If the secretKey is null or empty (via importKey).
834
- * @throws {TypeError} If hex2ab receives an invalid input type.
835
- */
836
- async function decrypt(encryptedValue, secretKey) {
837
- if (!encryptedValue) {
838
- throw new Error("Encrypted value cannot be null or empty.");
839
- }
840
- // For AES-CBC, the IV is ALWAYS 16 bytes.
841
- // 16 bytes * 2 hex characters/byte = 32 hex characters for the IV.
842
- if (encryptedValue.length < 32) {
843
- throw new Error("Encrypted value is too short. Expected at least 32 hexadecimal characters for the IV.");
844
- }
845
- const key = await importKey(secretKey);
846
- const ivHex = encryptedValue.substring(0, 32);
847
- const ciphertextHex = encryptedValue.substring(32);
848
- const iv = hex2ab(ivHex);
849
- const ciphertext = hex2ab(ciphertextHex);
850
- if (iv.byteLength !== 16) {
851
- throw new Error(`Converted IV has incorrect length: ${iv.byteLength} bytes. Expected 16 bytes.`);
852
- }
853
- if (ciphertext.byteLength === 0) {
854
- throw new Error("Ciphertext is empty. No data to decrypt.");
855
- }
856
- const decryptedBuffer = await crypto.subtle.decrypt({ name: "AES-CBC", iv: iv }, key, ciphertext);
857
- return new TextDecoder().decode(decryptedBuffer);
858
- }
859
-
860
- /**
861
- * Encrypts and stores an item in local or session storage.
862
- * Assumes the `window` environment is available.
863
- * @param key The key under which to store the value.
864
- * @param value The value to encrypt and store.
865
- * @param secretKey The secret key for encryption.
866
- * @param location Determines where the item is stored: 'local' for localStorage, 'session' for sessionStorage.
867
- * @returns A promise that resolves when the item is stored. Throws an error if it fails.
868
- */
869
- async function storeEncryptedItem(key, value, secretKey, location) {
870
- if (typeof window === "undefined") {
871
- throw new Error("Cannot access storage: window is not defined.");
872
- }
873
- const storage = location === "local" ? window.localStorage : window.sessionStorage;
874
- const encryptedValue = await encrypt(value, secretKey);
875
- storage.setItem(key, encryptedValue);
876
- }
877
- /**
878
- * Retrieves and decrypts a value from local or session storage.
879
- * Assumes the `window` environment is available.
880
- * @param key The key of the item to retrieve.
881
- * @param secretKey The secret key for decryption.
882
- * @param location Specifies where to search for the item: 'local' for localStorage, 'session' for sessionStorage, or 'any' to check both (session first).
883
- * @returns A promise that resolves with the decrypted value or null if not found or decryption fails.
884
- */
885
- async function getDecryptedItem(key, secretKey, location) {
886
- if (typeof window === "undefined") {
887
- return null;
888
- }
889
- let encryptedData = null;
890
- if (location === "session" || location === "any") {
891
- encryptedData = window.sessionStorage.getItem(key);
892
- }
893
- if (!encryptedData && (location === "local" || location === "any")) {
894
- encryptedData = window.localStorage.getItem(key);
895
- }
896
- if (!encryptedData) {
897
- return null;
898
- }
899
- try {
900
- return await decrypt(encryptedData, secretKey);
901
- }
902
- catch (error) {
903
- return null;
904
- }
905
- }
906
-
907
- let appKey = null;
908
- /**
909
- * Sets the main application encryption key.
910
- * This key is expected to be used for encryption purposes within the application.
911
- *
912
- * @param {AppKeyConfig} config - An object containing the application encryption key.
913
- * @param {string} config.key - The new application encryption key.
914
- *
915
- * @returns {void} Does not return anything, but updates the application key.
916
- * @throws {Error} If the provided key is null, undefined, or an empty string.
917
- */
918
- function configAppKey(config) {
919
- if (!config || !config.appKey || config.appKey.trim() === "") {
920
- throw new Error("The application encryption key cannot be null or empty.");
921
- }
922
- appKey = config.appKey;
923
- }
924
- /**
925
- * Retrieves the current application encryption key.
926
- * Throws an error if the application key has not been configured.
927
- *
928
- * @returns {string} The configured application encryption key.
929
- * @throws {Error} If the application encryption key has not been set.
930
- */
931
- function getAppKey() {
932
- if (appKey === null) {
933
- throw new Error("The application encryption key has not been configured. Please call 'configAppKey()' before attempting to access it.");
1080
+ return this.activeRequests;
934
1081
  }
935
- return appKey;
936
- }
937
-
938
- const SESSION_KEY = "session_config_";
939
- const internalSessionState = {
940
- sessionId: v4(),
941
- persistencePreference: "session",
942
- };
943
- let _sessionConfig = Object.freeze({
944
- SESSION_ID: internalSessionState.sessionId,
945
- PERSISTENCE: internalSessionState.persistencePreference,
946
- });
947
- /**
948
- * Updates the immutable `_sessionConfig` object with the current state of `internalSessionState`
949
- * and freezes it.
950
- */
951
- function updateSessionConfig() {
952
- _sessionConfig = Object.freeze({
953
- SESSION_ID: internalSessionState.sessionId,
954
- PERSISTENCE: internalSessionState.persistencePreference,
955
- });
956
- }
957
- /**
958
- * Saves the current session configuration to local or session storage.
959
- * @returns {Promise<void>} A promise that resolves when the session configuration has been saved.
960
- */
961
- async function saveSessionConfig() {
962
- const location = internalSessionState.persistencePreference;
963
- try {
964
- await storeEncryptedItem(SESSION_KEY, JSON.stringify(_sessionConfig), getAppKey(), location);
1082
+ /**
1083
+ * Returns the Axios instance with the configured settings and interceptors.
1084
+ * @returns {AxiosInstance} The configured Axios instance.
1085
+ */
1086
+ getAxiosInstance() {
1087
+ return this.instance;
965
1088
  }
966
- catch (error) {
967
- console.error("Error saving session configuration to storage:", error);
1089
+ /**
1090
+ * Cancels all ongoing requests.
1091
+ */
1092
+ cancelAllRequests() {
1093
+ this.cancelTokenSource.cancel('Operation canceled by the user.');
1094
+ this.cancelTokenSource = axios.CancelToken.source();
968
1095
  }
969
- }
970
- /**
971
- * Attempts to load the configuration from storage.
972
- * If it fails or is not found, `internalSessionState` will retain its current values (initial or last configured).
973
- * Then, it updates `_sessionConfig`.
974
- *
975
- * @returns {Promise<void>} A promise that resolves when the state has been loaded and updated.
976
- */
977
- async function loadSessionConfig() {
978
- const location = internalSessionState.persistencePreference;
979
- try {
980
- const storedConfig = await getDecryptedItem(SESSION_KEY, getAppKey(), location);
981
- if (storedConfig) {
982
- const parsedConfig = JSON.parse(storedConfig);
983
- internalSessionState.sessionId = parsedConfig.SESSION_ID;
984
- internalSessionState.persistencePreference = parsedConfig.PERSISTENCE;
985
- updateSessionConfig();
986
- }
1096
+ /**
1097
+ * Sets a new header for the Axios instance.
1098
+ * @param {string} key - The header key.
1099
+ * @param {string} value - The header value.
1100
+ */
1101
+ setHeader(key, value) {
1102
+ this.instance.defaults.headers.common[key] = value;
987
1103
  }
988
- catch (error) {
989
- console.warn(`Error loading or parsing from storage`, error);
1104
+ /**
1105
+ * Removes a header from the Axios instance.
1106
+ * @param {string} key - The header key to remove.
1107
+ */
1108
+ removeHeader(key) {
1109
+ delete this.instance.defaults.headers.common[key];
990
1110
  }
991
1111
  }
1112
+
1113
+ let axiosInstance;
992
1114
  /**
993
- * Configures the session identifier and/or data persistence preference
994
- * for the active browser session.
1115
+ * Configures the global Axios instance with a base URL.
995
1116
  *
996
- * This function is asynchronous because it will always attempt to load the current configuration
997
- * before applying changes and then saving them.
1117
+ * @param {AxiosConfig} config - An object containing the base URL for the Axios instance.
1118
+ * @param {string} config.baseURL - The base URL for the Axios instance.
998
1119
  *
999
- * @param {SessionConfigObject} config - An object containing the unique session identifier and/or
1000
- * the persistence preference.
1001
- * @returns {Promise<void>} A promise that resolves when the session has been configured and saved.
1120
+ * @returns {void}
1002
1121
  */
1003
- async function configSession(config) {
1004
- if (config.sessionId) {
1005
- internalSessionState.sessionId = config.sessionId;
1006
- }
1007
- if (config.persistencePreference) {
1008
- internalSessionState.persistencePreference = config.persistencePreference;
1009
- }
1010
- updateSessionConfig();
1011
- await saveSessionConfig();
1012
- }
1122
+ const configAxios = (config) => {
1123
+ axiosInstance = new AxiosService(config.baseURL);
1124
+ };
1013
1125
  /**
1014
- * Retrieves the current session identifier.
1015
- * Always attempts to load the configuration from storage. If it fails, it uses the internal state.
1126
+ * Retrieves the configured Axios instance.
1016
1127
  *
1017
- * @returns {Promise<string>} A promise that resolves with the unique session identifier.
1128
+ * @returns {AxiosService} The configured Axios instance.
1129
+ * @throws Will throw an error if the Axios instance is not configured.
1018
1130
  */
1019
- async function getSessionId() {
1020
- await loadSessionConfig();
1021
- return _sessionConfig.SESSION_ID;
1022
- }
1131
+ const getAxiosInstance = () => {
1132
+ if (!axiosInstance) {
1133
+ throw new Error("Axios instance not configured. Call configAxios first.");
1134
+ }
1135
+ return axiosInstance.getAxiosInstance();
1136
+ };
1023
1137
  /**
1024
- * Retrieves the current data persistence preference.
1025
- * Always attempts to load the configuration from storage. If it fails, it uses the internal state.
1138
+ * Creates a new AxiosService instance with custom headers.
1026
1139
  *
1027
- * @returns {Promise<SessionPreference>} A promise that resolves with the configured persistence preference ('local' or 'session').
1028
- */
1029
- async function getSessionPersistence() {
1030
- await loadSessionConfig();
1031
- return _sessionConfig.PERSISTENCE;
1032
- }
1033
- /**
1034
- * Retrieves the complete session configuration.
1035
- * Always attempts to load the configuration from storage. If it fails, it uses the internal state.
1140
+ * @param {CustomAxiosConfig} config - An object containing the base URL and optional custom headers.
1141
+ * @param {string} config.baseURL - The base URL for the Axios instance.
1142
+ * @param {Record<string, string>} [config.headers] - Custom headers to set (optional).
1036
1143
  *
1037
- * @returns {Promise<SessionConfig>} A promise that resolves with the session configuration object.
1144
+ * @returns {AxiosService} The new AxiosService instance.
1038
1145
  */
1039
- async function getSessionConfig() {
1040
- await loadSessionConfig();
1041
- return _sessionConfig;
1042
- }
1146
+ const createCustomAxiosInstance = (config) => {
1147
+ return new AxiosService(config.baseURL, config.headers).getAxiosInstance();
1148
+ };
1043
1149
 
1044
1150
  /**
1045
1151
  * Creates and downloads a file from Blob data.
@@ -2703,118 +2809,6 @@ function isValidHexNumber(hex) {
2703
2809
  // console.log(isValidHexNumber('1A3F')); // true
2704
2810
  // console.log(isValidHexNumber('GHIJ')); // false
2705
2811
 
2706
- /**
2707
- * Clears all stored authentication data (access and refresh tokens)
2708
- * from either sessionStorage, localStorage, or both based on the provided location preference.
2709
- *
2710
- * @param {LocationPreference} location - The storage preference ('local' for localStorage, 'session' for sessionStorage, 'any' for both).
2711
- * @returns {Promise<void>} A promise that resolves when all relevant storage items are removed.
2712
- */
2713
- const cleanCredentials = async (location) => {
2714
- const tokensConfig = getTokenConfig();
2715
- Object.keys(tokensConfig).forEach((key) => {
2716
- const itemKey = tokensConfig[key];
2717
- if (location === "local" || location === "any") {
2718
- localStorage.removeItem(itemKey);
2719
- }
2720
- if (location === "session" || location === "any") {
2721
- sessionStorage.removeItem(itemKey);
2722
- }
2723
- });
2724
- };
2725
- /**
2726
- * Retrieves the authentication token (access token) from storage, decrypting it
2727
- * using the provided secret key and based on the specified session preference.
2728
- *
2729
- * @param {string} secretKey - The secret key used for decryption.
2730
- * @param {SessionPreference} preference - The storage preference ('local' for localStorage, 'session' for sessionStorage).
2731
- * @returns {Promise<string | null>} A promise that resolves with the decrypted access token, or null if not found.
2732
- */
2733
- const getAuthToken = async (secretKey, location) => {
2734
- const tokensConfig = getTokenConfig();
2735
- return await getDecryptedItem(tokensConfig.ACCESS_TOKEN, secretKey, location);
2736
- };
2737
- /**
2738
- * Retrieves the authentication refresh token from storage, decrypting it
2739
- * using the provided secret key and based on the specified session preference.
2740
- *
2741
- * @param {string} secretKey - The secret key used for decryption.
2742
- * @param {SessionPreference} preference - The storage preference ('local' for localStorage, 'session' for sessionStorage).
2743
- * @returns {Promise<string | null>} A promise that resolves with the decrypted refresh token, or null if not found.
2744
- */
2745
- const getAuthRefreshToken = async (secretKey, location) => {
2746
- const tokensConfig = getTokenConfig();
2747
- return await getDecryptedItem(tokensConfig.REFRESH_TOKEN, secretKey, location);
2748
- };
2749
- /**
2750
- * Stores the authentication token (access token) in storage after encrypting it,
2751
- * based on the specified session preference.
2752
- *
2753
- * @param {string} token - The access token to store.
2754
- * @param {string} secretKey - The secret key used for encryption.
2755
- * @param {SessionPreference} preference - The storage preference ('local' for localStorage, 'session' for sessionStorage).
2756
- * @returns {Promise<void>} A promise that resolves when the token is successfully stored.
2757
- */
2758
- const storeAuthToken = async (token, secretKey, location) => {
2759
- const tokensConfig = getTokenConfig();
2760
- await storeEncryptedItem(tokensConfig.ACCESS_TOKEN, token, secretKey, location);
2761
- };
2762
- /**
2763
- * Stores the authentication refresh token in storage after encrypting it,
2764
- * based on the specified session preference.
2765
- *
2766
- * @param {string} token - The refresh token to store.
2767
- * @param {string} secretKey - The secret key used for encryption.
2768
- * @param {SessionPreference} preference - The storage preference ('local' for localStorage, 'session' for sessionStorage).
2769
- * @returns {Promise<void>} A promise that resolves when the token is successfully stored.
2770
- */
2771
- const storeAuthRefreshToken = async (token, secretKey, location) => {
2772
- const tokensConfig = getTokenConfig();
2773
- await storeEncryptedItem(tokensConfig.REFRESH_TOKEN, token, secretKey, location);
2774
- };
2775
- /**
2776
- * Verifies the validity and expiration of the current authentication token.
2777
- * If the token is missing, invalid, or expired, appropriate errors are thrown and credentials are cleaned.
2778
- *
2779
- * @returns {Promise<boolean>} True if the token is valid and unexpired.
2780
- * @throws {Error} "TOKEN_MISSING" if no token is found, "TOKEN_EXPIRED" if the token has expired,
2781
- * "TOKEN_INVALID" if the token format is invalid.
2782
- */
2783
- const verifyAuth = async () => {
2784
- const sessionPersistence = 'any';
2785
- const handleAuthError = async (message, shouldClean = true) => {
2786
- handleError(message, false);
2787
- if (shouldClean) {
2788
- await cleanCredentials(sessionPersistence);
2789
- }
2790
- return false;
2791
- };
2792
- try {
2793
- const token = await getAuthToken(getAppKey(), sessionPersistence);
2794
- if (!token) {
2795
- return await handleAuthError("TOKEN_MISSING: No valid token found");
2796
- }
2797
- let decoded;
2798
- try {
2799
- decoded = jwtDecode(token);
2800
- }
2801
- catch (decodeError) {
2802
- return await handleAuthError("TOKEN_INVALID: Invalid token format");
2803
- }
2804
- const currentTime = Date.now() / 1000;
2805
- if (typeof decoded.exp !== "number") {
2806
- return await handleAuthError("TOKEN_INVALID: Invalid expiration format");
2807
- }
2808
- if (decoded.exp <= currentTime) {
2809
- return await handleAuthError("TOKEN_EXPIRED: Token is expired");
2810
- }
2811
- return true;
2812
- }
2813
- catch (error) {
2814
- return await handleAuthError("AUTH_ERROR: An unexpected error occurred", true);
2815
- }
2816
- };
2817
-
2818
2812
  class RestStd {
2819
2813
  static resource;
2820
2814
  static isFormData = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arex95/vue-core",
3
- "version": "1.1.35",
3
+ "version": "1.1.36",
4
4
  "description": "Opinionated Vue Core",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",