@entropy-softworks/ui 2026.7.31
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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/lib/components/auth/LockOverlay.d.ts +22 -0
- package/lib/components/auth/LockOverlay.d.ts.map +1 -0
- package/lib/components/auth/LockOverlay.jsx +82 -0
- package/lib/components/auth/LockOverlay.jsx.map +1 -0
- package/lib/components/auth/PasswordStrengthMeter.d.ts +12 -0
- package/lib/components/auth/PasswordStrengthMeter.d.ts.map +1 -0
- package/lib/components/auth/PasswordStrengthMeter.jsx +81 -0
- package/lib/components/auth/PasswordStrengthMeter.jsx.map +1 -0
- package/lib/components/auth/ProtectedRoute.d.ts +24 -0
- package/lib/components/auth/ProtectedRoute.d.ts.map +1 -0
- package/lib/components/auth/ProtectedRoute.jsx +56 -0
- package/lib/components/auth/ProtectedRoute.jsx.map +1 -0
- package/lib/components/auth/PwnedPasswordWarning.d.ts +13 -0
- package/lib/components/auth/PwnedPasswordWarning.d.ts.map +1 -0
- package/lib/components/auth/PwnedPasswordWarning.jsx +29 -0
- package/lib/components/auth/PwnedPasswordWarning.jsx.map +1 -0
- package/lib/components/auth/index.d.ts +5 -0
- package/lib/components/auth/index.d.ts.map +1 -0
- package/lib/components/auth/index.js +5 -0
- package/lib/components/auth/index.js.map +1 -0
- package/lib/components/bottom-nav.d.ts +25 -0
- package/lib/components/bottom-nav.d.ts.map +1 -0
- package/lib/components/bottom-nav.jsx +51 -0
- package/lib/components/bottom-nav.jsx.map +1 -0
- package/lib/components/button.d.ts +16 -0
- package/lib/components/button.d.ts.map +1 -0
- package/lib/components/button.jsx +73 -0
- package/lib/components/button.jsx.map +1 -0
- package/lib/components/card.d.ts +22 -0
- package/lib/components/card.d.ts.map +1 -0
- package/lib/components/card.jsx +36 -0
- package/lib/components/card.jsx.map +1 -0
- package/lib/components/checkbox.d.ts +12 -0
- package/lib/components/checkbox.d.ts.map +1 -0
- package/lib/components/checkbox.jsx +16 -0
- package/lib/components/checkbox.jsx.map +1 -0
- package/lib/components/connection-settings.d.ts +41 -0
- package/lib/components/connection-settings.d.ts.map +1 -0
- package/lib/components/connection-settings.jsx +354 -0
- package/lib/components/connection-settings.jsx.map +1 -0
- package/lib/components/context-menu.d.ts +52 -0
- package/lib/components/context-menu.d.ts.map +1 -0
- package/lib/components/context-menu.jsx +24 -0
- package/lib/components/context-menu.jsx.map +1 -0
- package/lib/components/data-table.d.ts +59 -0
- package/lib/components/data-table.d.ts.map +1 -0
- package/lib/components/data-table.jsx +101 -0
- package/lib/components/data-table.jsx.map +1 -0
- package/lib/components/dialog.d.ts +58 -0
- package/lib/components/dialog.d.ts.map +1 -0
- package/lib/components/dialog.jsx +197 -0
- package/lib/components/dialog.jsx.map +1 -0
- package/lib/components/dropdown-menu.d.ts +55 -0
- package/lib/components/dropdown-menu.d.ts.map +1 -0
- package/lib/components/dropdown-menu.jsx +38 -0
- package/lib/components/dropdown-menu.jsx.map +1 -0
- package/lib/components/entropy-boot-splash.d.ts +24 -0
- package/lib/components/entropy-boot-splash.d.ts.map +1 -0
- package/lib/components/entropy-boot-splash.jsx +83 -0
- package/lib/components/entropy-boot-splash.jsx.map +1 -0
- package/lib/components/entropy-mark-animated.d.ts +20 -0
- package/lib/components/entropy-mark-animated.d.ts.map +1 -0
- package/lib/components/entropy-mark-animated.jsx +148 -0
- package/lib/components/entropy-mark-animated.jsx.map +1 -0
- package/lib/components/entropy-mark-geometry.d.ts +8 -0
- package/lib/components/entropy-mark-geometry.d.ts.map +1 -0
- package/lib/components/entropy-mark-geometry.js +10 -0
- package/lib/components/entropy-mark-geometry.js.map +1 -0
- package/lib/components/entropy-mark-sequential.d.ts +12 -0
- package/lib/components/entropy-mark-sequential.d.ts.map +1 -0
- package/lib/components/entropy-mark-sequential.jsx +53 -0
- package/lib/components/entropy-mark-sequential.jsx.map +1 -0
- package/lib/components/entropy-mark.d.ts +29 -0
- package/lib/components/entropy-mark.d.ts.map +1 -0
- package/lib/components/entropy-mark.jsx +66 -0
- package/lib/components/entropy-mark.jsx.map +1 -0
- package/lib/components/error-boundary.d.ts +31 -0
- package/lib/components/error-boundary.d.ts.map +1 -0
- package/lib/components/error-boundary.jsx +40 -0
- package/lib/components/error-boundary.jsx.map +1 -0
- package/lib/components/index.d.ts +41 -0
- package/lib/components/index.d.ts.map +1 -0
- package/lib/components/index.js +43 -0
- package/lib/components/index.js.map +1 -0
- package/lib/components/input.d.ts +26 -0
- package/lib/components/input.d.ts.map +1 -0
- package/lib/components/input.jsx +95 -0
- package/lib/components/input.jsx.map +1 -0
- package/lib/components/item-icon.d.ts +32 -0
- package/lib/components/item-icon.d.ts.map +1 -0
- package/lib/components/item-icon.jsx +54 -0
- package/lib/components/item-icon.jsx.map +1 -0
- package/lib/components/label.d.ts +13 -0
- package/lib/components/label.d.ts.map +1 -0
- package/lib/components/label.jsx +9 -0
- package/lib/components/label.jsx.map +1 -0
- package/lib/components/legal-dialog.d.ts +18 -0
- package/lib/components/legal-dialog.d.ts.map +1 -0
- package/lib/components/legal-dialog.jsx +31 -0
- package/lib/components/legal-dialog.jsx.map +1 -0
- package/lib/components/license-gate.d.ts +17 -0
- package/lib/components/license-gate.d.ts.map +1 -0
- package/lib/components/license-gate.jsx +21 -0
- package/lib/components/license-gate.jsx.map +1 -0
- package/lib/components/logo.d.ts +15 -0
- package/lib/components/logo.d.ts.map +1 -0
- package/lib/components/logo.jsx +20 -0
- package/lib/components/logo.jsx.map +1 -0
- package/lib/components/onboarding-wizard.d.ts +89 -0
- package/lib/components/onboarding-wizard.d.ts.map +1 -0
- package/lib/components/onboarding-wizard.jsx +185 -0
- package/lib/components/onboarding-wizard.jsx.map +1 -0
- package/lib/components/pagination.d.ts +43 -0
- package/lib/components/pagination.d.ts.map +1 -0
- package/lib/components/pagination.jsx +72 -0
- package/lib/components/pagination.jsx.map +1 -0
- package/lib/components/password-generator.d.ts +15 -0
- package/lib/components/password-generator.d.ts.map +1 -0
- package/lib/components/password-generator.jsx +273 -0
- package/lib/components/password-generator.jsx.map +1 -0
- package/lib/components/pin-pad.d.ts +20 -0
- package/lib/components/pin-pad.d.ts.map +1 -0
- package/lib/components/pin-pad.jsx +93 -0
- package/lib/components/pin-pad.jsx.map +1 -0
- package/lib/components/popover.d.ts +27 -0
- package/lib/components/popover.d.ts.map +1 -0
- package/lib/components/popover.jsx +18 -0
- package/lib/components/popover.jsx.map +1 -0
- package/lib/components/profile/ProfileDialog.d.ts +25 -0
- package/lib/components/profile/ProfileDialog.d.ts.map +1 -0
- package/lib/components/profile/ProfileDialog.jsx +71 -0
- package/lib/components/profile/ProfileDialog.jsx.map +1 -0
- package/lib/components/profile/ProfileTab.d.ts +12 -0
- package/lib/components/profile/ProfileTab.d.ts.map +1 -0
- package/lib/components/profile/ProfileTab.jsx +112 -0
- package/lib/components/profile/ProfileTab.jsx.map +1 -0
- package/lib/components/profile/SecurityTab.d.ts +32 -0
- package/lib/components/profile/SecurityTab.d.ts.map +1 -0
- package/lib/components/profile/SecurityTab.jsx +145 -0
- package/lib/components/profile/SecurityTab.jsx.map +1 -0
- package/lib/components/profile/index.d.ts +4 -0
- package/lib/components/profile/index.d.ts.map +1 -0
- package/lib/components/profile/index.js +4 -0
- package/lib/components/profile/index.js.map +1 -0
- package/lib/components/segmented-toggle.d.ts +28 -0
- package/lib/components/segmented-toggle.d.ts.map +1 -0
- package/lib/components/segmented-toggle.jsx +64 -0
- package/lib/components/segmented-toggle.jsx.map +1 -0
- package/lib/components/separator.d.ts +13 -0
- package/lib/components/separator.d.ts.map +1 -0
- package/lib/components/separator.jsx +16 -0
- package/lib/components/separator.jsx.map +1 -0
- package/lib/components/server-settings.d.ts +27 -0
- package/lib/components/server-settings.d.ts.map +1 -0
- package/lib/components/server-settings.jsx +307 -0
- package/lib/components/server-settings.jsx.map +1 -0
- package/lib/components/settings/AccountSection.d.ts +26 -0
- package/lib/components/settings/AccountSection.d.ts.map +1 -0
- package/lib/components/settings/AccountSection.jsx +32 -0
- package/lib/components/settings/AccountSection.jsx.map +1 -0
- package/lib/components/settings/DangerZoneSection.d.ts +13 -0
- package/lib/components/settings/DangerZoneSection.d.ts.map +1 -0
- package/lib/components/settings/DangerZoneSection.jsx +16 -0
- package/lib/components/settings/DangerZoneSection.jsx.map +1 -0
- package/lib/components/settings/GeneralSection.d.ts +11 -0
- package/lib/components/settings/GeneralSection.d.ts.map +1 -0
- package/lib/components/settings/GeneralSection.jsx +12 -0
- package/lib/components/settings/GeneralSection.jsx.map +1 -0
- package/lib/components/settings/SupportSection.d.ts +11 -0
- package/lib/components/settings/SupportSection.d.ts.map +1 -0
- package/lib/components/settings/SupportSection.jsx +22 -0
- package/lib/components/settings/SupportSection.jsx.map +1 -0
- package/lib/components/settings/index.d.ts +5 -0
- package/lib/components/settings/index.d.ts.map +1 -0
- package/lib/components/settings/index.js +5 -0
- package/lib/components/settings/index.js.map +1 -0
- package/lib/components/settings-row.d.ts +28 -0
- package/lib/components/settings-row.d.ts.map +1 -0
- package/lib/components/settings-row.jsx +68 -0
- package/lib/components/settings-row.jsx.map +1 -0
- package/lib/components/skeleton.d.ts +8 -0
- package/lib/components/skeleton.d.ts.map +1 -0
- package/lib/components/skeleton.jsx +24 -0
- package/lib/components/skeleton.jsx.map +1 -0
- package/lib/components/social-connections.d.ts +15 -0
- package/lib/components/social-connections.d.ts.map +1 -0
- package/lib/components/social-connections.jsx +31 -0
- package/lib/components/social-connections.jsx.map +1 -0
- package/lib/components/stat-card.d.ts +29 -0
- package/lib/components/stat-card.d.ts.map +1 -0
- package/lib/components/stat-card.jsx +36 -0
- package/lib/components/stat-card.jsx.map +1 -0
- package/lib/components/tag-chip.d.ts +18 -0
- package/lib/components/tag-chip.d.ts.map +1 -0
- package/lib/components/tag-chip.jsx +54 -0
- package/lib/components/tag-chip.jsx.map +1 -0
- package/lib/components/tag-multi-select.d.ts +27 -0
- package/lib/components/tag-multi-select.d.ts.map +1 -0
- package/lib/components/tag-multi-select.jsx +65 -0
- package/lib/components/tag-multi-select.jsx.map +1 -0
- package/lib/components/text.d.ts +22 -0
- package/lib/components/text.d.ts.map +1 -0
- package/lib/components/text.jsx +43 -0
- package/lib/components/text.jsx.map +1 -0
- package/lib/components/toast/ToastPortal.d.ts +11 -0
- package/lib/components/toast/ToastPortal.d.ts.map +1 -0
- package/lib/components/toast/ToastPortal.jsx +15 -0
- package/lib/components/toast/ToastPortal.jsx.map +1 -0
- package/lib/components/toast/ToastPortal.web.d.ts +14 -0
- package/lib/components/toast/ToastPortal.web.d.ts.map +1 -0
- package/lib/components/toast/ToastPortal.web.jsx +39 -0
- package/lib/components/toast/ToastPortal.web.jsx.map +1 -0
- package/lib/components/toast/index.d.ts +3 -0
- package/lib/components/toast/index.d.ts.map +1 -0
- package/lib/components/toast/index.js +3 -0
- package/lib/components/toast/index.js.map +1 -0
- package/lib/components/toast/toast-config.d.ts +14 -0
- package/lib/components/toast/toast-config.d.ts.map +1 -0
- package/lib/components/toast/toast-config.jsx +24 -0
- package/lib/components/toast/toast-config.jsx.map +1 -0
- package/lib/components/tooltip.d.ts +30 -0
- package/lib/components/tooltip.d.ts.map +1 -0
- package/lib/components/tooltip.jsx +14 -0
- package/lib/components/tooltip.jsx.map +1 -0
- package/lib/components/totp-display.d.ts +23 -0
- package/lib/components/totp-display.d.ts.map +1 -0
- package/lib/components/totp-display.jsx +132 -0
- package/lib/components/totp-display.jsx.map +1 -0
- package/lib/components/user-avatar.d.ts +18 -0
- package/lib/components/user-avatar.d.ts.map +1 -0
- package/lib/components/user-avatar.jsx +25 -0
- package/lib/components/user-avatar.jsx.map +1 -0
- package/lib/context/AppConfigContext.d.ts +56 -0
- package/lib/context/AppConfigContext.d.ts.map +1 -0
- package/lib/context/AppConfigContext.jsx +14 -0
- package/lib/context/AppConfigContext.jsx.map +1 -0
- package/lib/context/NavigationContext.d.ts +42 -0
- package/lib/context/NavigationContext.d.ts.map +1 -0
- package/lib/context/NavigationContext.jsx +53 -0
- package/lib/context/NavigationContext.jsx.map +1 -0
- package/lib/context/NotificationContext.d.ts +32 -0
- package/lib/context/NotificationContext.d.ts.map +1 -0
- package/lib/context/NotificationContext.jsx +49 -0
- package/lib/context/NotificationContext.jsx.map +1 -0
- package/lib/context/ThemeContext.d.ts +12 -0
- package/lib/context/ThemeContext.d.ts.map +1 -0
- package/lib/context/ThemeContext.jsx +66 -0
- package/lib/context/ThemeContext.jsx.map +1 -0
- package/lib/context/index.d.ts +5 -0
- package/lib/context/index.d.ts.map +1 -0
- package/lib/context/index.js +5 -0
- package/lib/context/index.js.map +1 -0
- package/lib/flags.d.ts +9 -0
- package/lib/flags.d.ts.map +1 -0
- package/lib/flags.js +37 -0
- package/lib/flags.js.map +1 -0
- package/lib/forms/form-error.d.ts +12 -0
- package/lib/forms/form-error.d.ts.map +1 -0
- package/lib/forms/form-error.jsx +14 -0
- package/lib/forms/form-error.jsx.map +1 -0
- package/lib/forms/index.d.ts +3 -0
- package/lib/forms/index.d.ts.map +1 -0
- package/lib/forms/index.js +3 -0
- package/lib/forms/index.js.map +1 -0
- package/lib/forms/shake-field.d.ts +18 -0
- package/lib/forms/shake-field.d.ts.map +1 -0
- package/lib/forms/shake-field.jsx +30 -0
- package/lib/forms/shake-field.jsx.map +1 -0
- package/lib/hooks/index.d.ts +6 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +6 -0
- package/lib/hooks/index.js.map +1 -0
- package/lib/hooks/useAuth.d.ts +109 -0
- package/lib/hooks/useAuth.d.ts.map +1 -0
- package/lib/hooks/useAuth.jsx +404 -0
- package/lib/hooks/useAuth.jsx.map +1 -0
- package/lib/hooks/useAutoLock.d.ts +22 -0
- package/lib/hooks/useAutoLock.d.ts.map +1 -0
- package/lib/hooks/useAutoLock.js +85 -0
- package/lib/hooks/useAutoLock.js.map +1 -0
- package/lib/hooks/useDebouncedValue.d.ts +19 -0
- package/lib/hooks/useDebouncedValue.d.ts.map +1 -0
- package/lib/hooks/useDebouncedValue.js +29 -0
- package/lib/hooks/useDebouncedValue.js.map +1 -0
- package/lib/hooks/useShakeAnimation.d.ts +17 -0
- package/lib/hooks/useShakeAnimation.d.ts.map +1 -0
- package/lib/hooks/useShakeAnimation.js +22 -0
- package/lib/hooks/useShakeAnimation.js.map +1 -0
- package/lib/hooks/useUploadBlobUrl.d.ts +13 -0
- package/lib/hooks/useUploadBlobUrl.d.ts.map +1 -0
- package/lib/hooks/useUploadBlobUrl.js +60 -0
- package/lib/hooks/useUploadBlobUrl.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +47 -0
- package/lib/index.js.map +1 -0
- package/lib/screens/auth/AccountPickerScreen.d.ts +24 -0
- package/lib/screens/auth/AccountPickerScreen.d.ts.map +1 -0
- package/lib/screens/auth/AccountPickerScreen.jsx +47 -0
- package/lib/screens/auth/AccountPickerScreen.jsx.map +1 -0
- package/lib/screens/auth/AuthScreen.d.ts +59 -0
- package/lib/screens/auth/AuthScreen.d.ts.map +1 -0
- package/lib/screens/auth/AuthScreen.jsx +594 -0
- package/lib/screens/auth/AuthScreen.jsx.map +1 -0
- package/lib/screens/auth/ConsentScreen.d.ts +27 -0
- package/lib/screens/auth/ConsentScreen.d.ts.map +1 -0
- package/lib/screens/auth/ConsentScreen.jsx +55 -0
- package/lib/screens/auth/ConsentScreen.jsx.map +1 -0
- package/lib/screens/auth/ErrorScreen.d.ts +20 -0
- package/lib/screens/auth/ErrorScreen.d.ts.map +1 -0
- package/lib/screens/auth/ErrorScreen.jsx +43 -0
- package/lib/screens/auth/ErrorScreen.jsx.map +1 -0
- package/lib/screens/auth/ExpiredLinkScreen.d.ts +19 -0
- package/lib/screens/auth/ExpiredLinkScreen.d.ts.map +1 -0
- package/lib/screens/auth/ExpiredLinkScreen.jsx +38 -0
- package/lib/screens/auth/ExpiredLinkScreen.jsx.map +1 -0
- package/lib/screens/auth/ForgotPasswordScreen.d.ts +4 -0
- package/lib/screens/auth/ForgotPasswordScreen.d.ts.map +1 -0
- package/lib/screens/auth/ForgotPasswordScreen.jsx +123 -0
- package/lib/screens/auth/ForgotPasswordScreen.jsx.map +1 -0
- package/lib/screens/auth/ImportDataScreen.d.ts +17 -0
- package/lib/screens/auth/ImportDataScreen.d.ts.map +1 -0
- package/lib/screens/auth/ImportDataScreen.jsx +73 -0
- package/lib/screens/auth/ImportDataScreen.jsx.map +1 -0
- package/lib/screens/auth/LoginScreen.d.ts +38 -0
- package/lib/screens/auth/LoginScreen.d.ts.map +1 -0
- package/lib/screens/auth/LoginScreen.jsx +326 -0
- package/lib/screens/auth/LoginScreen.jsx.map +1 -0
- package/lib/screens/auth/MfaChallengeScreen.d.ts +4 -0
- package/lib/screens/auth/MfaChallengeScreen.d.ts.map +1 -0
- package/lib/screens/auth/MfaChallengeScreen.jsx +139 -0
- package/lib/screens/auth/MfaChallengeScreen.jsx.map +1 -0
- package/lib/screens/auth/MfaEnrollScreen.d.ts +12 -0
- package/lib/screens/auth/MfaEnrollScreen.d.ts.map +1 -0
- package/lib/screens/auth/MfaEnrollScreen.jsx +267 -0
- package/lib/screens/auth/MfaEnrollScreen.jsx.map +1 -0
- package/lib/screens/auth/PasskeyEnrollScreen.d.ts +18 -0
- package/lib/screens/auth/PasskeyEnrollScreen.d.ts.map +1 -0
- package/lib/screens/auth/PasskeyEnrollScreen.jsx +51 -0
- package/lib/screens/auth/PasskeyEnrollScreen.jsx.map +1 -0
- package/lib/screens/auth/PrivacyPolicyScreen.d.ts +19 -0
- package/lib/screens/auth/PrivacyPolicyScreen.d.ts.map +1 -0
- package/lib/screens/auth/PrivacyPolicyScreen.jsx +143 -0
- package/lib/screens/auth/PrivacyPolicyScreen.jsx.map +1 -0
- package/lib/screens/auth/RegisterScreen.d.ts +14 -0
- package/lib/screens/auth/RegisterScreen.d.ts.map +1 -0
- package/lib/screens/auth/RegisterScreen.jsx +296 -0
- package/lib/screens/auth/RegisterScreen.jsx.map +1 -0
- package/lib/screens/auth/ResetPasswordScreen.d.ts +12 -0
- package/lib/screens/auth/ResetPasswordScreen.d.ts.map +1 -0
- package/lib/screens/auth/ResetPasswordScreen.jsx +276 -0
- package/lib/screens/auth/ResetPasswordScreen.jsx.map +1 -0
- package/lib/screens/auth/ServerConfigScreen.d.ts +18 -0
- package/lib/screens/auth/ServerConfigScreen.d.ts.map +1 -0
- package/lib/screens/auth/ServerConfigScreen.jsx +259 -0
- package/lib/screens/auth/ServerConfigScreen.jsx.map +1 -0
- package/lib/screens/auth/TermsOfServiceScreen.d.ts +30 -0
- package/lib/screens/auth/TermsOfServiceScreen.d.ts.map +1 -0
- package/lib/screens/auth/TermsOfServiceScreen.jsx +130 -0
- package/lib/screens/auth/TermsOfServiceScreen.jsx.map +1 -0
- package/lib/screens/auth/VerifyEmailScreen.d.ts +4 -0
- package/lib/screens/auth/VerifyEmailScreen.d.ts.map +1 -0
- package/lib/screens/auth/VerifyEmailScreen.jsx +249 -0
- package/lib/screens/auth/VerifyEmailScreen.jsx.map +1 -0
- package/lib/screens/auth/WelcomeScreen.d.ts +17 -0
- package/lib/screens/auth/WelcomeScreen.d.ts.map +1 -0
- package/lib/screens/auth/WelcomeScreen.jsx +58 -0
- package/lib/screens/auth/WelcomeScreen.jsx.map +1 -0
- package/lib/screens/auth/index.d.ts +19 -0
- package/lib/screens/auth/index.d.ts.map +1 -0
- package/lib/screens/auth/index.js +19 -0
- package/lib/screens/auth/index.js.map +1 -0
- package/lib/screens/index.d.ts +2 -0
- package/lib/screens/index.d.ts.map +1 -0
- package/lib/screens/index.js +2 -0
- package/lib/screens/index.js.map +1 -0
- package/lib/services/api-config.d.ts +87 -0
- package/lib/services/api-config.d.ts.map +1 -0
- package/lib/services/api-config.js +202 -0
- package/lib/services/api-config.js.map +1 -0
- package/lib/services/auth.service.d.ts +140 -0
- package/lib/services/auth.service.d.ts.map +1 -0
- package/lib/services/auth.service.js +376 -0
- package/lib/services/auth.service.js.map +1 -0
- package/lib/services/favicon.d.ts +46 -0
- package/lib/services/favicon.d.ts.map +1 -0
- package/lib/services/favicon.js +84 -0
- package/lib/services/favicon.js.map +1 -0
- package/lib/services/hibp-cache.d.ts +23 -0
- package/lib/services/hibp-cache.d.ts.map +1 -0
- package/lib/services/hibp-cache.js +56 -0
- package/lib/services/hibp-cache.js.map +1 -0
- package/lib/services/hibp.d.ts +28 -0
- package/lib/services/hibp.d.ts.map +1 -0
- package/lib/services/hibp.js +92 -0
- package/lib/services/hibp.js.map +1 -0
- package/lib/services/index.d.ts +8 -0
- package/lib/services/index.d.ts.map +1 -0
- package/lib/services/index.js +7 -0
- package/lib/services/index.js.map +1 -0
- package/lib/services/passkey.d.ts +10 -0
- package/lib/services/passkey.d.ts.map +1 -0
- package/lib/services/passkey.js +192 -0
- package/lib/services/passkey.js.map +1 -0
- package/lib/utils/api-fetch.d.ts +11 -0
- package/lib/utils/api-fetch.d.ts.map +1 -0
- package/lib/utils/api-fetch.js +52 -0
- package/lib/utils/api-fetch.js.map +1 -0
- package/lib/utils/clipboard.d.ts +31 -0
- package/lib/utils/clipboard.d.ts.map +1 -0
- package/lib/utils/clipboard.js +66 -0
- package/lib/utils/clipboard.js.map +1 -0
- package/lib/utils/cn.d.ts +9 -0
- package/lib/utils/cn.d.ts.map +1 -0
- package/lib/utils/cn.js +12 -0
- package/lib/utils/cn.js.map +1 -0
- package/lib/utils/debounce.d.ts +13 -0
- package/lib/utils/debounce.d.ts.map +1 -0
- package/lib/utils/debounce.js +28 -0
- package/lib/utils/debounce.js.map +1 -0
- package/lib/utils/error.d.ts +9 -0
- package/lib/utils/error.d.ts.map +1 -0
- package/lib/utils/error.js +11 -0
- package/lib/utils/error.js.map +1 -0
- package/lib/utils/fetch.d.ts +17 -0
- package/lib/utils/fetch.d.ts.map +1 -0
- package/lib/utils/fetch.js +29 -0
- package/lib/utils/fetch.js.map +1 -0
- package/lib/utils/http-error.d.ts +41 -0
- package/lib/utils/http-error.d.ts.map +1 -0
- package/lib/utils/http-error.js +63 -0
- package/lib/utils/http-error.js.map +1 -0
- package/lib/utils/index.d.ts +11 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +11 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/palette.d.ts +46 -0
- package/lib/utils/palette.d.ts.map +1 -0
- package/lib/utils/palette.js +75 -0
- package/lib/utils/palette.js.map +1 -0
- package/lib/utils/password.d.ts +20 -0
- package/lib/utils/password.d.ts.map +1 -0
- package/lib/utils/password.js +51 -0
- package/lib/utils/password.js.map +1 -0
- package/lib/utils/settings-storage.d.ts +3 -0
- package/lib/utils/settings-storage.d.ts.map +1 -0
- package/lib/utils/settings-storage.js +62 -0
- package/lib/utils/settings-storage.js.map +1 -0
- package/lib/utils/validation.d.ts +29 -0
- package/lib/utils/validation.d.ts.map +1 -0
- package/lib/utils/validation.js +58 -0
- package/lib/utils/validation.js.map +1 -0
- package/package.json +204 -0
- package/src/components/__tests__/data-table.test.ts +94 -0
- package/src/components/__tests__/pagination.test.ts +73 -0
- package/src/components/__tests__/stat-card.test.ts +21 -0
- package/src/components/auth/LockOverlay.tsx +122 -0
- package/src/components/auth/PasswordStrengthMeter.tsx +96 -0
- package/src/components/auth/ProtectedRoute.tsx +91 -0
- package/src/components/auth/PwnedPasswordWarning.tsx +54 -0
- package/src/components/auth/index.ts +10 -0
- package/src/components/bottom-nav.tsx +99 -0
- package/src/components/button.tsx +144 -0
- package/src/components/card.tsx +82 -0
- package/src/components/checkbox.tsx +48 -0
- package/src/components/connection-settings.tsx +583 -0
- package/src/components/context-menu.tsx +95 -0
- package/src/components/data-table.tsx +223 -0
- package/src/components/dialog.tsx +376 -0
- package/src/components/dropdown-menu.tsx +120 -0
- package/src/components/entropy-boot-splash.tsx +114 -0
- package/src/components/entropy-mark-animated.tsx +260 -0
- package/src/components/entropy-mark-geometry.ts +11 -0
- package/src/components/entropy-mark-sequential.tsx +132 -0
- package/src/components/entropy-mark.tsx +135 -0
- package/src/components/error-boundary.tsx +70 -0
- package/src/components/index.ts +156 -0
- package/src/components/input.tsx +246 -0
- package/src/components/item-icon.tsx +111 -0
- package/src/components/label.tsx +25 -0
- package/src/components/legal-dialog.tsx +47 -0
- package/src/components/license-gate.tsx +28 -0
- package/src/components/logo.tsx +34 -0
- package/src/components/onboarding-wizard.tsx +353 -0
- package/src/components/pagination.tsx +154 -0
- package/src/components/password-generator.tsx +393 -0
- package/src/components/pin-pad.tsx +156 -0
- package/src/components/popover.tsx +38 -0
- package/src/components/profile/ProfileDialog.tsx +129 -0
- package/src/components/profile/ProfileTab.tsx +151 -0
- package/src/components/profile/SecurityTab.tsx +229 -0
- package/src/components/profile/index.ts +3 -0
- package/src/components/segmented-toggle.tsx +119 -0
- package/src/components/separator.tsx +41 -0
- package/src/components/server-settings.tsx +442 -0
- package/src/components/settings/AccountSection.tsx +76 -0
- package/src/components/settings/DangerZoneSection.tsx +37 -0
- package/src/components/settings/GeneralSection.tsx +39 -0
- package/src/components/settings/SupportSection.tsx +53 -0
- package/src/components/settings/index.ts +4 -0
- package/src/components/settings-row.tsx +126 -0
- package/src/components/skeleton.tsx +48 -0
- package/src/components/social-connections.tsx +55 -0
- package/src/components/stat-card.tsx +64 -0
- package/src/components/tag-chip.tsx +78 -0
- package/src/components/tag-multi-select.tsx +136 -0
- package/src/components/text.tsx +77 -0
- package/src/components/toast/ToastPortal.tsx +16 -0
- package/src/components/toast/ToastPortal.web.tsx +47 -0
- package/src/components/toast/index.ts +2 -0
- package/src/components/toast/toast-config.tsx +31 -0
- package/src/components/tooltip.tsx +30 -0
- package/src/components/totp-display.tsx +207 -0
- package/src/components/user-avatar.tsx +46 -0
- package/src/context/AppConfigContext.tsx +76 -0
- package/src/context/NavigationContext.tsx +75 -0
- package/src/context/NotificationContext.tsx +69 -0
- package/src/context/ThemeContext.tsx +83 -0
- package/src/context/__tests__/NavigationContext.test.tsx +53 -0
- package/src/context/index.ts +24 -0
- package/src/flags.ts +44 -0
- package/src/forms/form-error.tsx +20 -0
- package/src/forms/index.ts +2 -0
- package/src/forms/shake-field.tsx +52 -0
- package/src/hooks/__tests__/useUploadBlobUrl.test.tsx +77 -0
- package/src/hooks/index.ts +18 -0
- package/src/hooks/useAuth.tsx +567 -0
- package/src/hooks/useAutoLock.ts +110 -0
- package/src/hooks/useDebouncedValue.ts +32 -0
- package/src/hooks/useShakeAnimation.ts +42 -0
- package/src/hooks/useUploadBlobUrl.ts +75 -0
- package/src/index.ts +48 -0
- package/src/screens/auth/AccountPickerScreen.tsx +103 -0
- package/src/screens/auth/AuthScreen.tsx +941 -0
- package/src/screens/auth/ConsentScreen.tsx +118 -0
- package/src/screens/auth/ErrorScreen.tsx +96 -0
- package/src/screens/auth/ExpiredLinkScreen.tsx +87 -0
- package/src/screens/auth/ForgotPasswordScreen.tsx +182 -0
- package/src/screens/auth/ImportDataScreen.tsx +112 -0
- package/src/screens/auth/LoginScreen.tsx +484 -0
- package/src/screens/auth/MfaChallengeScreen.tsx +216 -0
- package/src/screens/auth/MfaEnrollScreen.tsx +388 -0
- package/src/screens/auth/PasskeyEnrollScreen.tsx +96 -0
- package/src/screens/auth/PrivacyPolicyScreen.tsx +249 -0
- package/src/screens/auth/RegisterScreen.tsx +430 -0
- package/src/screens/auth/ResetPasswordScreen.tsx +377 -0
- package/src/screens/auth/ServerConfigScreen.tsx +388 -0
- package/src/screens/auth/TermsOfServiceScreen.tsx +236 -0
- package/src/screens/auth/VerifyEmailScreen.tsx +310 -0
- package/src/screens/auth/WelcomeScreen.tsx +78 -0
- package/src/screens/auth/index.ts +36 -0
- package/src/screens/index.ts +1 -0
- package/src/services/__tests__/api-config.test.ts +67 -0
- package/src/services/__tests__/favicon.test.ts +88 -0
- package/src/services/__tests__/hibp-cache.test.ts +37 -0
- package/src/services/__tests__/hibp.test.ts +39 -0
- package/src/services/api-config.ts +260 -0
- package/src/services/auth.service.ts +498 -0
- package/src/services/favicon.ts +102 -0
- package/src/services/hibp-cache.ts +71 -0
- package/src/services/hibp.ts +120 -0
- package/src/services/index.ts +52 -0
- package/src/services/passkey.ts +213 -0
- package/src/types/external.d.ts +24 -0
- package/src/utils/__tests__/clipboard.test.ts +59 -0
- package/src/utils/__tests__/cn.test.ts +24 -0
- package/src/utils/__tests__/debounce.test.ts +42 -0
- package/src/utils/__tests__/error.test.ts +18 -0
- package/src/utils/__tests__/http-error.test.ts +33 -0
- package/src/utils/__tests__/palette.test.ts +22 -0
- package/src/utils/__tests__/password.test.ts +55 -0
- package/src/utils/__tests__/settings-storage.test.ts +15 -0
- package/src/utils/__tests__/validation.test.ts +52 -0
- package/src/utils/api-fetch.ts +61 -0
- package/src/utils/clipboard.ts +69 -0
- package/src/utils/cn.ts +12 -0
- package/src/utils/debounce.ts +33 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/fetch.ts +33 -0
- package/src/utils/http-error.ts +81 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/palette.ts +105 -0
- package/src/utils/password.ts +64 -0
- package/src/utils/settings-storage.ts +68 -0
- package/src/utils/validation.ts +71 -0
- package/styles/globals.css +262 -0
- package/tailwind-preset.js +163 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { validateEmail, validatePassword, validateUsername } from "../validation";
|
|
2
|
+
|
|
3
|
+
describe("validateEmail", () => {
|
|
4
|
+
it.each(["a@b.co", "user.name+tag@example.com", "x@host.dev"])("accepts %s", (e) => {
|
|
5
|
+
expect(validateEmail(e)).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it.each(["", "no-at", "a@b", "a@b.c", "a@@b.co", " a@b.co", "a@b.co "])("rejects %s", (e) => {
|
|
9
|
+
expect(validateEmail(e)).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("validatePassword", () => {
|
|
14
|
+
it("accepts a strong password", () => {
|
|
15
|
+
const r = validatePassword("Aa1!2345");
|
|
16
|
+
expect(r.valid).toBe(true);
|
|
17
|
+
expect(r.errors).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("collects every rule violation in a single pass", () => {
|
|
21
|
+
const r = validatePassword("short");
|
|
22
|
+
expect(r.valid).toBe(false);
|
|
23
|
+
// length, upper, digit, symbol = 4 misses; lowercase is present.
|
|
24
|
+
expect(r.errors.length).toBe(4);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("flags missing symbol only", () => {
|
|
28
|
+
const r = validatePassword("Aaaaaaa1");
|
|
29
|
+
expect(r.valid).toBe(false);
|
|
30
|
+
expect(r.errors).toHaveLength(1);
|
|
31
|
+
expect(r.errors[0]).toMatch(/special/i);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("validateUsername", () => {
|
|
36
|
+
it("accepts 3..30 ascii alnum/underscore", () => {
|
|
37
|
+
expect(validateUsername("abc")).toEqual({ valid: true });
|
|
38
|
+
expect(validateUsername("a_1")).toEqual({ valid: true });
|
|
39
|
+
expect(validateUsername("a".repeat(30))).toEqual({ valid: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects out-of-range length", () => {
|
|
43
|
+
expect(validateUsername("ab").valid).toBe(false);
|
|
44
|
+
expect(validateUsername("a".repeat(31)).valid).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects non-ascii / special chars", () => {
|
|
48
|
+
expect(validateUsername("ab-c").valid).toBe(false);
|
|
49
|
+
expect(validateUsername("ab c").valid).toBe(false);
|
|
50
|
+
expect(validateUsername("ab.c").valid).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import { fetchWithTimeout } from "./fetch";
|
|
3
|
+
import { getBaseUrl, getSessionToken } from "../services/api-config";
|
|
4
|
+
|
|
5
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
6
|
+
|
|
7
|
+
const CSRF_COOKIE_NAME = "csrf_token";
|
|
8
|
+
const CSRF_HEADER_NAME = "X-CSRF-Token";
|
|
9
|
+
|
|
10
|
+
function readCookie(name: string): string | null {
|
|
11
|
+
if (typeof document === "undefined") {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const prefix = `${name}=`;
|
|
15
|
+
for (const part of document.cookie.split(";")) {
|
|
16
|
+
const trimmed = part.trim();
|
|
17
|
+
if (trimmed.startsWith(prefix)) {
|
|
18
|
+
return decodeURIComponent(trimmed.slice(prefix.length));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cross-platform authed `fetch` for the entropy-ui auth flows.
|
|
26
|
+
*
|
|
27
|
+
* **Web**: HttpOnly session cookie (auto-attached, used in same-origin /
|
|
28
|
+
* production) + `X-CSRF-Token` on writes. Also attaches a `Bearer` token when
|
|
29
|
+
* one is stored (sessionStorage) — needed for split-origin dev where the Lax
|
|
30
|
+
* cookie can't ride cross-origin XHR.
|
|
31
|
+
* **Native**: Bearer token loaded from SecureStore.
|
|
32
|
+
*/
|
|
33
|
+
export async function apiFetch(
|
|
34
|
+
path: string,
|
|
35
|
+
init: RequestInit = {},
|
|
36
|
+
timeoutMs?: number
|
|
37
|
+
): Promise<Response> {
|
|
38
|
+
const url = `${getBaseUrl()}${path}`;
|
|
39
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
40
|
+
const headers = new Headers(init.headers);
|
|
41
|
+
|
|
42
|
+
if (Platform.OS === "web") {
|
|
43
|
+
if (!SAFE_METHODS.has(method)) {
|
|
44
|
+
const csrf = readCookie(CSRF_COOKIE_NAME);
|
|
45
|
+
if (csrf) {
|
|
46
|
+
headers.set(CSRF_HEADER_NAME, csrf);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const webToken = await getSessionToken();
|
|
50
|
+
if (webToken && !headers.has("Authorization")) {
|
|
51
|
+
headers.set("Authorization", `Bearer ${webToken}`);
|
|
52
|
+
}
|
|
53
|
+
return fetchWithTimeout(url, { ...init, headers, credentials: "include" }, timeoutMs);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const token = await getSessionToken();
|
|
57
|
+
if (token) {
|
|
58
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
59
|
+
}
|
|
60
|
+
return fetchWithTimeout(url, { ...init, headers }, timeoutMs);
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised copy-to-clipboard with a single, module-scope auto-clear
|
|
3
|
+
* timer.
|
|
4
|
+
*
|
|
5
|
+
* Why module scope: tying the timer to a React component (via `useRef`)
|
|
6
|
+
* means it gets cancelled when the screen unmounts — which happens
|
|
7
|
+
* routinely (auto-lock on tab-switch, navigation to another item). The
|
|
8
|
+
* user's just-completed copy then either gets wiped immediately
|
|
9
|
+
* (component cleanup runs `setStringAsync("")`) or sits indefinitely.
|
|
10
|
+
*
|
|
11
|
+
* Behaviour:
|
|
12
|
+
* * `copyToClipboard(value)` writes the value, returns `true` on
|
|
13
|
+
* success.
|
|
14
|
+
* * On success, schedules a 30-second auto-wipe.
|
|
15
|
+
* * Subsequent copies cancel the prior timer and reschedule, so the
|
|
16
|
+
* wipe is always 30 seconds from the *last* copy regardless of
|
|
17
|
+
* which screen issued it.
|
|
18
|
+
*
|
|
19
|
+
* Limitation: we don't read the clipboard back (would require the
|
|
20
|
+
* clipboard-read permission), so if the user copies from another app
|
|
21
|
+
* between our copy and the auto-clear firing, we'll still wipe their
|
|
22
|
+
* value at the 30s mark. Acceptable trade-off for a password manager.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import * as Clipboard from "expo-clipboard";
|
|
26
|
+
|
|
27
|
+
const AUTO_CLEAR_MS = 30_000;
|
|
28
|
+
|
|
29
|
+
let clearTimer: ReturnType<typeof setTimeout> | null = null;
|
|
30
|
+
|
|
31
|
+
/** Write `value` to the clipboard. Returns `true` if the write succeeded. */
|
|
32
|
+
export async function copyToClipboard(value: string): Promise<boolean> {
|
|
33
|
+
let ok: boolean;
|
|
34
|
+
try {
|
|
35
|
+
ok = await Clipboard.setStringAsync(value);
|
|
36
|
+
} catch {
|
|
37
|
+
ok = false;
|
|
38
|
+
}
|
|
39
|
+
if (!ok) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (clearTimer !== null) {
|
|
44
|
+
clearTimeout(clearTimer);
|
|
45
|
+
}
|
|
46
|
+
clearTimer = setTimeout(() => {
|
|
47
|
+
clearTimer = null;
|
|
48
|
+
void Clipboard.setStringAsync("").catch(() => {
|
|
49
|
+
// Best-effort.
|
|
50
|
+
});
|
|
51
|
+
}, AUTO_CLEAR_MS);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Wipe the clipboard now (e.g. on explicit logout). Cancels the pending
|
|
57
|
+
* auto-clear so we don't fire a redundant wipe later.
|
|
58
|
+
*/
|
|
59
|
+
export async function clearClipboardNow(): Promise<void> {
|
|
60
|
+
if (clearTimer !== null) {
|
|
61
|
+
clearTimeout(clearTimer);
|
|
62
|
+
clearTimer = null;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await Clipboard.setStringAsync("");
|
|
66
|
+
} catch {
|
|
67
|
+
// Best-effort.
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/utils/cn.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Combines class names using clsx and merges Tailwind CSS classes intelligently.
|
|
6
|
+
*
|
|
7
|
+
* @param inputs - Class values to combine
|
|
8
|
+
* @returns Merged class string
|
|
9
|
+
*/
|
|
10
|
+
export function cn(...inputs: ClassValue[]) {
|
|
11
|
+
return twMerge(clsx(inputs));
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce a function. The returned function has a `.cancel()` method
|
|
3
|
+
* so a pending invocation can be cleared in a `useEffect` cleanup to
|
|
4
|
+
* avoid a setState-after-unmount on the trailing call.
|
|
5
|
+
*
|
|
6
|
+
* Intentionally a tiny, no-dependency implementation — `lodash.debounce`
|
|
7
|
+
* pulls 200kB of lodash with it; this is 15 lines, type-safe, and tree-
|
|
8
|
+
* shake-friendly.
|
|
9
|
+
*/
|
|
10
|
+
export function debounce<T extends (...args: Parameters<T>) => void>(
|
|
11
|
+
func: T,
|
|
12
|
+
wait: number
|
|
13
|
+
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
|
|
14
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
15
|
+
|
|
16
|
+
const debounced = (...args: Parameters<T>) => {
|
|
17
|
+
if (timeout) {
|
|
18
|
+
clearTimeout(timeout);
|
|
19
|
+
}
|
|
20
|
+
timeout = setTimeout(() => {
|
|
21
|
+
func(...args);
|
|
22
|
+
}, wait);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
debounced.cancel = () => {
|
|
26
|
+
if (timeout) {
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
timeout = null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return debounced;
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a human-readable message from an unknown error value.
|
|
3
|
+
*
|
|
4
|
+
* Use this in every `catch (e: unknown)` block instead of accessing
|
|
5
|
+
* `e?.message` directly — TypeScript's `unknown` type prevents property
|
|
6
|
+
* access without a type guard.
|
|
7
|
+
*/
|
|
8
|
+
export function getErrorMessage(e: unknown, fallback = "An unexpected error occurred"): string {
|
|
9
|
+
return e instanceof Error ? e.message : fallback;
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fetch` with a timeout abort signal merged onto the caller's own signal.
|
|
3
|
+
* The default timeout is 15s. Pass `Infinity` to disable the timeout
|
|
4
|
+
* (the caller's own `signal` still applies). Passing `0` aborts
|
|
5
|
+
* immediately — almost never what you want — so we treat any
|
|
6
|
+
* non-positive value as "no timeout" to avoid the footgun.
|
|
7
|
+
*
|
|
8
|
+
* **Lockstep duplicate** of `vault/clients/shared/src/utils/fetch.ts`.
|
|
9
|
+
* `@vault/shared` is the vault-specific lower layer; `@entropy-softworks/ui`
|
|
10
|
+
* is the cross-portfolio UI package. Neither can depend on the other
|
|
11
|
+
* (shared → ui would invert layering; ui → shared would chain every
|
|
12
|
+
* downstream Entropy Softworks app to vault). The function is small,
|
|
13
|
+
* pure, and stable; keep both copies byte-identical and update both
|
|
14
|
+
* when changing.
|
|
15
|
+
*/
|
|
16
|
+
export function fetchWithTimeout(
|
|
17
|
+
url: string,
|
|
18
|
+
options: RequestInit = {},
|
|
19
|
+
timeoutMs = 15000
|
|
20
|
+
): Promise<Response> {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const id =
|
|
23
|
+
timeoutMs > 0 && Number.isFinite(timeoutMs)
|
|
24
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
25
|
+
: null;
|
|
26
|
+
const signals = [controller.signal, options.signal].filter((s): s is AbortSignal => s != null);
|
|
27
|
+
const combinedSignal = signals.length > 1 ? AbortSignal.any(signals) : controller.signal;
|
|
28
|
+
return fetch(url, { ...options, signal: combinedSignal }).finally(() => {
|
|
29
|
+
if (id !== null) {
|
|
30
|
+
clearTimeout(id);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse an Entropy Softworks server error response into a single user-
|
|
3
|
+
* facing string. Falls back to `fallback` when the body isn't valid
|
|
4
|
+
* JSON or doesn't follow the expected `{ error: { message, details? } }`
|
|
5
|
+
* shape. Detail arrays longer than `MAX_DETAILS` are truncated so a
|
|
6
|
+
* runaway server response can't produce an unreadable toast.
|
|
7
|
+
*/
|
|
8
|
+
export interface ServerErrorBody {
|
|
9
|
+
error: {
|
|
10
|
+
message: string;
|
|
11
|
+
details?: string[];
|
|
12
|
+
code?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MAX_DETAILS = 5;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error subclass that carries the server's machine-readable
|
|
20
|
+
* `error.code` and HTTP `status` alongside the human-readable
|
|
21
|
+
* message. UI code can branch on `code` (e.g. `INVALID_CREDENTIALS`,
|
|
22
|
+
* `ACCOUNT_LOCKED`, `EMAIL_NOT_VERIFIED`) instead of substring-
|
|
23
|
+
* matching against an English message that breaks under i18n.
|
|
24
|
+
*/
|
|
25
|
+
export class ApiError extends Error {
|
|
26
|
+
readonly code: string | undefined;
|
|
27
|
+
readonly status: number;
|
|
28
|
+
readonly details: readonly string[] | undefined;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
message: string,
|
|
32
|
+
options: { code?: string; status: number; details?: readonly string[] }
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "ApiError";
|
|
36
|
+
this.code = options.code;
|
|
37
|
+
this.status = options.status;
|
|
38
|
+
this.details = options.details;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function extractErrorMessage(response: Response, fallback: string): Promise<string> {
|
|
43
|
+
try {
|
|
44
|
+
const error: ServerErrorBody = await response.json();
|
|
45
|
+
const details = error.error.details;
|
|
46
|
+
if (details && details.length > 0) {
|
|
47
|
+
const shown = details.slice(0, MAX_DETAILS).join(", ");
|
|
48
|
+
return details.length > MAX_DETAILS ? `${shown}, …` : shown;
|
|
49
|
+
}
|
|
50
|
+
return error.error.message || fallback;
|
|
51
|
+
} catch {
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Like `extractErrorMessage` but returns a structured `ApiError` so
|
|
58
|
+
* callers can branch on `code` / `status`. Use this when the caller
|
|
59
|
+
* is throwing and downstream consumers need machine-readable
|
|
60
|
+
* classification; use `extractErrorMessage` when all you need is a
|
|
61
|
+
* toast string.
|
|
62
|
+
*/
|
|
63
|
+
export async function extractApiError(response: Response, fallback: string): Promise<ApiError> {
|
|
64
|
+
let message = fallback;
|
|
65
|
+
let code: string | undefined;
|
|
66
|
+
let details: string[] | undefined;
|
|
67
|
+
try {
|
|
68
|
+
const error: ServerErrorBody = await response.json();
|
|
69
|
+
if (error.error.details && error.error.details.length > 0) {
|
|
70
|
+
details = error.error.details.slice(0, MAX_DETAILS);
|
|
71
|
+
const shown = details.join(", ");
|
|
72
|
+
message = error.error.details.length > MAX_DETAILS ? `${shown}, …` : shown;
|
|
73
|
+
} else {
|
|
74
|
+
message = error.error.message || fallback;
|
|
75
|
+
}
|
|
76
|
+
code = error.error.code;
|
|
77
|
+
} catch {
|
|
78
|
+
// Non-JSON body — keep fallback message, no code.
|
|
79
|
+
}
|
|
80
|
+
return new ApiError(message, { code, status: response.status, details });
|
|
81
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { apiFetch } from "./api-fetch";
|
|
2
|
+
export { cn } from "./cn";
|
|
3
|
+
export { getErrorMessage } from "./error";
|
|
4
|
+
export {
|
|
5
|
+
darkPalette,
|
|
6
|
+
getPalette,
|
|
7
|
+
lightPalette,
|
|
8
|
+
usePalette,
|
|
9
|
+
type ColorScheme,
|
|
10
|
+
type Palette,
|
|
11
|
+
} from "./palette";
|
|
12
|
+
export { clearClipboardNow, copyToClipboard } from "./clipboard";
|
|
13
|
+
export { loadSetting, saveSetting } from "./settings-storage";
|
|
14
|
+
export { debounce } from "./debounce";
|
|
15
|
+
export { ApiError, extractApiError, extractErrorMessage } from "./http-error";
|
|
16
|
+
export { generatePassword, type GeneratePasswordOptions } from "./password";
|
|
17
|
+
export {
|
|
18
|
+
validateEmail,
|
|
19
|
+
validatePassword,
|
|
20
|
+
validateUsername,
|
|
21
|
+
type PasswordValidation,
|
|
22
|
+
type UsernameValidation,
|
|
23
|
+
} from "./validation";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript-side representation of the same color tokens NativeWind
|
|
3
|
+
* exposes as CSS variables in `styles/globals.css`. Components that need
|
|
4
|
+
* a palette object (RN-Web inline styles, SVG `stroke`, etc.) consume
|
|
5
|
+
* this so the values stay in lockstep with the className-based theming.
|
|
6
|
+
*
|
|
7
|
+
* Always read via `usePalette()` (or `getPalette(scheme)` for non-hook
|
|
8
|
+
* call sites) — the table mirrors `:root` for light and `.dark` for
|
|
9
|
+
* dark.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useMemo } from "react";
|
|
13
|
+
import { useColorScheme } from "nativewind";
|
|
14
|
+
|
|
15
|
+
export interface Palette {
|
|
16
|
+
background: string;
|
|
17
|
+
foreground: string;
|
|
18
|
+
card: string;
|
|
19
|
+
cardForeground: string;
|
|
20
|
+
popover: string;
|
|
21
|
+
popoverForeground: string;
|
|
22
|
+
primary: string;
|
|
23
|
+
primaryForeground: string;
|
|
24
|
+
secondary: string;
|
|
25
|
+
secondaryForeground: string;
|
|
26
|
+
muted: string;
|
|
27
|
+
mutedForeground: string;
|
|
28
|
+
accent: string;
|
|
29
|
+
accentForeground: string;
|
|
30
|
+
destructive: string;
|
|
31
|
+
destructiveForeground: string;
|
|
32
|
+
border: string;
|
|
33
|
+
input: string;
|
|
34
|
+
inputBorder: string;
|
|
35
|
+
ring: string;
|
|
36
|
+
placeholder: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const lightPalette: Palette = {
|
|
40
|
+
background: "rgb(255, 255, 255)",
|
|
41
|
+
foreground: "rgb(0, 0, 0)",
|
|
42
|
+
card: "rgb(255, 255, 255)",
|
|
43
|
+
cardForeground: "rgb(0, 0, 0)",
|
|
44
|
+
popover: "rgb(255, 255, 255)",
|
|
45
|
+
popoverForeground: "rgb(0, 0, 0)",
|
|
46
|
+
primary: "rgb(0, 0, 0)",
|
|
47
|
+
primaryForeground: "rgb(255, 255, 255)",
|
|
48
|
+
secondary: "rgb(245, 245, 245)",
|
|
49
|
+
secondaryForeground: "rgb(0, 0, 0)",
|
|
50
|
+
muted: "rgb(245, 245, 245)",
|
|
51
|
+
mutedForeground: "rgb(115, 115, 115)",
|
|
52
|
+
accent: "rgb(245, 245, 245)",
|
|
53
|
+
accentForeground: "rgb(0, 0, 0)",
|
|
54
|
+
destructive: "rgb(239, 68, 68)",
|
|
55
|
+
destructiveForeground: "rgb(255, 255, 255)",
|
|
56
|
+
border: "rgb(229, 229, 229)",
|
|
57
|
+
input: "rgb(255, 255, 255)",
|
|
58
|
+
inputBorder: "rgb(229, 229, 229)",
|
|
59
|
+
ring: "rgb(0, 0, 0)",
|
|
60
|
+
placeholder: "rgb(163, 163, 163)",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const darkPalette: Palette = {
|
|
64
|
+
background: "rgb(0, 0, 0)",
|
|
65
|
+
foreground: "rgb(255, 255, 255)",
|
|
66
|
+
card: "rgb(0, 0, 0)",
|
|
67
|
+
cardForeground: "rgb(255, 255, 255)",
|
|
68
|
+
popover: "rgb(0, 0, 0)",
|
|
69
|
+
popoverForeground: "rgb(255, 255, 255)",
|
|
70
|
+
primary: "rgb(255, 255, 255)",
|
|
71
|
+
primaryForeground: "rgb(0, 0, 0)",
|
|
72
|
+
secondary: "rgb(38, 38, 38)",
|
|
73
|
+
secondaryForeground: "rgb(255, 255, 255)",
|
|
74
|
+
muted: "rgb(38, 38, 38)",
|
|
75
|
+
mutedForeground: "rgb(163, 163, 163)",
|
|
76
|
+
accent: "rgb(38, 38, 38)",
|
|
77
|
+
accentForeground: "rgb(255, 255, 255)",
|
|
78
|
+
destructive: "rgb(127, 29, 29)",
|
|
79
|
+
destructiveForeground: "rgb(255, 255, 255)",
|
|
80
|
+
border: "rgb(38, 38, 38)",
|
|
81
|
+
input: "rgb(0, 0, 0)",
|
|
82
|
+
inputBorder: "rgb(38, 38, 38)",
|
|
83
|
+
ring: "rgb(255, 255, 255)",
|
|
84
|
+
placeholder: "rgb(115, 115, 115)",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type ColorScheme = "light" | "dark";
|
|
88
|
+
|
|
89
|
+
export function getPalette(scheme: ColorScheme): Palette {
|
|
90
|
+
return scheme === "dark" ? darkPalette : lightPalette;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns the active palette + scheme. Re-renders the caller when the
|
|
95
|
+
* OS / class-toggled theme flips.
|
|
96
|
+
*/
|
|
97
|
+
export function usePalette(): { scheme: ColorScheme; palette: Palette } {
|
|
98
|
+
const { colorScheme } = useColorScheme();
|
|
99
|
+
const scheme: ColorScheme = colorScheme === "dark" ? "dark" : "light";
|
|
100
|
+
// Memoize the wrapper so callers using the result as a useMemo /
|
|
101
|
+
// useCallback dep see a stable reference until the scheme actually
|
|
102
|
+
// changes. The contained `palette` is already a module-level cached
|
|
103
|
+
// singleton; only the wrapper object was being re-created.
|
|
104
|
+
return useMemo(() => ({ scheme, palette: getPalette(scheme) }), [scheme]);
|
|
105
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { randomBytes } from "@noble/ciphers/utils.js";
|
|
2
|
+
|
|
3
|
+
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
4
|
+
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
|
5
|
+
const NUMBERS = "0123456789";
|
|
6
|
+
const SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
7
|
+
const SIMILAR = "il1Lo0O";
|
|
8
|
+
|
|
9
|
+
export interface GeneratePasswordOptions {
|
|
10
|
+
/** Output length in characters. */
|
|
11
|
+
length: number;
|
|
12
|
+
includeUppercase?: boolean;
|
|
13
|
+
includeLowercase?: boolean;
|
|
14
|
+
includeNumbers?: boolean;
|
|
15
|
+
includeSymbols?: boolean;
|
|
16
|
+
/** Drop ambiguous glyphs (`i l 1 L o 0 O`). */
|
|
17
|
+
excludeSimilar?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a random password using CSPRNG bytes (`@noble/ciphers`) with
|
|
22
|
+
* rejection sampling so the output distribution stays uniform across
|
|
23
|
+
* the chosen character set — `b % len` modulo-bias is rejected by
|
|
24
|
+
* trimming the byte range to a multiple of `len`.
|
|
25
|
+
*
|
|
26
|
+
* Throws when every character class is disabled (no charset).
|
|
27
|
+
*/
|
|
28
|
+
export function generatePassword(opts: GeneratePasswordOptions): string {
|
|
29
|
+
let charset = "";
|
|
30
|
+
if (opts.includeUppercase ?? true) {
|
|
31
|
+
charset += UPPERCASE;
|
|
32
|
+
}
|
|
33
|
+
if (opts.includeLowercase ?? true) {
|
|
34
|
+
charset += LOWERCASE;
|
|
35
|
+
}
|
|
36
|
+
if (opts.includeNumbers ?? true) {
|
|
37
|
+
charset += NUMBERS;
|
|
38
|
+
}
|
|
39
|
+
if (opts.includeSymbols ?? true) {
|
|
40
|
+
charset += SYMBOLS;
|
|
41
|
+
}
|
|
42
|
+
if (opts.excludeSimilar) {
|
|
43
|
+
charset = charset
|
|
44
|
+
.split("")
|
|
45
|
+
.filter((c) => !SIMILAR.includes(c))
|
|
46
|
+
.join("");
|
|
47
|
+
}
|
|
48
|
+
if (charset.length === 0) {
|
|
49
|
+
throw new Error("generatePassword: at least one character class must be enabled");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const limit = Math.floor(256 / charset.length) * charset.length;
|
|
53
|
+
let out = "";
|
|
54
|
+
while (out.length < opts.length) {
|
|
55
|
+
const bytes = randomBytes(opts.length - out.length + 16);
|
|
56
|
+
for (let i = 0; i < bytes.length && out.length < opts.length; i++) {
|
|
57
|
+
const b = bytes[i] ?? 0;
|
|
58
|
+
if (b < limit) {
|
|
59
|
+
out += charset.charAt(b % charset.length);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import * as SecureStore from "expo-secure-store";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Boolean preference storage shared across platforms.
|
|
6
|
+
*
|
|
7
|
+
* Web persists via `localStorage` so behaviour matches the native
|
|
8
|
+
* SecureStore path (also persistent). The web tier is origin-isolated
|
|
9
|
+
* but not encrypted at rest — don't use this for secrets, only for UX
|
|
10
|
+
* preferences whose disclosure is acceptable. Security-sensitive policy
|
|
11
|
+
* (e.g. enforced auto-lock) belongs server-side.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const WEB_STORAGE_AVAILABLE =
|
|
15
|
+
Platform.OS === "web" && typeof window !== "undefined" && !!window.localStorage;
|
|
16
|
+
|
|
17
|
+
let warnedNoStorage = false;
|
|
18
|
+
function warnNoStorage(action: "load" | "save"): void {
|
|
19
|
+
if (warnedNoStorage) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
warnedNoStorage = true;
|
|
23
|
+
if (!__DEV__) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.warn(
|
|
28
|
+
`[settings-storage] ${action} failed: localStorage unavailable; using fallbacks for this session.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function loadSetting(key: string, fallback: boolean): Promise<boolean> {
|
|
33
|
+
try {
|
|
34
|
+
if (Platform.OS === "web") {
|
|
35
|
+
if (!WEB_STORAGE_AVAILABLE) {
|
|
36
|
+
warnNoStorage("load");
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
const v = window.localStorage.getItem(key);
|
|
40
|
+
return v !== null ? v === "true" : fallback;
|
|
41
|
+
}
|
|
42
|
+
const v = await SecureStore.getItemAsync(key);
|
|
43
|
+
return v !== null ? v === "true" : fallback;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (__DEV__) {
|
|
46
|
+
console.warn(`[settings-storage] load(${key}) threw:`, e);
|
|
47
|
+
}
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function saveSetting(key: string, value: boolean): Promise<void> {
|
|
53
|
+
try {
|
|
54
|
+
if (Platform.OS === "web") {
|
|
55
|
+
if (!WEB_STORAGE_AVAILABLE) {
|
|
56
|
+
warnNoStorage("save");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
window.localStorage.setItem(key, String(value));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await SecureStore.setItemAsync(key, String(value));
|
|
63
|
+
} catch (e) {
|
|
64
|
+
if (__DEV__) {
|
|
65
|
+
console.warn(`[settings-storage] save(${key}) threw:`, e);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validators used by the UI to surface preview feedback before
|
|
3
|
+
* the user hits the server. The Entropy Softworks application servers
|
|
4
|
+
* enforce these same rules authoritatively (see `entropy-auth`'s
|
|
5
|
+
* `password::policy` / `Validation`); these implementations are kept
|
|
6
|
+
* deliberately strict to avoid false positives on the client.
|
|
7
|
+
*
|
|
8
|
+
* Email: a permissive `local@host.tld` shape — anything stricter risks
|
|
9
|
+
* rejecting valid addresses, and the server validates anyway.
|
|
10
|
+
*
|
|
11
|
+
* Password: matches the default `entropy-auth` policy (≥8 chars, mixed
|
|
12
|
+
* case, digit, symbol). Apps that opt their server into a different
|
|
13
|
+
* policy can ignore the `valid` result and let the server be the source
|
|
14
|
+
* of truth.
|
|
15
|
+
*
|
|
16
|
+
* Username: 3–30 chars, ASCII alnum + underscore.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface PasswordValidation {
|
|
20
|
+
valid: boolean;
|
|
21
|
+
errors: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UsernameValidation {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
30
|
+
const UPPER = /[A-Z]/;
|
|
31
|
+
const LOWER = /[a-z]/;
|
|
32
|
+
const DIGIT = /[0-9]/;
|
|
33
|
+
const SYMBOL = /[!@#$%^&*()_+\-=[\]{}|;:,.<>?]/;
|
|
34
|
+
const USERNAME = /^[a-zA-Z0-9_]+$/;
|
|
35
|
+
|
|
36
|
+
export function validateEmail(email: string): boolean {
|
|
37
|
+
return EMAIL.test(email);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validatePassword(password: string): PasswordValidation {
|
|
41
|
+
const errors: string[] = [];
|
|
42
|
+
if (password.length < 8) {
|
|
43
|
+
errors.push("Password must be at least 8 characters");
|
|
44
|
+
}
|
|
45
|
+
if (!UPPER.test(password)) {
|
|
46
|
+
errors.push("Password must contain at least one uppercase letter");
|
|
47
|
+
}
|
|
48
|
+
if (!LOWER.test(password)) {
|
|
49
|
+
errors.push("Password must contain at least one lowercase letter");
|
|
50
|
+
}
|
|
51
|
+
if (!DIGIT.test(password)) {
|
|
52
|
+
errors.push("Password must contain at least one number");
|
|
53
|
+
}
|
|
54
|
+
if (!SYMBOL.test(password)) {
|
|
55
|
+
errors.push("Password must contain at least one special character");
|
|
56
|
+
}
|
|
57
|
+
return { valid: errors.length === 0, errors };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateUsername(username: string): UsernameValidation {
|
|
61
|
+
if (username.length < 3 || username.length > 30) {
|
|
62
|
+
return { valid: false, error: "Username must be between 3 and 30 characters" };
|
|
63
|
+
}
|
|
64
|
+
if (!USERNAME.test(username)) {
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
error: "Username can only contain letters, numbers, and underscores",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { valid: true };
|
|
71
|
+
}
|