@nocios/crudify-ui 1.2.34 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1289,9 +1289,16 @@ __export(index_exports, {
1289
1289
  CrudifyLogin: () => CrudifyLogin_default,
1290
1290
  ERROR_CODES: () => ERROR_CODES,
1291
1291
  ERROR_SEVERITY_MAP: () => ERROR_SEVERITY_MAP,
1292
+ LoginComponent: () => LoginComponent,
1293
+ ProtectedRoute: () => ProtectedRoute,
1294
+ SessionDebugInfo: () => SessionDebugInfo,
1295
+ SessionManager: () => SessionManager,
1296
+ SessionProvider: () => SessionProvider,
1297
+ SessionStatus: () => SessionStatus,
1298
+ TokenStorage: () => TokenStorage,
1292
1299
  UserProfileDisplay: () => UserProfileDisplay_default,
1293
1300
  configurationManager: () => configurationManager,
1294
- crudify: () => import_crudify_browser8.default,
1301
+ crudify: () => import_crudify_browser9.default,
1295
1302
  crudifyInitializer: () => crudifyInitializer,
1296
1303
  decodeJwtSafely: () => decodeJwtSafely,
1297
1304
  getCookie: () => getCookie,
@@ -1314,10 +1321,12 @@ __export(index_exports, {
1314
1321
  useCrudifyInstance: () => useCrudifyInstance,
1315
1322
  useCrudifyLogin: () => useCrudifyLogin,
1316
1323
  useCrudifyUser: () => useCrudifyUser,
1324
+ useSession: () => useSession,
1325
+ useSessionContext: () => useSessionContext,
1317
1326
  useUserProfile: () => useUserProfile
1318
1327
  });
1319
1328
  module.exports = __toCommonJS(index_exports);
1320
- var import_crudify_browser8 = __toESM(require("@nocios/crudify-browser"));
1329
+ var import_crudify_browser9 = __toESM(require("@nocios/crudify-browser"));
1321
1330
  __reExport(index_exports, require("@nocios/crudify-browser"), module.exports);
1322
1331
 
1323
1332
  // src/components/CrudifyLogin/index.tsx
@@ -2009,7 +2018,7 @@ var useCrudifyAuth = () => {
2009
2018
  // src/components/CrudifyLogin/Forms/LoginForm.tsx
2010
2019
  var import_jsx_runtime5 = require("react/jsx-runtime");
2011
2020
  var LoginForm = ({ onScreenChange, onExternalNavigate, onLoginSuccess, onError, redirectUrl = "/" }) => {
2012
- const { crudify: crudify8 } = useCrudify();
2021
+ const { crudify: crudify9 } = useCrudify();
2013
2022
  const { state, updateFormData, setFieldError, clearErrors, setLoading } = useLoginState();
2014
2023
  const { setToken } = useCrudifyAuth();
2015
2024
  const { t } = useTranslation();
@@ -2061,10 +2070,10 @@ var LoginForm = ({ onScreenChange, onExternalNavigate, onLoginSuccess, onError,
2061
2070
  clearErrors();
2062
2071
  setLoading(true);
2063
2072
  try {
2064
- if (!crudify8) {
2073
+ if (!crudify9) {
2065
2074
  throw new Error("Crudify not initialized");
2066
2075
  }
2067
- const response = await crudify8.login(state.formData.username, state.formData.password);
2076
+ const response = await crudify9.login(state.formData.username, state.formData.password);
2068
2077
  setLoading(false);
2069
2078
  if (response.success) {
2070
2079
  console.log("\u{1F510} LoginForm - Login successful, setting tokens");
@@ -2227,7 +2236,7 @@ var import_react7 = require("react");
2227
2236
  var import_material2 = require("@mui/material");
2228
2237
  var import_jsx_runtime6 = require("react/jsx-runtime");
2229
2238
  var ForgotPasswordForm = ({ onScreenChange, onError }) => {
2230
- const { crudify: crudify8 } = useCrudify();
2239
+ const { crudify: crudify9 } = useCrudify();
2231
2240
  const [email, setEmail] = (0, import_react7.useState)("");
2232
2241
  const [loading, setLoading] = (0, import_react7.useState)(false);
2233
2242
  const [errors, setErrors] = (0, import_react7.useState)([]);
@@ -2256,7 +2265,7 @@ var ForgotPasswordForm = ({ onScreenChange, onError }) => {
2256
2265
  return emailRegex.test(email2);
2257
2266
  };
2258
2267
  const handleSubmit = async () => {
2259
- if (loading || !crudify8) return;
2268
+ if (loading || !crudify9) return;
2260
2269
  setErrors([]);
2261
2270
  setHelperTextEmail(null);
2262
2271
  if (!email) {
@@ -2270,7 +2279,7 @@ var ForgotPasswordForm = ({ onScreenChange, onError }) => {
2270
2279
  setLoading(true);
2271
2280
  try {
2272
2281
  const data = [{ operation: "requestPasswordReset", data: { email } }];
2273
- const response = await crudify8.transaction(data);
2282
+ const response = await crudify9.transaction(data);
2274
2283
  if (response.success) {
2275
2284
  if (response.data && response.data.existingCodeValid) {
2276
2285
  setCodeAlreadyExists(true);
@@ -2373,7 +2382,7 @@ var import_react8 = require("react");
2373
2382
  var import_material3 = require("@mui/material");
2374
2383
  var import_jsx_runtime7 = require("react/jsx-runtime");
2375
2384
  var ResetPasswordForm = ({ onScreenChange, onError, searchParams, onResetSuccess }) => {
2376
- const { crudify: crudify8 } = useCrudify();
2385
+ const { crudify: crudify9 } = useCrudify();
2377
2386
  const [newPassword, setNewPassword] = (0, import_react8.useState)("");
2378
2387
  const [confirmPassword, setConfirmPassword] = (0, import_react8.useState)("");
2379
2388
  const [loading, setLoading] = (0, import_react8.useState)(false);
@@ -2453,9 +2462,9 @@ var ResetPasswordForm = ({ onScreenChange, onError, searchParams, onResetSuccess
2453
2462
  setErrors([t("resetPassword.invalidCode")]);
2454
2463
  setValidatingCode(false);
2455
2464
  setTimeout(() => onScreenChange?.("forgotPassword"), 3e3);
2456
- }, [searchParams, crudify8, t, onScreenChange]);
2465
+ }, [searchParams, crudify9, t, onScreenChange]);
2457
2466
  (0, import_react8.useEffect)(() => {
2458
- if (crudify8 && pendingValidation && !isValidating) {
2467
+ if (crudify9 && pendingValidation && !isValidating) {
2459
2468
  setIsValidating(true);
2460
2469
  const validateCode = async (emailToValidate, codeToValidate) => {
2461
2470
  try {
@@ -2465,7 +2474,7 @@ var ResetPasswordForm = ({ onScreenChange, onError, searchParams, onResetSuccess
2465
2474
  data: { email: emailToValidate, codePassword: codeToValidate }
2466
2475
  }
2467
2476
  ];
2468
- const response = await crudify8.transaction(data);
2477
+ const response = await crudify9.transaction(data);
2469
2478
  if (response.data && Array.isArray(response.data)) {
2470
2479
  const validationResult = response.data[0];
2471
2480
  if (validationResult && validationResult.response && validationResult.response.status === "OK") {
@@ -2494,7 +2503,7 @@ var ResetPasswordForm = ({ onScreenChange, onError, searchParams, onResetSuccess
2494
2503
  };
2495
2504
  validateCode(pendingValidation.email, pendingValidation.code);
2496
2505
  }
2497
- }, [crudify8, pendingValidation, t, onScreenChange]);
2506
+ }, [crudify9, pendingValidation, t, onScreenChange]);
2498
2507
  const validatePassword = (password) => {
2499
2508
  if (password.length < 8) {
2500
2509
  return t("resetPassword.passwordTooShort");
@@ -2502,7 +2511,7 @@ var ResetPasswordForm = ({ onScreenChange, onError, searchParams, onResetSuccess
2502
2511
  return null;
2503
2512
  };
2504
2513
  const handleSubmit = async () => {
2505
- if (loading || !crudify8) return;
2514
+ if (loading || !crudify9) return;
2506
2515
  setErrors([]);
2507
2516
  setHelperTextNewPassword(null);
2508
2517
  setHelperTextConfirmPassword(null);
@@ -2533,7 +2542,7 @@ var ResetPasswordForm = ({ onScreenChange, onError, searchParams, onResetSuccess
2533
2542
  data: { email, codePassword: code, newPassword }
2534
2543
  }
2535
2544
  ];
2536
- const response = await crudify8.transaction(data);
2545
+ const response = await crudify9.transaction(data);
2537
2546
  if (response.success) {
2538
2547
  setErrors([]);
2539
2548
  setTimeout(() => {
@@ -2644,7 +2653,7 @@ var import_react9 = require("react");
2644
2653
  var import_material4 = require("@mui/material");
2645
2654
  var import_jsx_runtime8 = require("react/jsx-runtime");
2646
2655
  var CheckCodeForm = ({ onScreenChange, onError, searchParams }) => {
2647
- const { crudify: crudify8 } = useCrudify();
2656
+ const { crudify: crudify9 } = useCrudify();
2648
2657
  const [code, setCode] = (0, import_react9.useState)("");
2649
2658
  const [loading, setLoading] = (0, import_react9.useState)(false);
2650
2659
  const [errors, setErrors] = (0, import_react9.useState)([]);
@@ -2683,7 +2692,7 @@ var CheckCodeForm = ({ onScreenChange, onError, searchParams }) => {
2683
2692
  }
2684
2693
  }, [searchParams, onScreenChange]);
2685
2694
  const handleSubmit = async () => {
2686
- if (loading || !crudify8) return;
2695
+ if (loading || !crudify9) return;
2687
2696
  setErrors([]);
2688
2697
  setHelperTextCode(null);
2689
2698
  if (!code) {
@@ -2702,7 +2711,7 @@ var CheckCodeForm = ({ onScreenChange, onError, searchParams }) => {
2702
2711
  data: { email, codePassword: code }
2703
2712
  }
2704
2713
  ];
2705
- const response = await crudify8.transaction(data);
2714
+ const response = await crudify9.transaction(data);
2706
2715
  if (response.success) {
2707
2716
  onScreenChange?.("resetPassword", { email, code, fromCodeVerification: "true" });
2708
2717
  } else {
@@ -3699,12 +3708,18 @@ var useCrudifyInstance = () => {
3699
3708
  }
3700
3709
  }, [isReady, waitForReady]);
3701
3710
  const ensureTokenSync = (0, import_react15.useCallback)(async () => {
3711
+ console.log("\u{1F504} useCrudifyInstance - ensureTokenSync: Starting token sync check");
3702
3712
  const { tokenManager: tokenManager2 } = await Promise.resolve().then(() => (init_TokenManager(), TokenManager_exports));
3703
3713
  const tmToken = tokenManager2.getToken();
3704
3714
  const crudifyToken = import_crudify_browser7.default.getToken?.();
3715
+ console.log(" - TokenManager token in sync check:", tmToken ? `${tmToken.substring(0, 20)}...` : "null");
3716
+ console.log(" - crudify token in sync check:", crudifyToken ? `${crudifyToken.substring(0, 20)}...` : "null");
3705
3717
  if (tmToken && tmToken !== crudifyToken) {
3706
3718
  console.log("\u{1F504} useCrudifyInstance - Forcing token sync for authenticated operation");
3707
3719
  import_crudify_browser7.default.setToken(tmToken);
3720
+ console.log(" - crudify token after sync:", import_crudify_browser7.default.getToken?.() ? `${import_crudify_browser7.default.getToken?.()?.substring(0, 20)}...` : "null");
3721
+ } else {
3722
+ console.log("\u{1F504} useCrudifyInstance - No token sync needed, tokens match or no token");
3708
3723
  }
3709
3724
  }, []);
3710
3725
  const getStructure = (0, import_react15.useCallback)(async (options) => {
@@ -3869,12 +3884,839 @@ init_CrudifyDataProvider();
3869
3884
  init_jwtUtils();
3870
3885
  init_cookies();
3871
3886
  init_secureStorage();
3887
+
3888
+ // src/core/SessionManager.ts
3889
+ var import_crudify_browser8 = __toESM(require("@nocios/crudify-browser"));
3890
+
3891
+ // src/utils/tokenStorage.ts
3892
+ var import_crypto_js2 = __toESM(require("crypto-js"));
3893
+ var _TokenStorage = class _TokenStorage {
3894
+ /**
3895
+ * Configurar tipo de almacenamiento
3896
+ */
3897
+ static setStorageType(type) {
3898
+ _TokenStorage.storageType = type;
3899
+ }
3900
+ /**
3901
+ * Verificar si el storage está disponible
3902
+ */
3903
+ static isStorageAvailable(type) {
3904
+ try {
3905
+ const storage = window[type];
3906
+ const testKey = "__storage_test__";
3907
+ storage.setItem(testKey, "test");
3908
+ storage.removeItem(testKey);
3909
+ return true;
3910
+ } catch {
3911
+ return false;
3912
+ }
3913
+ }
3914
+ /**
3915
+ * Obtener instancia de storage
3916
+ */
3917
+ static getStorage() {
3918
+ if (_TokenStorage.storageType === "none") return null;
3919
+ if (!_TokenStorage.isStorageAvailable(_TokenStorage.storageType)) {
3920
+ console.warn(`Crudify: ${_TokenStorage.storageType} not available, tokens won't persist`);
3921
+ return null;
3922
+ }
3923
+ return window[_TokenStorage.storageType];
3924
+ }
3925
+ /**
3926
+ * Encriptar datos sensibles
3927
+ */
3928
+ static encrypt(data) {
3929
+ try {
3930
+ return import_crypto_js2.default.AES.encrypt(data, _TokenStorage.ENCRYPTION_KEY).toString();
3931
+ } catch (error) {
3932
+ console.error("Crudify: Encryption failed", error);
3933
+ return data;
3934
+ }
3935
+ }
3936
+ /**
3937
+ * Desencriptar datos
3938
+ */
3939
+ static decrypt(encryptedData) {
3940
+ try {
3941
+ const bytes = import_crypto_js2.default.AES.decrypt(encryptedData, _TokenStorage.ENCRYPTION_KEY);
3942
+ const decrypted = bytes.toString(import_crypto_js2.default.enc.Utf8);
3943
+ return decrypted || encryptedData;
3944
+ } catch (error) {
3945
+ console.error("Crudify: Decryption failed", error);
3946
+ return encryptedData;
3947
+ }
3948
+ }
3949
+ /**
3950
+ * Guardar tokens de forma segura
3951
+ */
3952
+ static saveTokens(tokens) {
3953
+ const storage = _TokenStorage.getStorage();
3954
+ if (!storage) return;
3955
+ try {
3956
+ const tokenData = {
3957
+ accessToken: tokens.accessToken,
3958
+ refreshToken: tokens.refreshToken,
3959
+ expiresAt: tokens.expiresAt,
3960
+ refreshExpiresAt: tokens.refreshExpiresAt,
3961
+ savedAt: Date.now()
3962
+ };
3963
+ const encrypted = _TokenStorage.encrypt(JSON.stringify(tokenData));
3964
+ storage.setItem(_TokenStorage.TOKEN_KEY, encrypted);
3965
+ console.debug("Crudify: Tokens saved successfully");
3966
+ } catch (error) {
3967
+ console.error("Crudify: Failed to save tokens", error);
3968
+ }
3969
+ }
3970
+ /**
3971
+ * Obtener tokens guardados
3972
+ */
3973
+ static getTokens() {
3974
+ const storage = _TokenStorage.getStorage();
3975
+ if (!storage) return null;
3976
+ try {
3977
+ const encrypted = storage.getItem(_TokenStorage.TOKEN_KEY);
3978
+ if (!encrypted) return null;
3979
+ const decrypted = _TokenStorage.decrypt(encrypted);
3980
+ const tokenData = JSON.parse(decrypted);
3981
+ if (!tokenData.accessToken || !tokenData.refreshToken || !tokenData.expiresAt || !tokenData.refreshExpiresAt) {
3982
+ console.warn("Crudify: Incomplete token data found, clearing storage");
3983
+ _TokenStorage.clearTokens();
3984
+ return null;
3985
+ }
3986
+ if (Date.now() >= tokenData.refreshExpiresAt) {
3987
+ console.info("Crudify: Refresh token expired, clearing storage");
3988
+ _TokenStorage.clearTokens();
3989
+ return null;
3990
+ }
3991
+ return {
3992
+ accessToken: tokenData.accessToken,
3993
+ refreshToken: tokenData.refreshToken,
3994
+ expiresAt: tokenData.expiresAt,
3995
+ refreshExpiresAt: tokenData.refreshExpiresAt
3996
+ };
3997
+ } catch (error) {
3998
+ console.error("Crudify: Failed to retrieve tokens", error);
3999
+ _TokenStorage.clearTokens();
4000
+ return null;
4001
+ }
4002
+ }
4003
+ /**
4004
+ * Limpiar tokens almacenados
4005
+ */
4006
+ static clearTokens() {
4007
+ const storage = _TokenStorage.getStorage();
4008
+ if (!storage) return;
4009
+ try {
4010
+ storage.removeItem(_TokenStorage.TOKEN_KEY);
4011
+ console.debug("Crudify: Tokens cleared from storage");
4012
+ } catch (error) {
4013
+ console.error("Crudify: Failed to clear tokens", error);
4014
+ }
4015
+ }
4016
+ /**
4017
+ * Verificar si hay tokens válidos guardados
4018
+ */
4019
+ static hasValidTokens() {
4020
+ const tokens = _TokenStorage.getTokens();
4021
+ return tokens !== null;
4022
+ }
4023
+ /**
4024
+ * Obtener información de expiración
4025
+ */
4026
+ static getExpirationInfo() {
4027
+ const tokens = _TokenStorage.getTokens();
4028
+ if (!tokens) return null;
4029
+ const now = Date.now();
4030
+ return {
4031
+ accessExpired: now >= tokens.expiresAt,
4032
+ refreshExpired: now >= tokens.refreshExpiresAt,
4033
+ accessExpiresIn: Math.max(0, tokens.expiresAt - now),
4034
+ refreshExpiresIn: Math.max(0, tokens.refreshExpiresAt - now)
4035
+ };
4036
+ }
4037
+ /**
4038
+ * Actualizar solo el access token (después de refresh)
4039
+ */
4040
+ static updateAccessToken(newAccessToken, newExpiresAt) {
4041
+ const existingTokens = _TokenStorage.getTokens();
4042
+ if (!existingTokens) {
4043
+ console.warn("Crudify: Cannot update access token, no existing tokens found");
4044
+ return;
4045
+ }
4046
+ _TokenStorage.saveTokens({
4047
+ ...existingTokens,
4048
+ accessToken: newAccessToken,
4049
+ expiresAt: newExpiresAt
4050
+ });
4051
+ }
4052
+ };
4053
+ _TokenStorage.TOKEN_KEY = "crudify_tokens";
4054
+ _TokenStorage.ENCRYPTION_KEY = "crudify_secure_key_v1";
4055
+ _TokenStorage.storageType = "localStorage";
4056
+ var TokenStorage = _TokenStorage;
4057
+
4058
+ // src/core/SessionManager.ts
4059
+ var SessionManager = class _SessionManager {
4060
+ constructor() {
4061
+ this.config = {};
4062
+ this.initialized = false;
4063
+ }
4064
+ static getInstance() {
4065
+ if (!_SessionManager.instance) {
4066
+ _SessionManager.instance = new _SessionManager();
4067
+ }
4068
+ return _SessionManager.instance;
4069
+ }
4070
+ /**
4071
+ * Inicializar el SessionManager
4072
+ */
4073
+ async initialize(config = {}) {
4074
+ if (this.initialized) {
4075
+ console.warn("SessionManager: Already initialized");
4076
+ return;
4077
+ }
4078
+ this.config = {
4079
+ storageType: "localStorage",
4080
+ autoRestore: true,
4081
+ enableLogging: false,
4082
+ ...config
4083
+ };
4084
+ TokenStorage.setStorageType(this.config.storageType || "localStorage");
4085
+ if (this.config.enableLogging) {
4086
+ }
4087
+ if (this.config.autoRestore) {
4088
+ await this.restoreSession();
4089
+ }
4090
+ this.initialized = true;
4091
+ this.log("SessionManager initialized successfully");
4092
+ }
4093
+ /**
4094
+ * Login con persistencia automática
4095
+ */
4096
+ async login(email, password) {
4097
+ try {
4098
+ this.log("Attempting login...");
4099
+ const response = await import_crudify_browser8.default.login(email, password);
4100
+ if (!response.success) {
4101
+ this.log("Login failed:", response.errors);
4102
+ return {
4103
+ success: false,
4104
+ error: this.formatError(response.errors)
4105
+ };
4106
+ }
4107
+ const tokens = {
4108
+ accessToken: response.data.token,
4109
+ refreshToken: response.data.refreshToken,
4110
+ expiresAt: response.data.expiresAt,
4111
+ refreshExpiresAt: response.data.refreshExpiresAt
4112
+ };
4113
+ TokenStorage.saveTokens(tokens);
4114
+ this.log("Login successful, tokens saved");
4115
+ this.config.onLoginSuccess?.(tokens);
4116
+ return {
4117
+ success: true,
4118
+ tokens
4119
+ };
4120
+ } catch (error) {
4121
+ this.log("Login error:", error);
4122
+ return {
4123
+ success: false,
4124
+ error: error instanceof Error ? error.message : "Unknown error"
4125
+ };
4126
+ }
4127
+ }
4128
+ /**
4129
+ * Logout con limpieza de tokens
4130
+ */
4131
+ async logout() {
4132
+ try {
4133
+ this.log("Logging out...");
4134
+ await import_crudify_browser8.default.logout();
4135
+ TokenStorage.clearTokens();
4136
+ this.log("Logout successful");
4137
+ this.config.onLogout?.();
4138
+ } catch (error) {
4139
+ this.log("Logout error:", error);
4140
+ TokenStorage.clearTokens();
4141
+ }
4142
+ }
4143
+ /**
4144
+ * Restaurar sesión desde storage
4145
+ */
4146
+ async restoreSession() {
4147
+ try {
4148
+ this.log("Attempting to restore session...");
4149
+ const savedTokens = TokenStorage.getTokens();
4150
+ if (!savedTokens) {
4151
+ this.log("No valid tokens found in storage");
4152
+ return false;
4153
+ }
4154
+ import_crudify_browser8.default.setTokens({
4155
+ accessToken: savedTokens.accessToken,
4156
+ refreshToken: savedTokens.refreshToken,
4157
+ expiresAt: savedTokens.expiresAt,
4158
+ refreshExpiresAt: savedTokens.refreshExpiresAt
4159
+ });
4160
+ this.log("Session restored successfully");
4161
+ this.config.onSessionRestored?.(savedTokens);
4162
+ return true;
4163
+ } catch (error) {
4164
+ this.log("Session restore error:", error);
4165
+ TokenStorage.clearTokens();
4166
+ return false;
4167
+ }
4168
+ }
4169
+ /**
4170
+ * Verificar si el usuario está autenticado
4171
+ */
4172
+ isAuthenticated() {
4173
+ return import_crudify_browser8.default.isLogin() || TokenStorage.hasValidTokens();
4174
+ }
4175
+ /**
4176
+ * Obtener información de tokens actuales
4177
+ */
4178
+ getTokenInfo() {
4179
+ const crudifyTokens = import_crudify_browser8.default.getTokenData();
4180
+ const storageInfo = TokenStorage.getExpirationInfo();
4181
+ return {
4182
+ isLoggedIn: this.isAuthenticated(),
4183
+ crudifyTokens,
4184
+ storageInfo,
4185
+ hasValidTokens: TokenStorage.hasValidTokens()
4186
+ };
4187
+ }
4188
+ /**
4189
+ * Refrescar tokens manualmente
4190
+ */
4191
+ async refreshTokens() {
4192
+ try {
4193
+ this.log("Manually refreshing tokens...");
4194
+ const response = await import_crudify_browser8.default.refreshAccessToken();
4195
+ if (!response.success) {
4196
+ this.log("Token refresh failed:", response.errors);
4197
+ TokenStorage.clearTokens();
4198
+ this.config.onSessionExpired?.();
4199
+ return false;
4200
+ }
4201
+ const newTokens = {
4202
+ accessToken: response.data.token,
4203
+ refreshToken: response.data.refreshToken,
4204
+ expiresAt: response.data.expiresAt,
4205
+ refreshExpiresAt: response.data.refreshExpiresAt
4206
+ };
4207
+ TokenStorage.saveTokens(newTokens);
4208
+ this.log("Tokens refreshed and saved successfully");
4209
+ return true;
4210
+ } catch (error) {
4211
+ this.log("Token refresh error:", error);
4212
+ TokenStorage.clearTokens();
4213
+ this.config.onSessionExpired?.();
4214
+ return false;
4215
+ }
4216
+ }
4217
+ /**
4218
+ * Configurar interceptor de respuesta para manejo automático de errores
4219
+ */
4220
+ setupResponseInterceptor() {
4221
+ import_crudify_browser8.default.setResponseInterceptor(async (response) => {
4222
+ if (response.errors) {
4223
+ const hasAuthError = response.errors.some(
4224
+ (error) => error.message?.includes("Unauthorized") || error.message?.includes("Token") || error.extensions?.code === "UNAUTHENTICATED"
4225
+ );
4226
+ if (hasAuthError && TokenStorage.hasValidTokens()) {
4227
+ this.log("Auth error detected, attempting token refresh...");
4228
+ const refreshSuccess = await this.refreshTokens();
4229
+ if (!refreshSuccess) {
4230
+ this.log("Session expired, triggering callback");
4231
+ this.config.onSessionExpired?.();
4232
+ }
4233
+ }
4234
+ }
4235
+ return response;
4236
+ });
4237
+ this.log("Response interceptor configured");
4238
+ }
4239
+ /**
4240
+ * Limpiar sesión completamente
4241
+ */
4242
+ clearSession() {
4243
+ TokenStorage.clearTokens();
4244
+ import_crudify_browser8.default.logout();
4245
+ this.log("Session cleared completely");
4246
+ }
4247
+ // Métodos privados
4248
+ log(message, ...args) {
4249
+ if (this.config.enableLogging) {
4250
+ console.log(`[SessionManager] ${message}`, ...args);
4251
+ }
4252
+ }
4253
+ formatError(errors) {
4254
+ if (!errors) return "Unknown error";
4255
+ if (typeof errors === "string") return errors;
4256
+ if (typeof errors === "object") {
4257
+ const errorMessages = Object.values(errors).flat();
4258
+ return errorMessages.join(", ");
4259
+ }
4260
+ return "Authentication failed";
4261
+ }
4262
+ };
4263
+
4264
+ // src/hooks/useSession.ts
4265
+ var import_react16 = require("react");
4266
+ function useSession(options = {}) {
4267
+ const [state, setState] = (0, import_react16.useState)({
4268
+ isAuthenticated: false,
4269
+ isLoading: true,
4270
+ isInitialized: false,
4271
+ tokens: null,
4272
+ error: null
4273
+ });
4274
+ const sessionManager = SessionManager.getInstance();
4275
+ const initialize = (0, import_react16.useCallback)(async () => {
4276
+ try {
4277
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
4278
+ const config = {
4279
+ autoRestore: options.autoRestore ?? true,
4280
+ enableLogging: options.enableLogging ?? false,
4281
+ onSessionExpired: () => {
4282
+ setState((prev) => ({
4283
+ ...prev,
4284
+ isAuthenticated: false,
4285
+ tokens: null,
4286
+ error: "Session expired"
4287
+ }));
4288
+ options.onSessionExpired?.();
4289
+ },
4290
+ onSessionRestored: (tokens) => {
4291
+ setState((prev) => ({
4292
+ ...prev,
4293
+ isAuthenticated: true,
4294
+ tokens,
4295
+ error: null
4296
+ }));
4297
+ options.onSessionRestored?.(tokens);
4298
+ },
4299
+ onLoginSuccess: (tokens) => {
4300
+ setState((prev) => ({
4301
+ ...prev,
4302
+ isAuthenticated: true,
4303
+ tokens,
4304
+ error: null
4305
+ }));
4306
+ },
4307
+ onLogout: () => {
4308
+ setState((prev) => ({
4309
+ ...prev,
4310
+ isAuthenticated: false,
4311
+ tokens: null,
4312
+ error: null
4313
+ }));
4314
+ }
4315
+ };
4316
+ await sessionManager.initialize(config);
4317
+ sessionManager.setupResponseInterceptor();
4318
+ const isAuth = sessionManager.isAuthenticated();
4319
+ const tokenInfo = sessionManager.getTokenInfo();
4320
+ setState((prev) => ({
4321
+ ...prev,
4322
+ isAuthenticated: isAuth,
4323
+ isInitialized: true,
4324
+ isLoading: false,
4325
+ tokens: tokenInfo.crudifyTokens.accessToken ? {
4326
+ accessToken: tokenInfo.crudifyTokens.accessToken,
4327
+ refreshToken: tokenInfo.crudifyTokens.refreshToken,
4328
+ expiresAt: tokenInfo.crudifyTokens.expiresAt,
4329
+ refreshExpiresAt: tokenInfo.crudifyTokens.refreshExpiresAt
4330
+ } : null
4331
+ }));
4332
+ } catch (error) {
4333
+ setState((prev) => ({
4334
+ ...prev,
4335
+ isLoading: false,
4336
+ isInitialized: true,
4337
+ error: error instanceof Error ? error.message : "Initialization failed"
4338
+ }));
4339
+ }
4340
+ }, [options.autoRestore, options.enableLogging, options.onSessionExpired, options.onSessionRestored]);
4341
+ const login = (0, import_react16.useCallback)(async (email, password) => {
4342
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
4343
+ try {
4344
+ const result = await sessionManager.login(email, password);
4345
+ if (result.success && result.tokens) {
4346
+ setState((prev) => ({
4347
+ ...prev,
4348
+ isAuthenticated: true,
4349
+ tokens: result.tokens,
4350
+ isLoading: false,
4351
+ error: null
4352
+ }));
4353
+ } else {
4354
+ setState((prev) => ({
4355
+ ...prev,
4356
+ isAuthenticated: false,
4357
+ tokens: null,
4358
+ isLoading: false,
4359
+ error: result.error || "Login failed"
4360
+ }));
4361
+ }
4362
+ return result;
4363
+ } catch (error) {
4364
+ const errorMsg = error instanceof Error ? error.message : "Login failed";
4365
+ setState((prev) => ({
4366
+ ...prev,
4367
+ isAuthenticated: false,
4368
+ tokens: null,
4369
+ isLoading: false,
4370
+ error: errorMsg
4371
+ }));
4372
+ return {
4373
+ success: false,
4374
+ error: errorMsg
4375
+ };
4376
+ }
4377
+ }, [sessionManager]);
4378
+ const logout = (0, import_react16.useCallback)(async () => {
4379
+ setState((prev) => ({ ...prev, isLoading: true }));
4380
+ try {
4381
+ await sessionManager.logout();
4382
+ setState((prev) => ({
4383
+ ...prev,
4384
+ isAuthenticated: false,
4385
+ tokens: null,
4386
+ isLoading: false,
4387
+ error: null
4388
+ }));
4389
+ } catch (error) {
4390
+ setState((prev) => ({
4391
+ ...prev,
4392
+ isAuthenticated: false,
4393
+ tokens: null,
4394
+ isLoading: false,
4395
+ error: error instanceof Error ? error.message : "Logout error"
4396
+ }));
4397
+ }
4398
+ }, [sessionManager]);
4399
+ const refreshTokens = (0, import_react16.useCallback)(async () => {
4400
+ try {
4401
+ const success = await sessionManager.refreshTokens();
4402
+ if (success) {
4403
+ const tokenInfo = sessionManager.getTokenInfo();
4404
+ setState((prev) => ({
4405
+ ...prev,
4406
+ tokens: tokenInfo.crudifyTokens.accessToken ? {
4407
+ accessToken: tokenInfo.crudifyTokens.accessToken,
4408
+ refreshToken: tokenInfo.crudifyTokens.refreshToken,
4409
+ expiresAt: tokenInfo.crudifyTokens.expiresAt,
4410
+ refreshExpiresAt: tokenInfo.crudifyTokens.refreshExpiresAt
4411
+ } : null,
4412
+ error: null
4413
+ }));
4414
+ } else {
4415
+ setState((prev) => ({
4416
+ ...prev,
4417
+ isAuthenticated: false,
4418
+ tokens: null,
4419
+ error: "Token refresh failed"
4420
+ }));
4421
+ }
4422
+ return success;
4423
+ } catch (error) {
4424
+ setState((prev) => ({
4425
+ ...prev,
4426
+ isAuthenticated: false,
4427
+ tokens: null,
4428
+ error: error instanceof Error ? error.message : "Token refresh failed"
4429
+ }));
4430
+ return false;
4431
+ }
4432
+ }, [sessionManager]);
4433
+ const clearError = (0, import_react16.useCallback)(() => {
4434
+ setState((prev) => ({ ...prev, error: null }));
4435
+ }, []);
4436
+ const getTokenInfo = (0, import_react16.useCallback)(() => {
4437
+ return sessionManager.getTokenInfo();
4438
+ }, [sessionManager]);
4439
+ (0, import_react16.useEffect)(() => {
4440
+ initialize();
4441
+ }, [initialize]);
4442
+ return {
4443
+ // Estado
4444
+ ...state,
4445
+ // Acciones
4446
+ login,
4447
+ logout,
4448
+ refreshTokens,
4449
+ clearError,
4450
+ getTokenInfo,
4451
+ // Utilidades
4452
+ isExpiringSoon: state.tokens ? state.tokens.expiresAt - Date.now() < 5 * 60 * 1e3 : false,
4453
+ // 5 minutos
4454
+ expiresIn: state.tokens ? Math.max(0, state.tokens.expiresAt - Date.now()) : 0,
4455
+ refreshExpiresIn: state.tokens ? Math.max(0, state.tokens.refreshExpiresAt - Date.now()) : 0
4456
+ };
4457
+ }
4458
+
4459
+ // src/providers/SessionProvider.tsx
4460
+ var import_react17 = require("react");
4461
+ var import_jsx_runtime12 = require("react/jsx-runtime");
4462
+ var SessionContext = (0, import_react17.createContext)(void 0);
4463
+ function SessionProvider({ children, options = {} }) {
4464
+ const sessionData = useSession(options);
4465
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SessionContext.Provider, { value: sessionData, children });
4466
+ }
4467
+ function useSessionContext() {
4468
+ const context = (0, import_react17.useContext)(SessionContext);
4469
+ if (context === void 0) {
4470
+ throw new Error("useSessionContext must be used within a SessionProvider");
4471
+ }
4472
+ return context;
4473
+ }
4474
+ function ProtectedRoute({
4475
+ children,
4476
+ fallback = /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { children: "Please log in to access this content" }),
4477
+ redirectTo
4478
+ }) {
4479
+ const { isAuthenticated, isLoading, isInitialized } = useSessionContext();
4480
+ if (!isInitialized || isLoading) {
4481
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { children: "Loading..." });
4482
+ }
4483
+ if (!isAuthenticated) {
4484
+ if (redirectTo) {
4485
+ redirectTo();
4486
+ return null;
4487
+ }
4488
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_jsx_runtime12.Fragment, { children: fallback });
4489
+ }
4490
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_jsx_runtime12.Fragment, { children });
4491
+ }
4492
+ function SessionDebugInfo() {
4493
+ const session = useSessionContext();
4494
+ if (!session.isInitialized) {
4495
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { children: "Session not initialized" });
4496
+ }
4497
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: {
4498
+ padding: "10px",
4499
+ margin: "10px",
4500
+ border: "1px solid #ccc",
4501
+ borderRadius: "4px",
4502
+ fontSize: "12px",
4503
+ fontFamily: "monospace"
4504
+ }, children: [
4505
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h4", { children: "Session Debug Info" }),
4506
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
4507
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: "Authenticated:" }),
4508
+ " ",
4509
+ session.isAuthenticated ? "Yes" : "No"
4510
+ ] }),
4511
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
4512
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: "Loading:" }),
4513
+ " ",
4514
+ session.isLoading ? "Yes" : "No"
4515
+ ] }),
4516
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
4517
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: "Error:" }),
4518
+ " ",
4519
+ session.error || "None"
4520
+ ] }),
4521
+ session.tokens && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
4522
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
4523
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: "Access Token:" }),
4524
+ " ",
4525
+ session.tokens.accessToken.substring(0, 20),
4526
+ "..."
4527
+ ] }),
4528
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
4529
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: "Refresh Token:" }),
4530
+ " ",
4531
+ session.tokens.refreshToken.substring(0, 20),
4532
+ "..."
4533
+ ] }),
4534
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
4535
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: "Access Expires In:" }),
4536
+ " ",
4537
+ Math.round(session.expiresIn / 1e3 / 60),
4538
+ " minutes"
4539
+ ] }),
4540
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
4541
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: "Refresh Expires In:" }),
4542
+ " ",
4543
+ Math.round(session.refreshExpiresIn / 1e3 / 60 / 60),
4544
+ " hours"
4545
+ ] }),
4546
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
4547
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: "Expiring Soon:" }),
4548
+ " ",
4549
+ session.isExpiringSoon ? "Yes" : "No"
4550
+ ] })
4551
+ ] })
4552
+ ] });
4553
+ }
4554
+
4555
+ // src/components/LoginComponent.tsx
4556
+ var import_react18 = require("react");
4557
+ var import_material8 = require("@mui/material");
4558
+ var import_jsx_runtime13 = require("react/jsx-runtime");
4559
+ function LoginComponent() {
4560
+ const [email, setEmail] = (0, import_react18.useState)("");
4561
+ const [password, setPassword] = (0, import_react18.useState)("");
4562
+ const [showForm, setShowForm] = (0, import_react18.useState)(false);
4563
+ const {
4564
+ isAuthenticated,
4565
+ isLoading,
4566
+ error,
4567
+ login,
4568
+ logout,
4569
+ refreshTokens,
4570
+ clearError,
4571
+ isExpiringSoon,
4572
+ expiresIn
4573
+ } = useSessionContext();
4574
+ const handleLogin = async (e) => {
4575
+ e.preventDefault();
4576
+ if (!email || !password) {
4577
+ return;
4578
+ }
4579
+ const result = await login(email, password);
4580
+ if (result.success) {
4581
+ setEmail("");
4582
+ setPassword("");
4583
+ setShowForm(false);
4584
+ }
4585
+ };
4586
+ const handleLogout = async () => {
4587
+ await logout();
4588
+ };
4589
+ const handleRefreshTokens = async () => {
4590
+ await refreshTokens();
4591
+ };
4592
+ if (isAuthenticated) {
4593
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_material8.Box, { sx: { maxWidth: 600, mx: "auto", p: 3 }, children: [
4594
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Typography, { variant: "h4", gutterBottom: true, children: "Welcome! \u{1F389}" }),
4595
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Alert, { severity: "success", sx: { mb: 3 }, children: "You are successfully logged in with Refresh Token Pattern enabled" }),
4596
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_material8.Box, { sx: { mb: 3, p: 2, bgcolor: "background.paper", border: 1, borderColor: "divider", borderRadius: 1 }, children: [
4597
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Typography, { variant: "h6", gutterBottom: true, children: "Token Status" }),
4598
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_material8.Typography, { variant: "body2", color: "text.secondary", children: [
4599
+ "Access Token expires in: ",
4600
+ Math.round(expiresIn / 1e3 / 60),
4601
+ " minutes"
4602
+ ] }),
4603
+ isExpiringSoon && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Alert, { severity: "warning", sx: { mt: 1 }, children: "Token expires soon - automatic refresh will happen" })
4604
+ ] }),
4605
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_material8.Box, { sx: { display: "flex", gap: 2, flexWrap: "wrap" }, children: [
4606
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4607
+ import_material8.Button,
4608
+ {
4609
+ variant: "contained",
4610
+ onClick: handleRefreshTokens,
4611
+ disabled: isLoading,
4612
+ startIcon: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.CircularProgress, { size: 16 }) : null,
4613
+ children: "Refresh Tokens"
4614
+ }
4615
+ ),
4616
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4617
+ import_material8.Button,
4618
+ {
4619
+ variant: "outlined",
4620
+ color: "error",
4621
+ onClick: handleLogout,
4622
+ disabled: isLoading,
4623
+ children: "Logout"
4624
+ }
4625
+ )
4626
+ ] }),
4627
+ error && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Alert, { severity: "error", sx: { mt: 2 }, onClose: clearError, children: error })
4628
+ ] });
4629
+ }
4630
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_material8.Box, { sx: { maxWidth: 400, mx: "auto", p: 3 }, children: [
4631
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Typography, { variant: "h4", gutterBottom: true, align: "center", children: "Login with Refresh Tokens" }),
4632
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Alert, { severity: "info", sx: { mb: 3 }, children: "This demo shows the new Refresh Token Pattern with automatic session management" }),
4633
+ !showForm ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4634
+ import_material8.Button,
4635
+ {
4636
+ fullWidth: true,
4637
+ variant: "contained",
4638
+ size: "large",
4639
+ onClick: () => setShowForm(true),
4640
+ sx: { mt: 2 },
4641
+ children: "Show Login Form"
4642
+ }
4643
+ ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("form", { onSubmit: handleLogin, children: [
4644
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4645
+ import_material8.TextField,
4646
+ {
4647
+ fullWidth: true,
4648
+ label: "Email",
4649
+ type: "email",
4650
+ value: email,
4651
+ onChange: (e) => setEmail(e.target.value),
4652
+ margin: "normal",
4653
+ required: true,
4654
+ autoComplete: "email"
4655
+ }
4656
+ ),
4657
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4658
+ import_material8.TextField,
4659
+ {
4660
+ fullWidth: true,
4661
+ label: "Password",
4662
+ type: "password",
4663
+ value: password,
4664
+ onChange: (e) => setPassword(e.target.value),
4665
+ margin: "normal",
4666
+ required: true,
4667
+ autoComplete: "current-password"
4668
+ }
4669
+ ),
4670
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4671
+ import_material8.Button,
4672
+ {
4673
+ type: "submit",
4674
+ fullWidth: true,
4675
+ variant: "contained",
4676
+ size: "large",
4677
+ disabled: isLoading,
4678
+ startIcon: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.CircularProgress, { size: 16 }) : null,
4679
+ sx: { mt: 3, mb: 2 },
4680
+ children: isLoading ? "Logging in..." : "Login"
4681
+ }
4682
+ )
4683
+ ] }),
4684
+ error && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Alert, { severity: "error", sx: { mt: 2 }, onClose: clearError, children: error })
4685
+ ] });
4686
+ }
4687
+ function SessionStatus() {
4688
+ const { isAuthenticated, isLoading, isExpiringSoon, expiresIn } = useSessionContext();
4689
+ if (isLoading) {
4690
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_material8.Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
4691
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.CircularProgress, { size: 16 }),
4692
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Typography, { variant: "caption", children: "Loading session..." })
4693
+ ] });
4694
+ }
4695
+ if (!isAuthenticated) {
4696
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Typography, { variant: "caption", color: "text.secondary", children: "Not logged in" });
4697
+ }
4698
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_material8.Box, { children: [
4699
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_material8.Typography, { variant: "caption", color: "success.main", children: "\u2713 Authenticated" }),
4700
+ isExpiringSoon && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_material8.Typography, { variant: "caption", color: "warning.main", display: "block", children: [
4701
+ "\u26A0 Token expires in ",
4702
+ Math.round(expiresIn / 1e3 / 60),
4703
+ " min"
4704
+ ] })
4705
+ ] });
4706
+ }
3872
4707
  // Annotate the CommonJS export names for ESM import in node:
3873
4708
  0 && (module.exports = {
3874
4709
  CrudifyDataProvider,
3875
4710
  CrudifyLogin,
3876
4711
  ERROR_CODES,
3877
4712
  ERROR_SEVERITY_MAP,
4713
+ LoginComponent,
4714
+ ProtectedRoute,
4715
+ SessionDebugInfo,
4716
+ SessionManager,
4717
+ SessionProvider,
4718
+ SessionStatus,
4719
+ TokenStorage,
3878
4720
  UserProfileDisplay,
3879
4721
  configurationManager,
3880
4722
  crudify,
@@ -3900,6 +4742,8 @@ init_secureStorage();
3900
4742
  useCrudifyInstance,
3901
4743
  useCrudifyLogin,
3902
4744
  useCrudifyUser,
4745
+ useSession,
4746
+ useSessionContext,
3903
4747
  useUserProfile,
3904
4748
  ...require("@nocios/crudify-browser")
3905
4749
  });