@payez/next-mvp 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +782 -0
- package/dist/api/auth-handler.d.ts +67 -0
- package/dist/api/auth-handler.js +397 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +19 -0
- package/dist/api-handlers/account/change-password.d.ts +9 -0
- package/dist/api-handlers/account/change-password.js +112 -0
- package/dist/api-handlers/account/masked-info.d.ts +2 -0
- package/dist/api-handlers/account/masked-info.js +41 -0
- package/dist/api-handlers/account/profile.d.ts +3 -0
- package/dist/api-handlers/account/profile.js +63 -0
- package/dist/api-handlers/account/recovery/initiate.d.ts +2 -0
- package/dist/api-handlers/account/recovery/initiate.js +26 -0
- package/dist/api-handlers/account/recovery/send-code.d.ts +2 -0
- package/dist/api-handlers/account/recovery/send-code.js +28 -0
- package/dist/api-handlers/account/recovery/verify-code.d.ts +2 -0
- package/dist/api-handlers/account/recovery/verify-code.js +28 -0
- package/dist/api-handlers/account/reset-password.d.ts +2 -0
- package/dist/api-handlers/account/reset-password.js +26 -0
- package/dist/api-handlers/account/send-code.d.ts +24 -0
- package/dist/api-handlers/account/send-code.js +60 -0
- package/dist/api-handlers/account/update-phone.d.ts +27 -0
- package/dist/api-handlers/account/update-phone.js +64 -0
- package/dist/api-handlers/account/validate-password.d.ts +17 -0
- package/dist/api-handlers/account/validate-password.js +81 -0
- package/dist/api-handlers/account/verify-email.d.ts +26 -0
- package/dist/api-handlers/account/verify-email.js +106 -0
- package/dist/api-handlers/account/verify-sms.d.ts +26 -0
- package/dist/api-handlers/account/verify-sms.js +106 -0
- package/dist/api-handlers/admin/analytics.d.ts +20 -0
- package/dist/api-handlers/admin/analytics.js +379 -0
- package/dist/api-handlers/admin/audit.d.ts +20 -0
- package/dist/api-handlers/admin/audit.js +214 -0
- package/dist/api-handlers/admin/index.d.ts +21 -0
- package/dist/api-handlers/admin/index.js +41 -0
- package/dist/api-handlers/admin/redis-sessions.d.ts +36 -0
- package/dist/api-handlers/admin/redis-sessions.js +204 -0
- package/dist/api-handlers/admin/sessions.d.ts +21 -0
- package/dist/api-handlers/admin/sessions.js +284 -0
- package/dist/api-handlers/admin/site-logs.d.ts +46 -0
- package/dist/api-handlers/admin/site-logs.js +318 -0
- package/dist/api-handlers/admin/users.d.ts +20 -0
- package/dist/api-handlers/admin/users.js +222 -0
- package/dist/api-handlers/admin/vibe-data.d.ts +80 -0
- package/dist/api-handlers/admin/vibe-data.js +268 -0
- package/dist/api-handlers/anon/preferences.d.ts +37 -0
- package/dist/api-handlers/anon/preferences.js +96 -0
- package/dist/api-handlers/auth/jwks.d.ts +2 -0
- package/dist/api-handlers/auth/jwks.js +24 -0
- package/dist/api-handlers/auth/login.d.ts +42 -0
- package/dist/api-handlers/auth/login.js +178 -0
- package/dist/api-handlers/auth/refresh.d.ts +74 -0
- package/dist/api-handlers/auth/refresh.js +635 -0
- package/dist/api-handlers/auth/signout.d.ts +37 -0
- package/dist/api-handlers/auth/signout.js +187 -0
- package/dist/api-handlers/auth/status.d.ts +8 -0
- package/dist/api-handlers/auth/status.js +26 -0
- package/dist/api-handlers/auth/update-session.d.ts +37 -0
- package/dist/api-handlers/auth/update-session.js +95 -0
- package/dist/api-handlers/auth/validate.d.ts +6 -0
- package/dist/api-handlers/auth/validate.js +43 -0
- package/dist/api-handlers/auth/verify-code.d.ts +43 -0
- package/dist/api-handlers/auth/verify-code.js +94 -0
- package/dist/api-handlers/session/refresh-viability.d.ts +14 -0
- package/dist/api-handlers/session/refresh-viability.js +39 -0
- package/dist/api-handlers/session/viability.d.ts +13 -0
- package/dist/api-handlers/session/viability.js +146 -0
- package/dist/api-handlers/test/force-expire.d.ts +23 -0
- package/dist/api-handlers/test/force-expire.js +65 -0
- package/dist/auth/auth-decision.d.ts +39 -0
- package/dist/auth/auth-decision.js +182 -0
- package/dist/auth/auth-options.d.ts +57 -0
- package/dist/auth/auth-options.js +213 -0
- package/dist/auth/callbacks/index.d.ts +6 -0
- package/dist/auth/callbacks/index.js +12 -0
- package/dist/auth/callbacks/jwt.d.ts +45 -0
- package/dist/auth/callbacks/jwt.js +305 -0
- package/dist/auth/callbacks/session.d.ts +60 -0
- package/dist/auth/callbacks/session.js +170 -0
- package/dist/auth/callbacks/signin.d.ts +23 -0
- package/dist/auth/callbacks/signin.js +44 -0
- package/dist/auth/events/index.d.ts +4 -0
- package/dist/auth/events/index.js +8 -0
- package/dist/auth/events/signout.d.ts +17 -0
- package/dist/auth/events/signout.js +32 -0
- package/dist/auth/providers/credentials.d.ts +32 -0
- package/dist/auth/providers/credentials.js +223 -0
- package/dist/auth/providers/index.d.ts +5 -0
- package/dist/auth/providers/index.js +21 -0
- package/dist/auth/providers/oauth.d.ts +26 -0
- package/dist/auth/providers/oauth.js +105 -0
- package/dist/auth/route-config.d.ts +66 -0
- package/dist/auth/route-config.js +190 -0
- package/dist/auth/types/auth-types.d.ts +417 -0
- package/dist/auth/types/auth-types.js +53 -0
- package/dist/auth/types/index.d.ts +6 -0
- package/dist/auth/types/index.js +22 -0
- package/dist/auth/unauthenticated-routes.d.ts +1 -0
- package/dist/auth/unauthenticated-routes.js +19 -0
- package/dist/auth/utils/idp-client.d.ts +94 -0
- package/dist/auth/utils/idp-client.js +383 -0
- package/dist/auth/utils/index.d.ts +5 -0
- package/dist/auth/utils/index.js +21 -0
- package/dist/auth/utils/token-utils.d.ts +84 -0
- package/dist/auth/utils/token-utils.js +219 -0
- package/dist/client/AuthContext.d.ts +19 -0
- package/dist/client/AuthContext.js +112 -0
- package/dist/client/fetch-with-auth.d.ts +11 -0
- package/dist/client/fetch-with-auth.js +44 -0
- package/dist/client/fetchWithSession.d.ts +3 -0
- package/dist/client/fetchWithSession.js +24 -0
- package/dist/client/index.d.ts +9 -0
- package/dist/client/index.js +20 -0
- package/dist/client/useAnonSession.d.ts +36 -0
- package/dist/client/useAnonSession.js +99 -0
- package/dist/components/SessionSync.d.ts +13 -0
- package/dist/components/SessionSync.js +119 -0
- package/dist/components/SignalRHealthCheck.d.ts +10 -0
- package/dist/components/SignalRHealthCheck.js +97 -0
- package/dist/components/account/UserAvatarMenu.d.ts +20 -0
- package/dist/components/account/UserAvatarMenu.js +80 -0
- package/dist/components/account/index.d.ts +7 -0
- package/dist/components/account/index.js +10 -0
- package/dist/components/admin/AlertSettingsTab.d.ts +48 -0
- package/dist/components/admin/AlertSettingsTab.js +351 -0
- package/dist/components/admin/AnalyticsTab.d.ts +22 -0
- package/dist/components/admin/AnalyticsTab.js +167 -0
- package/dist/components/admin/DataBrowserTab.d.ts +19 -0
- package/dist/components/admin/DataBrowserTab.js +252 -0
- package/dist/components/admin/LoggingSettingsTab.d.ts +73 -0
- package/dist/components/admin/LoggingSettingsTab.js +339 -0
- package/dist/components/admin/SessionsTab.d.ts +37 -0
- package/dist/components/admin/SessionsTab.js +165 -0
- package/dist/components/admin/StatsTab.d.ts +53 -0
- package/dist/components/admin/StatsTab.js +161 -0
- package/dist/components/admin/VibeAdminContext.d.ts +32 -0
- package/dist/components/admin/VibeAdminContext.js +38 -0
- package/dist/components/admin/VibeAdminLayout.d.ts +11 -0
- package/dist/components/admin/VibeAdminLayout.js +69 -0
- package/dist/components/admin/index.d.ts +29 -0
- package/dist/components/admin/index.js +44 -0
- package/dist/components/auth/FederatedAuthSection.d.ts +8 -0
- package/dist/components/auth/FederatedAuthSection.js +45 -0
- package/dist/components/auth/ModeAwareLoginPage.d.ts +10 -0
- package/dist/components/auth/ModeAwareLoginPage.js +42 -0
- package/dist/components/auth/ModeAwareSignupPage.d.ts +9 -0
- package/dist/components/auth/ModeAwareSignupPage.js +78 -0
- package/dist/components/auth/TraditionalAuthSection.d.ts +14 -0
- package/dist/components/auth/TraditionalAuthSection.js +20 -0
- package/dist/components/recovery/CompleteStep.d.ts +5 -0
- package/dist/components/recovery/CompleteStep.js +8 -0
- package/dist/components/recovery/InitiateRecoveryStep.d.ts +8 -0
- package/dist/components/recovery/InitiateRecoveryStep.js +20 -0
- package/dist/components/recovery/SelectMethodStep.d.ts +8 -0
- package/dist/components/recovery/SelectMethodStep.js +8 -0
- package/dist/components/recovery/SetPasswordStep.d.ts +6 -0
- package/dist/components/recovery/SetPasswordStep.js +20 -0
- package/dist/components/recovery/VerifyCodeStep.d.ts +10 -0
- package/dist/components/recovery/VerifyCodeStep.js +24 -0
- package/dist/components/reserved/ReservedRecoveryWarning.d.ts +38 -0
- package/dist/components/reserved/ReservedRecoveryWarning.js +92 -0
- package/dist/components/reserved/ReservedStatusBox.d.ts +30 -0
- package/dist/components/reserved/ReservedStatusBox.js +71 -0
- package/dist/components/ui/BetaBadge.d.ts +29 -0
- package/dist/components/ui/BetaBadge.js +38 -0
- package/dist/components/ui/Footer.d.ts +37 -0
- package/dist/components/ui/Footer.js +41 -0
- package/dist/config/env.d.ts +66 -0
- package/dist/config/env.js +57 -0
- package/dist/config/logger.d.ts +57 -0
- package/dist/config/logger.js +73 -0
- package/dist/config/logging-config.d.ts +30 -0
- package/dist/config/logging-config.js +122 -0
- package/dist/config/unauthenticated-routes.d.ts +17 -0
- package/dist/config/unauthenticated-routes.js +24 -0
- package/dist/config/vibe-log-transport.d.ts +79 -0
- package/dist/config/vibe-log-transport.js +203 -0
- package/dist/edge/internal-api-url.d.ts +53 -0
- package/dist/edge/internal-api-url.js +63 -0
- package/dist/edge/middleware.d.ts +14 -0
- package/dist/edge/middleware.js +32 -0
- package/dist/hooks/useAuth.d.ts +23 -0
- package/dist/hooks/useAuth.js +81 -0
- package/dist/hooks/useAuthSettings.d.ts +59 -0
- package/dist/hooks/useAuthSettings.js +93 -0
- package/dist/hooks/useAvailableProviders.d.ts +45 -0
- package/dist/hooks/useAvailableProviders.js +108 -0
- package/dist/hooks/usePasswordValidation.d.ts +27 -0
- package/dist/hooks/usePasswordValidation.js +102 -0
- package/dist/hooks/useProfile.d.ts +15 -0
- package/dist/hooks/useProfile.js +59 -0
- package/dist/hooks/usePublicAuthSettings.d.ts +56 -0
- package/dist/hooks/usePublicAuthSettings.js +131 -0
- package/dist/hooks/useSessionExpiration.d.ts +57 -0
- package/dist/hooks/useSessionExpiration.js +72 -0
- package/dist/hooks/useViabilitySession.d.ts +75 -0
- package/dist/hooks/useViabilitySession.js +268 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +54 -0
- package/dist/lib/anon-session.d.ts +74 -0
- package/dist/lib/anon-session.js +169 -0
- package/dist/lib/api-handler.d.ts +123 -0
- package/dist/lib/api-handler.js +478 -0
- package/dist/lib/app-slug.d.ts +95 -0
- package/dist/lib/app-slug.js +172 -0
- package/dist/lib/demo-mode.d.ts +6 -0
- package/dist/lib/demo-mode.js +16 -0
- package/dist/lib/geolocation.d.ts +64 -0
- package/dist/lib/geolocation.js +235 -0
- package/dist/lib/idp-client-config.d.ts +75 -0
- package/dist/lib/idp-client-config.js +351 -0
- package/dist/lib/idp-fetch.d.ts +14 -0
- package/dist/lib/idp-fetch.js +91 -0
- package/dist/lib/internal-api.d.ts +87 -0
- package/dist/lib/internal-api.js +122 -0
- package/dist/lib/jwt-decode-client.d.ts +10 -0
- package/dist/lib/jwt-decode-client.js +46 -0
- package/dist/lib/jwt-decode.d.ts +48 -0
- package/dist/lib/jwt-decode.js +57 -0
- package/dist/lib/nextauth-secret.d.ts +10 -0
- package/dist/lib/nextauth-secret.js +104 -0
- package/dist/lib/rate-limit-service.d.ts +23 -0
- package/dist/lib/rate-limit-service.js +6 -0
- package/dist/lib/redis.d.ts +5 -0
- package/dist/lib/redis.js +28 -0
- package/dist/lib/refresh-token-validator.d.ts +13 -0
- package/dist/lib/refresh-token-validator.js +117 -0
- package/dist/lib/roles.d.ts +145 -0
- package/dist/lib/roles.js +168 -0
- package/dist/lib/secret-validation.d.ts +4 -0
- package/dist/lib/secret-validation.js +14 -0
- package/dist/lib/session-store.d.ts +166 -0
- package/dist/lib/session-store.js +537 -0
- package/dist/lib/session.d.ts +21 -0
- package/dist/lib/session.js +26 -0
- package/dist/lib/site-logger.d.ts +214 -0
- package/dist/lib/site-logger.js +210 -0
- package/dist/lib/standardized-client-api.d.ts +161 -0
- package/dist/lib/standardized-client-api.js +786 -0
- package/dist/lib/startup-init.d.ts +40 -0
- package/dist/lib/startup-init.js +261 -0
- package/dist/lib/test-aware-get-token.d.ts +2 -0
- package/dist/lib/test-aware-get-token.js +81 -0
- package/dist/lib/token-expiry.d.ts +14 -0
- package/dist/lib/token-expiry.js +39 -0
- package/dist/lib/token-lifecycle.d.ts +52 -0
- package/dist/lib/token-lifecycle.js +398 -0
- package/dist/lib/types/api-responses.d.ts +128 -0
- package/dist/lib/types/api-responses.js +171 -0
- package/dist/lib/user-agent-parser.d.ts +50 -0
- package/dist/lib/user-agent-parser.js +220 -0
- package/dist/logging/api/admin-analytics.d.ts +3 -0
- package/dist/logging/api/admin-analytics.js +45 -0
- package/dist/logging/api/audit-log.d.ts +3 -0
- package/dist/logging/api/audit-log.js +52 -0
- package/dist/logging/components/AdminAnalyticsLayout.d.ts +10 -0
- package/dist/logging/components/AdminAnalyticsLayout.js +11 -0
- package/dist/logging/components/AuditLogViewer.d.ts +7 -0
- package/dist/logging/components/AuditLogViewer.js +51 -0
- package/dist/logging/components/ErrorMetricsCard.d.ts +7 -0
- package/dist/logging/components/ErrorMetricsCard.js +16 -0
- package/dist/logging/components/HealthMetricsCard.d.ts +7 -0
- package/dist/logging/components/HealthMetricsCard.js +19 -0
- package/dist/logging/hooks/useAdminAnalytics.d.ts +24 -0
- package/dist/logging/hooks/useAdminAnalytics.js +22 -0
- package/dist/logging/hooks/useAuditLog.d.ts +6 -0
- package/dist/logging/hooks/useAuditLog.js +25 -0
- package/dist/logging/hooks/useErrorMetrics.d.ts +6 -0
- package/dist/logging/hooks/useErrorMetrics.js +38 -0
- package/dist/logging/hooks/useHealthMetrics.d.ts +6 -0
- package/dist/logging/hooks/useHealthMetrics.js +41 -0
- package/dist/logging/index.d.ts +11 -0
- package/dist/logging/index.js +40 -0
- package/dist/logging/types/analytics.d.ts +68 -0
- package/dist/logging/types/analytics.js +3 -0
- package/dist/logging/types/audit.d.ts +29 -0
- package/dist/logging/types/audit.js +2 -0
- package/dist/logging/types/index.d.ts +2 -0
- package/dist/logging/types/index.js +19 -0
- package/dist/middleware/auth-decision.d.ts +33 -0
- package/dist/middleware/auth-decision.js +65 -0
- package/dist/middleware/create-middleware.d.ts +100 -0
- package/dist/middleware/create-middleware.js +445 -0
- package/dist/middleware/rbac-check.d.ts +44 -0
- package/dist/middleware/rbac-check.js +191 -0
- package/dist/middleware/twofa-presets.d.ts +134 -0
- package/dist/middleware/twofa-presets.js +175 -0
- package/dist/models/DecodedAccessToken.d.ts +17 -0
- package/dist/models/DecodedAccessToken.js +2 -0
- package/dist/models/SessionModel.d.ts +122 -0
- package/dist/models/SessionModel.js +136 -0
- package/dist/pages/admin-login/page.d.ts +31 -0
- package/dist/pages/admin-login/page.js +83 -0
- package/dist/pages/admin-roles/RolesAdminPage.d.ts +15 -0
- package/dist/pages/admin-roles/RolesAdminPage.js +78 -0
- package/dist/pages/admin-roles/index.d.ts +8 -0
- package/dist/pages/admin-roles/index.js +15 -0
- package/dist/pages/admin-roles/modals.d.ts +72 -0
- package/dist/pages/admin-roles/modals.js +154 -0
- package/dist/pages/client-admin/ClientSiteAdminPage.d.ts +79 -0
- package/dist/pages/client-admin/ClientSiteAdminPage.js +177 -0
- package/dist/pages/client-admin/index.d.ts +32 -0
- package/dist/pages/client-admin/index.js +37 -0
- package/dist/pages/login/page.d.ts +22 -0
- package/dist/pages/login/page.js +239 -0
- package/dist/pages/profile/EnhancedProfilePage.d.ts +13 -0
- package/dist/pages/profile/EnhancedProfilePage.js +150 -0
- package/dist/pages/profile/index.d.ts +8 -0
- package/dist/pages/profile/index.js +16 -0
- package/dist/pages/profile/page.d.ts +19 -0
- package/dist/pages/profile/page.js +47 -0
- package/dist/pages/profile/profile-patch.d.ts +1 -0
- package/dist/pages/profile/profile-patch.js +281 -0
- package/dist/pages/recovery/page.d.ts +1 -0
- package/dist/pages/recovery/page.js +142 -0
- package/dist/pages/roles/MyRolesPage.d.ts +24 -0
- package/dist/pages/roles/MyRolesPage.js +71 -0
- package/dist/pages/roles/components.d.ts +63 -0
- package/dist/pages/roles/components.js +108 -0
- package/dist/pages/roles/index.d.ts +8 -0
- package/dist/pages/roles/index.js +19 -0
- package/dist/pages/security/EnhancedSecurityPage.d.ts +14 -0
- package/dist/pages/security/EnhancedSecurityPage.js +248 -0
- package/dist/pages/security/index.d.ts +8 -0
- package/dist/pages/security/index.js +16 -0
- package/dist/pages/security/page.d.ts +21 -0
- package/dist/pages/security/page.js +212 -0
- package/dist/pages/security/security-patch.d.ts +1 -0
- package/dist/pages/security/security-patch.js +302 -0
- package/dist/pages/settings/EnhancedSettingsPage.d.ts +46 -0
- package/dist/pages/settings/EnhancedSettingsPage.js +231 -0
- package/dist/pages/settings/index.d.ts +8 -0
- package/dist/pages/settings/index.js +16 -0
- package/dist/pages/settings/page.d.ts +7 -0
- package/dist/pages/settings/page.js +26 -0
- package/dist/pages/showcase/ShowcasePage.d.ts +13 -0
- package/dist/pages/showcase/ShowcasePage.js +140 -0
- package/dist/pages/showcase/index.d.ts +12 -0
- package/dist/pages/showcase/index.js +17 -0
- package/dist/pages/test-env/EmergencyLogoutPage.d.ts +14 -0
- package/dist/pages/test-env/EmergencyLogoutPage.js +98 -0
- package/dist/pages/test-env/JwtInspectPage.d.ts +14 -0
- package/dist/pages/test-env/JwtInspectPage.js +114 -0
- package/dist/pages/test-env/RefreshTokenPage.d.ts +15 -0
- package/dist/pages/test-env/RefreshTokenPage.js +91 -0
- package/dist/pages/test-env/TestEnvPage.d.ts +13 -0
- package/dist/pages/test-env/TestEnvPage.js +49 -0
- package/dist/pages/test-env/index.d.ts +24 -0
- package/dist/pages/test-env/index.js +32 -0
- package/dist/pages/verify-code/page.d.ts +30 -0
- package/dist/pages/verify-code/page.js +408 -0
- package/dist/routes/account/index.d.ts +28 -0
- package/dist/routes/account/index.js +71 -0
- package/dist/routes/account/masked-info.d.ts +33 -0
- package/dist/routes/account/masked-info.js +39 -0
- package/dist/routes/account/send-code.d.ts +37 -0
- package/dist/routes/account/send-code.js +42 -0
- package/dist/routes/account/update-phone.d.ts +13 -0
- package/dist/routes/account/update-phone.js +17 -0
- package/dist/routes/account/verify-email.d.ts +38 -0
- package/dist/routes/account/verify-email.js +43 -0
- package/dist/routes/account/verify-sms.d.ts +38 -0
- package/dist/routes/account/verify-sms.js +43 -0
- package/dist/routes/auth/index.d.ts +19 -0
- package/dist/routes/auth/index.js +64 -0
- package/dist/routes/auth/logout.d.ts +31 -0
- package/dist/routes/auth/logout.js +113 -0
- package/dist/routes/auth/nextauth.d.ts +19 -0
- package/dist/routes/auth/nextauth.js +72 -0
- package/dist/routes/auth/refresh.d.ts +30 -0
- package/dist/routes/auth/refresh.js +51 -0
- package/dist/routes/auth/session.d.ts +72 -0
- package/dist/routes/auth/session.js +180 -0
- package/dist/routes/auth/settings.d.ts +25 -0
- package/dist/routes/auth/settings.js +55 -0
- package/dist/routes/auth/viability.d.ts +52 -0
- package/dist/routes/auth/viability.js +201 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.js +54 -0
- package/dist/routes/session/index.d.ts +6 -0
- package/dist/routes/session/index.js +10 -0
- package/dist/routes/session/refresh-viability.d.ts +16 -0
- package/dist/routes/session/refresh-viability.js +20 -0
- package/dist/services/signalrActivityService.d.ts +44 -0
- package/dist/services/signalrActivityService.js +257 -0
- package/dist/stores/authStore.d.ts +154 -0
- package/dist/stores/authStore.js +1531 -0
- package/dist/theme/ThemeProvider.d.ts +14 -0
- package/dist/theme/ThemeProvider.js +28 -0
- package/dist/theme/default.d.ts +8 -0
- package/dist/theme/default.js +33 -0
- package/dist/theme/index.d.ts +15 -0
- package/dist/theme/index.js +25 -0
- package/dist/theme/types.d.ts +56 -0
- package/dist/theme/types.js +8 -0
- package/dist/theme/useTheme.d.ts +60 -0
- package/dist/theme/useTheme.js +63 -0
- package/dist/theme/utils.d.ts +13 -0
- package/dist/theme/utils.js +39 -0
- package/dist/types/api.d.ts +134 -0
- package/dist/types/api.js +44 -0
- package/dist/types/auth.d.ts +19 -0
- package/dist/types/auth.js +2 -0
- package/dist/types/logging.d.ts +42 -0
- package/dist/types/logging.js +2 -0
- package/dist/types/recovery.d.ts +48 -0
- package/dist/types/recovery.js +2 -0
- package/dist/types/security.d.ts +1 -0
- package/dist/types/security.js +2 -0
- package/dist/utils/api.d.ts +85 -0
- package/dist/utils/api.js +287 -0
- package/dist/utils/circuitBreaker.d.ts +43 -0
- package/dist/utils/circuitBreaker.js +91 -0
- package/dist/utils/error-message.d.ts +1 -0
- package/dist/utils/error-message.js +103 -0
- package/dist/utils/layout/reservedSpace.d.ts +59 -0
- package/dist/utils/layout/reservedSpace.js +102 -0
- package/dist/utils/logout.d.ts +14 -0
- package/dist/utils/logout.js +32 -0
- package/dist/vibe/client.d.ts +261 -0
- package/dist/vibe/client.js +445 -0
- package/dist/vibe/errors.d.ts +83 -0
- package/dist/vibe/errors.js +146 -0
- package/dist/vibe/generic.d.ts +234 -0
- package/dist/vibe/generic.js +369 -0
- package/dist/vibe/hooks/index.d.ts +169 -0
- package/dist/vibe/hooks/index.js +252 -0
- package/dist/vibe/index.d.ts +23 -0
- package/dist/vibe/index.js +67 -0
- package/dist/vibe/sessions.d.ts +161 -0
- package/dist/vibe/sessions.js +391 -0
- package/dist/vibe/types.d.ts +353 -0
- package/dist/vibe/types.js +315 -0
- package/package.json +855 -0
- package/scripts/check-internal-url-usage.sh +73 -0
- package/scripts/dev-broker.ps1 +35 -0
- package/scripts/dev-local.ps1 +45 -0
- package/src/api/auth-handler.ts +550 -0
- package/src/api/index.ts +18 -0
- package/src/api-handlers/account/change-password.ts +145 -0
- package/src/api-handlers/account/masked-info.ts +45 -0
- package/src/api-handlers/account/profile.ts +80 -0
- package/src/api-handlers/account/recovery/initiate.ts +23 -0
- package/src/api-handlers/account/recovery/send-code.ts +25 -0
- package/src/api-handlers/account/recovery/verify-code.ts +25 -0
- package/src/api-handlers/account/reset-password.ts +23 -0
- package/src/api-handlers/account/send-code.ts +76 -0
- package/src/api-handlers/account/update-phone.ts +79 -0
- package/src/api-handlers/account/validate-password.ts +118 -0
- package/src/api-handlers/account/verify-email.ts +125 -0
- package/src/api-handlers/account/verify-sms.ts +125 -0
- package/src/api-handlers/admin/analytics.ts +445 -0
- package/src/api-handlers/admin/audit.ts +225 -0
- package/src/api-handlers/admin/index.ts +59 -0
- package/src/api-handlers/admin/redis-sessions.ts +253 -0
- package/src/api-handlers/admin/sessions.ts +320 -0
- package/src/api-handlers/admin/site-logs.ts +367 -0
- package/src/api-handlers/admin/users.ts +244 -0
- package/src/api-handlers/admin/vibe-data.ts +326 -0
- package/src/api-handlers/anon/preferences.ts +123 -0
- package/src/api-handlers/auth/jwks.ts +20 -0
- package/src/api-handlers/auth/login.ts +240 -0
- package/src/api-handlers/auth/refresh.ts +687 -0
- package/src/api-handlers/auth/signout.ts +212 -0
- package/src/api-handlers/auth/status.ts +23 -0
- package/src/api-handlers/auth/update-session.ts +125 -0
- package/src/api-handlers/auth/validate.ts +44 -0
- package/src/api-handlers/auth/verify-code.ts +129 -0
- package/src/api-handlers/session/refresh-viability.ts +36 -0
- package/src/api-handlers/session/viability.ts +166 -0
- package/src/api-handlers/test/force-expire.ts +67 -0
- package/src/auth/auth-decision.ts +230 -0
- package/src/auth/auth-options.ts +237 -0
- package/src/auth/callbacks/index.ts +7 -0
- package/src/auth/callbacks/jwt.ts +382 -0
- package/src/auth/callbacks/session.ts +243 -0
- package/src/auth/callbacks/signin.ts +56 -0
- package/src/auth/events/index.ts +5 -0
- package/src/auth/events/signout.ts +33 -0
- package/src/auth/providers/credentials.ts +256 -0
- package/src/auth/providers/index.ts +6 -0
- package/src/auth/providers/oauth.ts +114 -0
- package/src/auth/route-config.ts +220 -0
- package/src/auth/types/auth-types.ts +555 -0
- package/src/auth/types/index.ts +7 -0
- package/src/auth/unauthenticated-routes.ts +3 -0
- package/src/auth/utils/idp-client.ts +444 -0
- package/src/auth/utils/index.ts +6 -0
- package/src/auth/utils/token-utils.ts +244 -0
- package/src/client/AuthContext.tsx +140 -0
- package/src/client/fetch-with-auth.ts +48 -0
- package/src/client/fetchWithSession.ts +21 -0
- package/src/client/index.ts +13 -0
- package/src/client/useAnonSession.ts +131 -0
- package/src/components/SessionSync.tsx +137 -0
- package/src/components/SignalRHealthCheck.tsx +131 -0
- package/src/components/account/UserAvatarMenu.tsx +217 -0
- package/src/components/account/index.ts +8 -0
- package/src/components/admin/AlertSettingsTab.tsx +728 -0
- package/src/components/admin/AnalyticsTab.tsx +703 -0
- package/src/components/admin/DataBrowserTab.tsx +505 -0
- package/src/components/admin/LoggingSettingsTab.tsx +665 -0
- package/src/components/admin/SessionsTab.tsx +414 -0
- package/src/components/admin/StatsTab.tsx +379 -0
- package/src/components/admin/VibeAdminContext.tsx +87 -0
- package/src/components/admin/VibeAdminLayout.tsx +185 -0
- package/src/components/admin/index.ts +59 -0
- package/src/components/auth/FederatedAuthSection.tsx +95 -0
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -0
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -0
- package/src/components/auth/TraditionalAuthSection.tsx +99 -0
- package/src/components/recovery/CompleteStep.tsx +36 -0
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -0
- package/src/components/recovery/SelectMethodStep.tsx +73 -0
- package/src/components/recovery/SetPasswordStep.tsx +97 -0
- package/src/components/recovery/VerifyCodeStep.tsx +90 -0
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -0
- package/src/components/reserved/ReservedStatusBox.tsx +118 -0
- package/src/components/ui/BetaBadge.tsx +58 -0
- package/src/components/ui/Footer.tsx +93 -0
- package/src/config/env.ts +57 -0
- package/src/config/logger.ts +62 -0
- package/src/config/logging-config.ts +82 -0
- package/src/config/unauthenticated-routes.ts +19 -0
- package/src/config/vibe-log-transport.ts +250 -0
- package/src/edge/internal-api-url.ts +65 -0
- package/src/edge/middleware.ts +42 -0
- package/src/hooks/useAuth.ts +115 -0
- package/src/hooks/useAuthSettings.ts +97 -0
- package/src/hooks/useAvailableProviders.ts +118 -0
- package/src/hooks/usePasswordValidation.ts +127 -0
- package/src/hooks/useProfile.ts +75 -0
- package/src/hooks/usePublicAuthSettings.ts +149 -0
- package/src/hooks/useSessionExpiration.ts +102 -0
- package/src/hooks/useViabilitySession.ts +335 -0
- package/src/index.ts +63 -0
- package/src/lib/anon-session.ts +213 -0
- package/src/lib/api-handler.ts +625 -0
- package/src/lib/app-slug.ts +178 -0
- package/src/lib/demo-mode.ts +13 -0
- package/src/lib/geolocation.ts +265 -0
- package/src/lib/idp-client-config.ts +442 -0
- package/src/lib/idp-fetch.ts +101 -0
- package/src/lib/internal-api.ts +171 -0
- package/src/lib/jwt-decode-client.ts +45 -0
- package/src/lib/jwt-decode.ts +83 -0
- package/src/lib/nextauth-secret.ts +126 -0
- package/src/lib/rate-limit-service.ts +9 -0
- package/src/lib/redis.ts +27 -0
- package/src/lib/refresh-token-validator.ts +64 -0
- package/src/lib/roles.ts +177 -0
- package/src/lib/secret-validation.ts +8 -0
- package/src/lib/session-store.ts +637 -0
- package/src/lib/session.ts +34 -0
- package/src/lib/site-logger.ts +245 -0
- package/src/lib/standardized-client-api.ts +896 -0
- package/src/lib/startup-init.ts +247 -0
- package/src/lib/test-aware-get-token.ts +30 -0
- package/src/lib/token-expiry.ts +40 -0
- package/src/lib/token-lifecycle.ts +477 -0
- package/src/lib/types/api-responses.ts +336 -0
- package/src/lib/user-agent-parser.ts +252 -0
- package/src/logging/api/admin-analytics.ts +51 -0
- package/src/logging/api/audit-log.ts +53 -0
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -0
- package/src/logging/components/AuditLogViewer.tsx +125 -0
- package/src/logging/components/ErrorMetricsCard.tsx +98 -0
- package/src/logging/components/HealthMetricsCard.tsx +70 -0
- package/src/logging/hooks/useAdminAnalytics.ts +22 -0
- package/src/logging/hooks/useAuditLog.ts +24 -0
- package/src/logging/hooks/useErrorMetrics.ts +40 -0
- package/src/logging/hooks/useHealthMetrics.ts +44 -0
- package/src/logging/index.ts +18 -0
- package/src/logging/types/analytics.ts +81 -0
- package/src/logging/types/audit.ts +31 -0
- package/src/logging/types/index.ts +3 -0
- package/src/middleware/auth-decision.ts +43 -0
- package/src/middleware/create-middleware.ts +626 -0
- package/src/middleware/rbac-check.ts +244 -0
- package/src/middleware/twofa-presets.ts +224 -0
- package/src/models/DecodedAccessToken.ts +17 -0
- package/src/models/SessionModel.ts +258 -0
- package/src/pages/admin-login/page.tsx +229 -0
- package/src/pages/admin-roles/RolesAdminPage.tsx +357 -0
- package/src/pages/admin-roles/index.ts +9 -0
- package/src/pages/admin-roles/modals.tsx +469 -0
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +380 -0
- package/src/pages/client-admin/index.ts +33 -0
- package/src/pages/login/page.tsx +463 -0
- package/src/pages/profile/EnhancedProfilePage.tsx +479 -0
- package/src/pages/profile/index.ts +9 -0
- package/src/pages/profile/page.tsx +166 -0
- package/src/pages/recovery/page.tsx +234 -0
- package/src/pages/roles/MyRolesPage.tsx +211 -0
- package/src/pages/roles/components.tsx +294 -0
- package/src/pages/roles/index.ts +17 -0
- package/src/pages/security/EnhancedSecurityPage.tsx +574 -0
- package/src/pages/security/index.ts +9 -0
- package/src/pages/security/page.tsx +507 -0
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -0
- package/src/pages/settings/index.ts +9 -0
- package/src/pages/settings/page.tsx +47 -0
- package/src/pages/showcase/ShowcasePage.tsx +530 -0
- package/src/pages/showcase/index.ts +13 -0
- package/src/pages/test-env/EmergencyLogoutPage.tsx +179 -0
- package/src/pages/test-env/JwtInspectPage.tsx +418 -0
- package/src/pages/test-env/RefreshTokenPage.tsx +155 -0
- package/src/pages/test-env/TestEnvPage.tsx +116 -0
- package/src/pages/test-env/index.ts +25 -0
- package/src/pages/verify-code/page.tsx +648 -0
- package/src/routes/account/index.ts +32 -0
- package/src/routes/account/masked-info.ts +37 -0
- package/src/routes/account/send-code.ts +40 -0
- package/src/routes/account/update-phone.ts +13 -0
- package/src/routes/account/verify-email.ts +41 -0
- package/src/routes/account/verify-sms.ts +41 -0
- package/src/routes/auth/index.ts +23 -0
- package/src/routes/auth/logout.ts +127 -0
- package/src/routes/auth/nextauth.ts +71 -0
- package/src/routes/auth/refresh.ts +54 -0
- package/src/routes/auth/session.ts +193 -0
- package/src/routes/auth/settings.ts +75 -0
- package/src/routes/auth/viability.ts +220 -0
- package/src/routes/index.ts +18 -0
- package/src/routes/session/index.ts +7 -0
- package/src/routes/session/refresh-viability.ts +17 -0
- package/src/services/signalrActivityService.ts +258 -0
- package/src/stores/authStore.ts +1904 -0
- package/src/templates/instrumentation.ts +41 -0
- package/src/theme/ThemeProvider.tsx +39 -0
- package/src/theme/default.ts +33 -0
- package/src/theme/index.ts +31 -0
- package/src/theme/types.ts +69 -0
- package/src/theme/useTheme.ts +57 -0
- package/src/theme/utils.ts +40 -0
- package/src/types/api.ts +13 -0
- package/src/types/auth.d.ts +15 -0
- package/src/types/auth.ts +22 -0
- package/src/types/logging.ts +11 -0
- package/src/types/next-auth.d.ts +15 -0
- package/src/types/recovery.ts +54 -0
- package/src/types/security.ts +1 -0
- package/src/utils/api.ts +353 -0
- package/src/utils/circuitBreaker.ts +40 -0
- package/src/utils/error-message.ts +108 -0
- package/src/utils/layout/reservedSpace.ts +124 -0
- package/src/utils/logout.ts +30 -0
- package/src/vibe/client.ts +590 -0
- package/src/vibe/errors.ts +185 -0
- package/src/vibe/generic.ts +429 -0
- package/src/vibe/hooks/index.ts +367 -0
- package/src/vibe/index.ts +121 -0
- package/src/vibe/sessions.ts +551 -0
- package/src/vibe/types.ts +577 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ========================================================================================
|
|
4
|
+
// BULLETPROOF STANDARDIZED CLIENT API - ZERO TOLERANCE FOR BAD RESPONSES
|
|
5
|
+
// ========================================================================================
|
|
6
|
+
// This client API ENFORCES the standardized response format
|
|
7
|
+
// It will BREAK if APIs don't return the expected structure
|
|
8
|
+
// NO MORE GUESSING data.data.data.data - EVER AGAIN!
|
|
9
|
+
// ========================================================================================
|
|
10
|
+
|
|
11
|
+
import { getSession } from 'next-auth/react';
|
|
12
|
+
import { DefaultSession } from 'next-auth';
|
|
13
|
+
import {
|
|
14
|
+
StandardizedResponse,
|
|
15
|
+
StandardizedApiResponse,
|
|
16
|
+
StandardizedPagedResponse,
|
|
17
|
+
StandardizedErrorResponse,
|
|
18
|
+
validateStandardizedResponse,
|
|
19
|
+
isSuccessResponse,
|
|
20
|
+
isPagedResponse,
|
|
21
|
+
isErrorResponse
|
|
22
|
+
} from './types/api-responses';
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// ========================================================================================
|
|
27
|
+
// CLIENT API ERROR TYPES
|
|
28
|
+
// ========================================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ERROR THROWN WHEN API RESPONSE FORMAT IS INVALID
|
|
32
|
+
* This means the API is NOT following our standardized format
|
|
33
|
+
*/
|
|
34
|
+
export class ApiResponseFormatError extends Error {
|
|
35
|
+
constructor(message: string, public readonly endpoint: string, public readonly rawResponse: unknown) {
|
|
36
|
+
super(`API_FORMAT_ERROR: ${message}`);
|
|
37
|
+
this.name = 'ApiResponseFormatError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ERROR THROWN WHEN API RETURNS A STANDARDIZED ERROR RESPONSE
|
|
43
|
+
* This is a properly formatted error from the API
|
|
44
|
+
*/
|
|
45
|
+
export class ApiBusinessLogicError extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
public readonly errorCode: string,
|
|
48
|
+
message: string,
|
|
49
|
+
public readonly operation: string,
|
|
50
|
+
public readonly details?: unknown
|
|
51
|
+
) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = 'ApiBusinessLogicError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* ERROR THROWN WHEN VALIDATION FAILS
|
|
59
|
+
* This is a properly formatted validation error from the API
|
|
60
|
+
*/
|
|
61
|
+
export class ApiValidationError extends Error {
|
|
62
|
+
constructor(
|
|
63
|
+
message: string,
|
|
64
|
+
public readonly operation: string,
|
|
65
|
+
public readonly validationErrors: Record<string, string[]>,
|
|
66
|
+
public readonly invalidValue?: unknown,
|
|
67
|
+
public readonly primaryField?: string
|
|
68
|
+
) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = 'ApiValidationError';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ERROR THROWN WHEN NETWORK/HTTP ISSUES OCCUR
|
|
76
|
+
*/
|
|
77
|
+
export class ApiNetworkError extends Error {
|
|
78
|
+
constructor(message: string, public readonly status: number, public readonly endpoint: string) {
|
|
79
|
+
super(`NETWORK_ERROR: ${message}`);
|
|
80
|
+
this.name = 'ApiNetworkError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ========================================================================================
|
|
85
|
+
// AUTHENTICATION STATE MANAGEMENT
|
|
86
|
+
// ========================================================================================
|
|
87
|
+
|
|
88
|
+
// Coordinate client-side refresh to avoid duplicate refresh calls racing with
|
|
89
|
+
// server-side middleware or other tabs. Only one refresh runs at a time.
|
|
90
|
+
let refreshInFlight: Promise<boolean> | null = null;
|
|
91
|
+
|
|
92
|
+
// Enhanced redirect logic with grace period and retry attempts
|
|
93
|
+
let authRedirectScheduled = false;
|
|
94
|
+
let lastAuthFailureTime = 0;
|
|
95
|
+
let consecutiveAuthFailures = 0;
|
|
96
|
+
const AUTH_FAILURE_GRACE_PERIOD = 2000; // 2 seconds grace period
|
|
97
|
+
const MAX_AUTH_FAILURES_BEFORE_REDIRECT = 2; // Allow 2 failures before redirect
|
|
98
|
+
const AUTH_FAILURE_RESET_WINDOW = 30000; // Reset failure count after 30 seconds
|
|
99
|
+
|
|
100
|
+
// Helper: detect pre-2FA session (session exists, requires 2FA and not completed)
|
|
101
|
+
function isPreTwoFactorSession(session: any | null | undefined): boolean {
|
|
102
|
+
return !!(session?.user?.requiresTwoFactor && !session?.user?.twoFactorSessionVerified);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Reset auth failure state on successful requests
|
|
106
|
+
function resetAuthFailureState() {
|
|
107
|
+
if (consecutiveAuthFailures > 0) {
|
|
108
|
+
console.log(`✅ Resetting auth failure state (was ${consecutiveAuthFailures} failures)`);
|
|
109
|
+
consecutiveAuthFailures = 0;
|
|
110
|
+
lastAuthFailureTime = 0;
|
|
111
|
+
authRedirectScheduled = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scheduleLoginRedirect(isImmediate = false) {
|
|
116
|
+
if (authRedirectScheduled) return;
|
|
117
|
+
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
|
|
120
|
+
// Reset consecutive failures if enough time has passed
|
|
121
|
+
if (now - lastAuthFailureTime > AUTH_FAILURE_RESET_WINDOW) {
|
|
122
|
+
consecutiveAuthFailures = 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
consecutiveAuthFailures++;
|
|
126
|
+
lastAuthFailureTime = now;
|
|
127
|
+
|
|
128
|
+
console.warn(`🔴 Auth failure #${consecutiveAuthFailures}, immediate: ${isImmediate}`);
|
|
129
|
+
|
|
130
|
+
// Only redirect if we've had multiple failures or if explicitly requested
|
|
131
|
+
if (!isImmediate && consecutiveAuthFailures < MAX_AUTH_FAILURES_BEFORE_REDIRECT) {
|
|
132
|
+
console.log(`⏳ Delaying redirect - only ${consecutiveAuthFailures} failures so far`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
authRedirectScheduled = true;
|
|
137
|
+
|
|
138
|
+
const redirectFunction = () => {
|
|
139
|
+
try {
|
|
140
|
+
const returnUrl = encodeURIComponent(`${window.location.pathname}${window.location.search}`);
|
|
141
|
+
console.warn(`🔄 Redirecting to login with return URL: ${returnUrl}`);
|
|
142
|
+
window.location.href = `/account-auth/login?returnUrl=${returnUrl}`;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('❌ Error during login redirect:', error);
|
|
145
|
+
// Final fallback
|
|
146
|
+
try {
|
|
147
|
+
window.location.href = '/account-auth/login';
|
|
148
|
+
} catch {
|
|
149
|
+
// no-op if window is not available
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (isImmediate) {
|
|
155
|
+
// Immediate redirect for critical auth failures
|
|
156
|
+
redirectFunction();
|
|
157
|
+
} else {
|
|
158
|
+
// Small delay to allow any pending requests to complete
|
|
159
|
+
console.log(`⏳ Scheduling redirect with ${AUTH_FAILURE_GRACE_PERIOD}ms grace period`);
|
|
160
|
+
setTimeout(redirectFunction, AUTH_FAILURE_GRACE_PERIOD);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ========================================================================================
|
|
165
|
+
// RESULT TYPES FOR CLIENT CONSUMPTION
|
|
166
|
+
// ========================================================================================
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* SUCCESSFUL API CALL RESULT
|
|
170
|
+
* This is what gets returned to the calling code for successful operations
|
|
171
|
+
*/
|
|
172
|
+
export interface ApiSuccessResult<TData> {
|
|
173
|
+
/** Always true for success */
|
|
174
|
+
success: true;
|
|
175
|
+
/** The actual data - NO NESTING! Direct access! */
|
|
176
|
+
data: TData;
|
|
177
|
+
/** Human-readable success message from API */
|
|
178
|
+
message: string;
|
|
179
|
+
/** Operation code for tracking/debugging */
|
|
180
|
+
operation_code: string;
|
|
181
|
+
/** Server timestamp (if provided) */
|
|
182
|
+
timestamp?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* SUCCESSFUL PAGED API CALL RESULT
|
|
187
|
+
* This is what gets returned for successful paged operations
|
|
188
|
+
*/
|
|
189
|
+
export interface ApiPagedResult<TData> {
|
|
190
|
+
/** Always true for success */
|
|
191
|
+
success: true;
|
|
192
|
+
/** The actual data array - NO NESTING! Direct access! */
|
|
193
|
+
items: TData[];
|
|
194
|
+
/** Human-readable success message from API */
|
|
195
|
+
message: string;
|
|
196
|
+
/** Operation code for tracking/debugging */
|
|
197
|
+
operation_code: string;
|
|
198
|
+
/** Pagination information */
|
|
199
|
+
pagination: {
|
|
200
|
+
current_page: number;
|
|
201
|
+
total_pages: number;
|
|
202
|
+
page_size: number;
|
|
203
|
+
total_items: number;
|
|
204
|
+
has_next_page: boolean;
|
|
205
|
+
has_previous_page: boolean;
|
|
206
|
+
};
|
|
207
|
+
/** Server timestamp (if provided) */
|
|
208
|
+
timestamp?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* FAILED API CALL RESULT
|
|
213
|
+
* This is what gets returned to the calling code for failed operations
|
|
214
|
+
*/
|
|
215
|
+
export interface ApiErrorResult {
|
|
216
|
+
/** Always false for errors */
|
|
217
|
+
success: false;
|
|
218
|
+
/** Standardized error code */
|
|
219
|
+
error_code: string;
|
|
220
|
+
/** Human-readable error message */
|
|
221
|
+
message: string;
|
|
222
|
+
/** Operation that failed */
|
|
223
|
+
operation: string;
|
|
224
|
+
/** Additional error details (if any) */
|
|
225
|
+
details?: unknown;
|
|
226
|
+
/** Validation errors (if any) */
|
|
227
|
+
validation_errors?: Record<string, string[]>;
|
|
228
|
+
/** Server-provided request identifier for tracing (only set on real errors) */
|
|
229
|
+
request_id?: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** UNION TYPE FOR ALL POSSIBLE API RESULTS */
|
|
233
|
+
export type ApiResult<TData> = ApiSuccessResult<TData> | ApiPagedResult<TData> | ApiErrorResult;
|
|
234
|
+
|
|
235
|
+
// ========================================================================================
|
|
236
|
+
// BULLETPROOF CLIENT API SERVICE
|
|
237
|
+
// ========================================================================================
|
|
238
|
+
|
|
239
|
+
class StandardizedClientApiService {
|
|
240
|
+
private baseUrl: string;
|
|
241
|
+
|
|
242
|
+
constructor() {
|
|
243
|
+
this.baseUrl = '';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* MAKES HTTP REQUEST AND VALIDATES RESPONSE FORMAT
|
|
248
|
+
* This method ENFORCES standardized response format compliance
|
|
249
|
+
* Will throw ApiResponseFormatError if format is invalid
|
|
250
|
+
*/
|
|
251
|
+
private async makeRequest<TData = unknown>(
|
|
252
|
+
endpoint: string,
|
|
253
|
+
options: RequestInit = {},
|
|
254
|
+
sessionToken?: string
|
|
255
|
+
): Promise<ApiResult<TData>> {
|
|
256
|
+
const fullEndpoint = `${this.baseUrl}${endpoint}`;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
// Use provided token or get from NextAuth session
|
|
260
|
+
const currentSession = await getSession();
|
|
261
|
+
let token = sessionToken || currentSession?.accessToken;
|
|
262
|
+
|
|
263
|
+
// Preflight freshness check: if token is near expiry, coordinate refresh BEFORE making request
|
|
264
|
+
const pre2FA = isPreTwoFactorSession(currentSession);
|
|
265
|
+
const hasRefresh = !!currentSession?.refreshToken;
|
|
266
|
+
if (!pre2FA && hasRefresh) {
|
|
267
|
+
try {
|
|
268
|
+
let timeLeft: number | null = null;
|
|
269
|
+
if (currentSession?.accessTokenExpires) {
|
|
270
|
+
timeLeft = currentSession.accessTokenExpires - Date.now();
|
|
271
|
+
} else if (token) {
|
|
272
|
+
// Fallback decode only if session did not include expiry
|
|
273
|
+
const { jwtDecode } = await import('./jwt-decode-client');
|
|
274
|
+
const decoded: any = jwtDecode(token);
|
|
275
|
+
const expMs = decoded?.exp ? decoded.exp * 1000 : 0;
|
|
276
|
+
timeLeft = expMs - Date.now();
|
|
277
|
+
}
|
|
278
|
+
// Refresh if <= 60s remaining (or already expired)
|
|
279
|
+
if (timeLeft !== null && !Number.isNaN(timeLeft) && timeLeft <= 60000) {
|
|
280
|
+
console.log(`⏳ Access token expiring soon (${Math.floor(timeLeft/1000)}s). Coordinating refresh before request...`);
|
|
281
|
+
if (!refreshInFlight) {
|
|
282
|
+
refreshInFlight = (async () => {
|
|
283
|
+
const reqId = crypto.randomUUID();
|
|
284
|
+
const rr = await fetch('/api/auth/refresh', {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
credentials: 'include',
|
|
287
|
+
headers: { 'X-Request-ID': reqId },
|
|
288
|
+
});
|
|
289
|
+
if (rr.ok || rr.status === 409) {
|
|
290
|
+
// ok or in-progress; give it a beat in case of 409
|
|
291
|
+
if (rr.status === 409) await new Promise(r => setTimeout(r, 1200));
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (rr.status === 401 || rr.status === 403) {
|
|
295
|
+
scheduleLoginRedirect();
|
|
296
|
+
throw new ApiNetworkError('Authentication failed - unable to refresh session', rr.status, endpoint);
|
|
297
|
+
}
|
|
298
|
+
const et = await rr.text();
|
|
299
|
+
throw new ApiNetworkError(et || 'Token refresh failed', rr.status, endpoint);
|
|
300
|
+
})().finally(() => { refreshInFlight = null; });
|
|
301
|
+
}
|
|
302
|
+
await refreshInFlight;
|
|
303
|
+
const newSessionAfter = await getSession();
|
|
304
|
+
token = newSessionAfter?.accessToken || token;
|
|
305
|
+
}
|
|
306
|
+
} catch (preErr) {
|
|
307
|
+
console.warn('Preflight token freshness check failed (continuing to attempt request):', preErr);
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
// Elegantly skip preflight refresh in pre-2FA window or when no refresh token exists
|
|
311
|
+
if (pre2FA) {
|
|
312
|
+
console.log('⏭️ Skipping preflight refresh: 2FA not complete (no refresh allowed yet)');
|
|
313
|
+
} else if (!hasRefresh) {
|
|
314
|
+
console.log('⏭️ Skipping preflight refresh: no refresh token present');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const config: RequestInit = {
|
|
319
|
+
...options,
|
|
320
|
+
headers: {
|
|
321
|
+
'Content-Type': 'application/json',
|
|
322
|
+
...(token && { 'Authorization': `Bearer ${token}` }),
|
|
323
|
+
...options.headers,
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
console.log(`🔄 API Request: ${options.method || 'GET'} ${fullEndpoint}`);
|
|
328
|
+
|
|
329
|
+
const response = await fetch(fullEndpoint, config);
|
|
330
|
+
|
|
331
|
+
if (!response.ok) {
|
|
332
|
+
// Handle coordination blocking (503) with auto-retry
|
|
333
|
+
if (response.status === 503) {
|
|
334
|
+
console.log('🔄 Got 503 Service Unavailable, attempting auto-retry for coordination...');
|
|
335
|
+
|
|
336
|
+
// Parse Retry-After header (in seconds)
|
|
337
|
+
const retryAfterHeader = response.headers.get('Retry-After');
|
|
338
|
+
let retryAfterSeconds = 1; // Default to 1 second
|
|
339
|
+
|
|
340
|
+
if (retryAfterHeader && /^\d+$/.test(retryAfterHeader)) {
|
|
341
|
+
retryAfterSeconds = parseInt(retryAfterHeader, 10);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const baseDelayMs = retryAfterSeconds * 1000;
|
|
345
|
+
const maxRetries = 3;
|
|
346
|
+
|
|
347
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
348
|
+
// Add jitter to prevent thundering herd
|
|
349
|
+
const jitterMs = Math.floor(Math.random() * 300) - 150; // ±150ms jitter
|
|
350
|
+
const exponentialBackoff = Math.pow(1.5, attempt - 1); // Mild exponential backoff
|
|
351
|
+
const delayMs = Math.max(100, baseDelayMs * exponentialBackoff + jitterMs);
|
|
352
|
+
|
|
353
|
+
console.log(`🔄 503 retry attempt ${attempt}/${maxRetries}, waiting ${delayMs}ms...`);
|
|
354
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const retryResponse = await fetch(fullEndpoint, config);
|
|
358
|
+
|
|
359
|
+
if (retryResponse.ok) {
|
|
360
|
+
console.log(`✅ 503 retry attempt ${attempt} succeeded`);
|
|
361
|
+
const rawData = await retryResponse.json();
|
|
362
|
+
resetAuthFailureState();
|
|
363
|
+
|
|
364
|
+
// COMPATIBILITY MODE: Handle both formats
|
|
365
|
+
if (rawData && typeof rawData === 'object' && 'success' in rawData) {
|
|
366
|
+
const validatedResponse = validateStandardizedResponse<TData>(rawData, endpoint);
|
|
367
|
+
return this.convertToApiResult(validatedResponse);
|
|
368
|
+
} else {
|
|
369
|
+
// New format - raw data
|
|
370
|
+
const wrappedResponse: ApiSuccessResult<TData> = {
|
|
371
|
+
success: true,
|
|
372
|
+
data: rawData as TData,
|
|
373
|
+
message: 'Success',
|
|
374
|
+
operation_code: 'RAW_RESPONSE',
|
|
375
|
+
timestamp: new Date().toISOString()
|
|
376
|
+
};
|
|
377
|
+
return wrappedResponse;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// If we get another 503, continue retrying
|
|
382
|
+
if (retryResponse.status === 503) {
|
|
383
|
+
console.log(`🔄 503 retry attempt ${attempt} got another 503, will retry...`);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// If we get a different error, break and handle it normally
|
|
388
|
+
console.log(`❌ 503 retry attempt ${attempt} got ${retryResponse.status}, stopping retries`);
|
|
389
|
+
// Fall through to handle the retry response error
|
|
390
|
+
const errorText = await retryResponse.text();
|
|
391
|
+
let errorData;
|
|
392
|
+
try {
|
|
393
|
+
errorData = JSON.parse(errorText);
|
|
394
|
+
if (errorData && typeof errorData === 'object' && 'success' in errorData) {
|
|
395
|
+
// Detect PayEz canonical error envelope and map accordingly
|
|
396
|
+
if (errorData.error && typeof errorData.error === 'object') {
|
|
397
|
+
const reqIdHeader = retryResponse.headers.get('X-Request-ID') || retryResponse.headers.get('X-Correlation-ID');
|
|
398
|
+
const reqIdBody = errorData?.request_id || errorData?.requestId;
|
|
399
|
+
const errorResult: ApiErrorResult = {
|
|
400
|
+
success: false,
|
|
401
|
+
error_code: errorData?.error?.code || errorData?.error_code || errorData?.code || `HTTP_${retryResponse.status}`,
|
|
402
|
+
message: errorData?.error?.message || errorData?.message || `Request failed with status ${retryResponse.status}`,
|
|
403
|
+
operation: endpoint,
|
|
404
|
+
details: (errorData?.error?.details ?? errorData?.details) || undefined,
|
|
405
|
+
...(reqIdBody || reqIdHeader ? { request_id: (reqIdBody || reqIdHeader) as string } : {})
|
|
406
|
+
};
|
|
407
|
+
return errorResult;
|
|
408
|
+
}
|
|
409
|
+
// Otherwise attempt to validate as our standardized error shape
|
|
410
|
+
const validatedError = validateStandardizedResponse(errorData, endpoint);
|
|
411
|
+
return this.convertToApiResult(validatedError) as ApiResult<TData>;
|
|
412
|
+
} else {
|
|
413
|
+
// New/unknown error format - best-effort mapping
|
|
414
|
+
const reqIdHeader = retryResponse.headers.get('X-Request-ID') || retryResponse.headers.get('X-Correlation-ID');
|
|
415
|
+
const reqIdBody = errorData?.request_id || errorData?.requestId;
|
|
416
|
+
const errorResult: ApiErrorResult = {
|
|
417
|
+
success: false,
|
|
418
|
+
error_code: errorData?.error_code || errorData?.code || `HTTP_${retryResponse.status}`,
|
|
419
|
+
message: errorData?.message || (typeof errorData?.error === 'string' ? errorData.error : errorData?.error?.message) || errorText || `Request failed with status ${retryResponse.status}`,
|
|
420
|
+
operation: endpoint,
|
|
421
|
+
details: (errorData?.error?.details ?? errorData?.details) || undefined,
|
|
422
|
+
...(reqIdBody || reqIdHeader ? { request_id: (reqIdBody || reqIdHeader) as string } : {})
|
|
423
|
+
};
|
|
424
|
+
return errorResult;
|
|
425
|
+
}
|
|
426
|
+
} catch {
|
|
427
|
+
const reqIdHeader2 = retryResponse.headers.get('X-Request-ID') || retryResponse.headers.get('X-Correlation-ID');
|
|
428
|
+
const errorResult: ApiErrorResult = {
|
|
429
|
+
success: false,
|
|
430
|
+
error_code: `HTTP_${retryResponse.status}`,
|
|
431
|
+
message: errorText || `Request failed with status ${retryResponse.status}`,
|
|
432
|
+
operation: endpoint,
|
|
433
|
+
...(reqIdHeader2 ? { request_id: reqIdHeader2 } : {})
|
|
434
|
+
};
|
|
435
|
+
return errorResult;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
} catch (retryError) {
|
|
439
|
+
console.log(`❌ 503 retry attempt ${attempt} failed with network error:`, retryError);
|
|
440
|
+
if (attempt === maxRetries) {
|
|
441
|
+
// If all retries failed with network errors, throw the original error
|
|
442
|
+
throw new ApiNetworkError('Service temporarily unavailable after retries', 503, endpoint);
|
|
443
|
+
}
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// If we got here, all retries failed - fall through to normal error handling
|
|
449
|
+
console.log('❌ All 503 retry attempts exhausted, treating as error');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Handle authentication errors with a single refresh+retry
|
|
453
|
+
if (response.status === 401) {
|
|
454
|
+
console.log('🔑 Got 401, checking if we have a session to refresh...');
|
|
455
|
+
|
|
456
|
+
// CRITICAL FIX: Check if we actually have a session before attempting refresh
|
|
457
|
+
const currentSession = await getSession();
|
|
458
|
+
if (!currentSession || !currentSession.accessToken) {
|
|
459
|
+
console.log('🚫 No valid session found, redirecting to login instead of refresh');
|
|
460
|
+
scheduleLoginRedirect(true); // Immediate redirect
|
|
461
|
+
throw new ApiNetworkError('Authentication required - no valid session', 401, endpoint);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Elegantly gate refresh during pre-2FA or when no refresh token exists
|
|
465
|
+
const pre2FA_now = isPreTwoFactorSession(currentSession);
|
|
466
|
+
const hasRefresh_now = !!currentSession?.refreshToken;
|
|
467
|
+
if (pre2FA_now || !hasRefresh_now) {
|
|
468
|
+
console.log('⏭️ Skipping 401-driven refresh:', {
|
|
469
|
+
reason: pre2FA_now ? 'pre-2FA session' : 'no refresh token',
|
|
470
|
+
requiresTwoFactor: (currentSession?.user as any)?.requiresTwoFactor,
|
|
471
|
+
twoFactorVerified: (currentSession?.user as any)?.twoFactorSessionVerified,
|
|
472
|
+
hasRefreshToken: hasRefresh_now
|
|
473
|
+
});
|
|
474
|
+
// CRITICAL: Redirect to login immediately if refresh is impossible
|
|
475
|
+
console.log('🚫 Cannot refresh session, redirecting to login');
|
|
476
|
+
scheduleLoginRedirect(true); // Immediate redirect
|
|
477
|
+
throw new ApiNetworkError(
|
|
478
|
+
pre2FA_now
|
|
479
|
+
? 'Two-factor authentication required'
|
|
480
|
+
: 'Session expired - refresh token unavailable',
|
|
481
|
+
401,
|
|
482
|
+
endpoint
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
console.log('🔑 Valid session found, attempting token refresh...');
|
|
487
|
+
|
|
488
|
+
// Try to refresh the token, but coordinate to avoid double refresh
|
|
489
|
+
if (!refreshInFlight) {
|
|
490
|
+
refreshInFlight = (async () => {
|
|
491
|
+
const reqId = crypto.randomUUID();
|
|
492
|
+
const refreshResponse = await fetch('/api/auth/refresh', {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
credentials: 'include',
|
|
495
|
+
headers: { 'X-Request-ID': reqId },
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (refreshResponse.ok) {
|
|
499
|
+
console.log('✅ Token refreshed successfully (client-side coordinator)');
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// If refresh is already in progress server-side, wait briefly and allow retry
|
|
504
|
+
if (refreshResponse.status === 409) {
|
|
505
|
+
console.log('↪️ Refresh in progress server-side (409). Waiting for completion...');
|
|
506
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// For auth failures, schedule redirect; for others, throw
|
|
511
|
+
if (refreshResponse.status === 401 || refreshResponse.status === 403) {
|
|
512
|
+
scheduleLoginRedirect();
|
|
513
|
+
throw new ApiNetworkError('Authentication failed - unable to refresh session', refreshResponse.status, endpoint);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const errorText = await refreshResponse.text();
|
|
517
|
+
throw new ApiNetworkError(errorText || 'Token refresh failed', refreshResponse.status, endpoint);
|
|
518
|
+
})().finally(() => { refreshInFlight = null; });
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
await refreshInFlight;
|
|
523
|
+
} catch (e) {
|
|
524
|
+
throw e; // bubble up to caller handling
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
console.log('🔁 Retrying original request after coordinated refresh...');
|
|
528
|
+
// Get the new session and retry the original request
|
|
529
|
+
const newSession = await getSession();
|
|
530
|
+
const newToken = newSession?.accessToken;
|
|
531
|
+
const retryConfig: RequestInit = {
|
|
532
|
+
...options,
|
|
533
|
+
headers: {
|
|
534
|
+
'Content-Type': 'application/json',
|
|
535
|
+
...(newToken && { 'Authorization': `Bearer ${newToken}` }),
|
|
536
|
+
...options.headers,
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const retryResponse = await fetch(fullEndpoint, retryConfig);
|
|
541
|
+
|
|
542
|
+
if (retryResponse.ok) {
|
|
543
|
+
const rawData = await retryResponse.json();
|
|
544
|
+
// Reset auth failure state on successful retry
|
|
545
|
+
resetAuthFailureState();
|
|
546
|
+
|
|
547
|
+
// COMPATIBILITY MODE: Handle both formats in retry as well
|
|
548
|
+
if (rawData && typeof rawData === 'object' && 'success' in rawData) {
|
|
549
|
+
const validatedResponse = validateStandardizedResponse<TData>(rawData, endpoint);
|
|
550
|
+
return this.convertToApiResult(validatedResponse);
|
|
551
|
+
} else {
|
|
552
|
+
// New format - raw data
|
|
553
|
+
console.log(`🔄 Converting raw retry response to standardized format for ${endpoint}`);
|
|
554
|
+
const wrappedResponse: ApiSuccessResult<TData> = {
|
|
555
|
+
success: true,
|
|
556
|
+
data: rawData as TData,
|
|
557
|
+
message: 'Success',
|
|
558
|
+
operation_code: 'RAW_RESPONSE',
|
|
559
|
+
timestamp: new Date().toISOString()
|
|
560
|
+
};
|
|
561
|
+
return wrappedResponse;
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
// If retry still 401, schedule redirect
|
|
565
|
+
if (retryResponse.status === 401) {
|
|
566
|
+
scheduleLoginRedirect();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const errorText = await retryResponse.text();
|
|
570
|
+
let errorData;
|
|
571
|
+
try {
|
|
572
|
+
errorData = JSON.parse(errorText);
|
|
573
|
+
// Check if it has the success field (old format)
|
|
574
|
+
if (errorData && typeof errorData === 'object' && 'success' in errorData) {
|
|
575
|
+
// Detect PayEz canonical error envelope and map accordingly
|
|
576
|
+
if (errorData.error && typeof errorData.error === 'object') {
|
|
577
|
+
const reqIdHeader = retryResponse.headers.get('X-Request-ID') || retryResponse.headers.get('X-Correlation-ID');
|
|
578
|
+
const reqIdBody = errorData?.request_id || errorData?.requestId;
|
|
579
|
+
const errorResult: ApiErrorResult = {
|
|
580
|
+
success: false,
|
|
581
|
+
error_code: errorData?.error?.code || errorData?.error_code || errorData?.code || `HTTP_${retryResponse.status}`,
|
|
582
|
+
message: errorData?.error?.message || errorData?.message || `Request failed with status ${retryResponse.status}`,
|
|
583
|
+
operation: endpoint,
|
|
584
|
+
details: (errorData?.error?.details ?? errorData?.details) || undefined,
|
|
585
|
+
...(reqIdBody || reqIdHeader ? { request_id: (reqIdBody || reqIdHeader) as string } : {})
|
|
586
|
+
};
|
|
587
|
+
return errorResult;
|
|
588
|
+
}
|
|
589
|
+
const validatedError = validateStandardizedResponse(errorData, endpoint);
|
|
590
|
+
return this.convertToApiResult(validatedError) as ApiResult<TData>;
|
|
591
|
+
} else {
|
|
592
|
+
// New format - convert raw error to standardized format
|
|
593
|
+
const reqIdHeader = retryResponse.headers.get('X-Request-ID') || retryResponse.headers.get('X-Correlation-ID');
|
|
594
|
+
const reqIdBody = errorData?.request_id || errorData?.requestId;
|
|
595
|
+
const errorResult: ApiErrorResult = {
|
|
596
|
+
success: false,
|
|
597
|
+
error_code: errorData?.error_code || errorData?.code || `HTTP_${retryResponse.status}`,
|
|
598
|
+
message: errorData?.message || (typeof errorData?.error === 'string' ? errorData.error : errorData?.error?.message) || errorText || `Request failed with status ${retryResponse.status}`,
|
|
599
|
+
operation: endpoint,
|
|
600
|
+
details: (errorData?.error?.details ?? errorData?.details) || undefined,
|
|
601
|
+
...(reqIdBody || reqIdHeader ? { request_id: (reqIdBody || reqIdHeader) as string } : {})
|
|
602
|
+
};
|
|
603
|
+
return errorResult;
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
// If we can't parse the error, create a generic error response
|
|
607
|
+
const reqIdHeader = retryResponse.headers.get('X-Request-ID') || retryResponse.headers.get('X-Correlation-ID');
|
|
608
|
+
const errorResult: ApiErrorResult = {
|
|
609
|
+
success: false,
|
|
610
|
+
error_code: `HTTP_${retryResponse.status}`,
|
|
611
|
+
message: errorText || `Request failed with status ${retryResponse.status}`,
|
|
612
|
+
operation: endpoint,
|
|
613
|
+
...(reqIdHeader ? { request_id: reqIdHeader } : {})
|
|
614
|
+
};
|
|
615
|
+
return errorResult;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Non-401 error: try to parse as standardized error response
|
|
621
|
+
const errorText = await response.text();
|
|
622
|
+
let errorData;
|
|
623
|
+
try {
|
|
624
|
+
errorData = JSON.parse(errorText);
|
|
625
|
+
// Check if it has the success field (old format)
|
|
626
|
+
if (errorData && typeof errorData === 'object' && 'success' in errorData) {
|
|
627
|
+
// Detect PayEz canonical error envelope and map accordingly
|
|
628
|
+
if (errorData.error && typeof errorData.error === 'object') {
|
|
629
|
+
const reqIdHeader = response.headers.get('X-Request-ID') || response.headers.get('X-Correlation-ID');
|
|
630
|
+
const reqIdBody = errorData?.request_id || errorData?.requestId;
|
|
631
|
+
const errorResult: ApiErrorResult = {
|
|
632
|
+
success: false,
|
|
633
|
+
error_code: errorData?.error?.code || errorData?.error_code || errorData?.code || `HTTP_${response.status}`,
|
|
634
|
+
message: errorData?.error?.message || errorData?.message || `Request failed with status ${response.status}`,
|
|
635
|
+
operation: endpoint,
|
|
636
|
+
details: (errorData?.error?.details ?? errorData?.details) || undefined,
|
|
637
|
+
validation_errors: errorData?.validation_errors || undefined,
|
|
638
|
+
...(reqIdBody || reqIdHeader ? { request_id: (reqIdBody || reqIdHeader) as string } : {})
|
|
639
|
+
};
|
|
640
|
+
return errorResult;
|
|
641
|
+
}
|
|
642
|
+
const validatedError = validateStandardizedResponse(errorData, endpoint);
|
|
643
|
+
return this.convertToApiResult(validatedError) as ApiResult<TData>;
|
|
644
|
+
} else {
|
|
645
|
+
// New format - convert raw error to standardized format
|
|
646
|
+
const reqIdHeader = response.headers.get('X-Request-ID') || response.headers.get('X-Correlation-ID');
|
|
647
|
+
const reqIdBody = errorData?.request_id || errorData?.requestId;
|
|
648
|
+
const errorResult: ApiErrorResult = {
|
|
649
|
+
success: false,
|
|
650
|
+
error_code: errorData?.error_code || errorData?.code || `HTTP_${response.status}`,
|
|
651
|
+
message: errorData?.message || (typeof errorData?.error === 'string' ? errorData.error : errorData?.error?.message) || errorText || `Request failed with status ${response.status}`,
|
|
652
|
+
operation: endpoint,
|
|
653
|
+
details: (errorData?.error?.details ?? errorData?.details) || undefined,
|
|
654
|
+
validation_errors: errorData?.validation_errors || undefined,
|
|
655
|
+
...(reqIdBody || reqIdHeader ? { request_id: (reqIdBody || reqIdHeader) as string } : {})
|
|
656
|
+
};
|
|
657
|
+
return errorResult;
|
|
658
|
+
}
|
|
659
|
+
} catch (parseError) {
|
|
660
|
+
// If we can't parse the error, create a generic error response
|
|
661
|
+
const reqIdHeader = response.headers.get('X-Request-ID') || response.headers.get('X-Correlation-ID');
|
|
662
|
+
const errorResult: ApiErrorResult = {
|
|
663
|
+
success: false,
|
|
664
|
+
error_code: `HTTP_${response.status}`,
|
|
665
|
+
message: errorText || `Request failed with status ${response.status}`,
|
|
666
|
+
operation: endpoint,
|
|
667
|
+
details: undefined,
|
|
668
|
+
...(reqIdHeader ? { request_id: reqIdHeader } : {})
|
|
669
|
+
};
|
|
670
|
+
return errorResult;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// SUCCESS PATH: Parse and validate response
|
|
675
|
+
const rawData = await response.json();
|
|
676
|
+
|
|
677
|
+
// Reset auth failure state on successful request
|
|
678
|
+
resetAuthFailureState();
|
|
679
|
+
|
|
680
|
+
// COMPATIBILITY MODE: Handle both old envelope format and new raw format
|
|
681
|
+
try {
|
|
682
|
+
// First check if it's the old standardized format with success field
|
|
683
|
+
if (rawData && typeof rawData === 'object' && 'success' in rawData) {
|
|
684
|
+
// Old format - validate as standardized response
|
|
685
|
+
const validatedResponse = validateStandardizedResponse<TData>(rawData, endpoint);
|
|
686
|
+
return this.convertToApiResult(validatedResponse);
|
|
687
|
+
} else {
|
|
688
|
+
// New format - raw data, wrap it in success envelope for compatibility
|
|
689
|
+
console.log(`🔄 Converting raw response to standardized format for ${endpoint}`);
|
|
690
|
+
const wrappedResponse: ApiSuccessResult<TData> = {
|
|
691
|
+
success: true,
|
|
692
|
+
data: rawData as TData,
|
|
693
|
+
message: 'Success',
|
|
694
|
+
operation_code: 'RAW_RESPONSE',
|
|
695
|
+
timestamp: new Date().toISOString()
|
|
696
|
+
};
|
|
697
|
+
return wrappedResponse;
|
|
698
|
+
}
|
|
699
|
+
} catch (validationError) {
|
|
700
|
+
// If response format is invalid, this is a CRITICAL error
|
|
701
|
+
throw new ApiResponseFormatError(
|
|
702
|
+
validationError instanceof Error ? validationError.message : 'Response format validation failed',
|
|
703
|
+
endpoint,
|
|
704
|
+
rawData
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
} catch (error) {
|
|
709
|
+
// Re-throw our custom errors as-is
|
|
710
|
+
if (error instanceof ApiResponseFormatError ||
|
|
711
|
+
error instanceof ApiBusinessLogicError ||
|
|
712
|
+
error instanceof ApiValidationError ||
|
|
713
|
+
error instanceof ApiNetworkError) {
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Wrap unknown errors as network errors
|
|
718
|
+
console.error('❌ API request failed:', error);
|
|
719
|
+
throw new ApiNetworkError(
|
|
720
|
+
error instanceof Error ? error.message : 'Network error',
|
|
721
|
+
0,
|
|
722
|
+
endpoint
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* CONVERTS VALIDATED STANDARDIZED RESPONSE TO CLIENT RESULT
|
|
729
|
+
* This normalizes the response for client consumption
|
|
730
|
+
*/
|
|
731
|
+
private convertToApiResult<TData>(validatedResponse: StandardizedResponse<any>): ApiResult<TData> {
|
|
732
|
+
if (isSuccessResponse(validatedResponse)) {
|
|
733
|
+
return {
|
|
734
|
+
success: true,
|
|
735
|
+
data: validatedResponse.data as TData,
|
|
736
|
+
message: validatedResponse.message,
|
|
737
|
+
operation_code: validatedResponse.operation_code,
|
|
738
|
+
timestamp: validatedResponse.timestamp
|
|
739
|
+
} as ApiSuccessResult<TData>;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (isPagedResponse(validatedResponse)) {
|
|
743
|
+
return {
|
|
744
|
+
success: true,
|
|
745
|
+
items: validatedResponse.data as TData[],
|
|
746
|
+
message: validatedResponse.message,
|
|
747
|
+
operation_code: validatedResponse.operation_code,
|
|
748
|
+
pagination: validatedResponse.pagination,
|
|
749
|
+
timestamp: validatedResponse.timestamp
|
|
750
|
+
} as ApiPagedResult<TData>;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (isErrorResponse(validatedResponse)) {
|
|
754
|
+
const reqId = (validatedResponse as any)?.request_id || (validatedResponse as any)?.requestId;
|
|
755
|
+
if (validatedResponse.error_code === 'VALIDATION_ERROR') {
|
|
756
|
+
// Handle validation error
|
|
757
|
+
const valError = validatedResponse as any;
|
|
758
|
+
return {
|
|
759
|
+
success: false,
|
|
760
|
+
error_code: validatedResponse.error_code,
|
|
761
|
+
message: validatedResponse.message,
|
|
762
|
+
operation: validatedResponse.operation,
|
|
763
|
+
details: validatedResponse.details,
|
|
764
|
+
validation_errors: valError.payload?.validation_errors,
|
|
765
|
+
...(reqId ? { request_id: reqId } : {})
|
|
766
|
+
} as ApiErrorResult;
|
|
767
|
+
} else {
|
|
768
|
+
// Handle regular error
|
|
769
|
+
return {
|
|
770
|
+
success: false,
|
|
771
|
+
error_code: validatedResponse.error_code,
|
|
772
|
+
message: validatedResponse.message,
|
|
773
|
+
operation: validatedResponse.operation,
|
|
774
|
+
details: validatedResponse.details,
|
|
775
|
+
...(reqId ? { request_id: reqId } : {})
|
|
776
|
+
} as ApiErrorResult;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// This should never happen due to validation, but TypeScript requires it
|
|
781
|
+
throw new ApiResponseFormatError('Unknown response type after validation', 'unknown', validatedResponse);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ========================================================================================
|
|
785
|
+
// HTTP METHOD WRAPPERS - PUBLIC API
|
|
786
|
+
// ========================================================================================
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* GET REQUEST - Returns typed result with direct data access
|
|
790
|
+
*/
|
|
791
|
+
async get<TData = unknown>(endpoint: string, sessionToken?: string): Promise<ApiResult<TData>> {
|
|
792
|
+
return this.makeRequest<TData>(endpoint, { method: 'GET' }, sessionToken);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* POST REQUEST - Returns typed result with direct data access
|
|
797
|
+
*/
|
|
798
|
+
async post<TData = unknown>(endpoint: string, data?: unknown, sessionToken?: string): Promise<ApiResult<TData>> {
|
|
799
|
+
return this.makeRequest<TData>(endpoint, {
|
|
800
|
+
method: 'POST',
|
|
801
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
802
|
+
}, sessionToken);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* PUT REQUEST - Returns typed result with direct data access
|
|
807
|
+
*/
|
|
808
|
+
async put<TData = unknown>(endpoint: string, data?: unknown, sessionToken?: string): Promise<ApiResult<TData>> {
|
|
809
|
+
return this.makeRequest<TData>(endpoint, {
|
|
810
|
+
method: 'PUT',
|
|
811
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
812
|
+
}, sessionToken);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* DELETE REQUEST - Returns typed result with direct data access
|
|
817
|
+
*/
|
|
818
|
+
async delete<TData = unknown>(endpoint: string): Promise<ApiResult<TData>> {
|
|
819
|
+
return this.makeRequest<TData>(endpoint, { method: 'DELETE' });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ========================================================================================
|
|
824
|
+
// SINGLETON INSTANCE - READY TO USE
|
|
825
|
+
// ========================================================================================
|
|
826
|
+
|
|
827
|
+
export const standardizedApi = new StandardizedClientApiService();
|
|
828
|
+
|
|
829
|
+
// ========================================================================================
|
|
830
|
+
// CONVENIENCE HELPER FUNCTIONS
|
|
831
|
+
// ========================================================================================
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* TYPE-SAFE SUCCESS CHECK
|
|
835
|
+
* Use this to check if API call was successful with proper type narrowing
|
|
836
|
+
*/
|
|
837
|
+
export function isApiSuccess<TData>(result: ApiResult<TData>): result is ApiSuccessResult<TData> {
|
|
838
|
+
return result.success === true && 'data' in result;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* TYPE-SAFE PAGED SUCCESS CHECK
|
|
843
|
+
* Use this to check if API call was successful paged response with proper type narrowing
|
|
844
|
+
*/
|
|
845
|
+
export function isApiPagedSuccess<TData>(result: ApiResult<TData>): result is ApiPagedResult<TData> {
|
|
846
|
+
return result.success === true && 'items' in result;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* TYPE-SAFE ERROR CHECK
|
|
851
|
+
* Use this to check if API call failed with proper type narrowing
|
|
852
|
+
*/
|
|
853
|
+
export function isApiError<TData>(result: ApiResult<TData>): result is ApiErrorResult {
|
|
854
|
+
return result.success === false;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* EXTRACT DATA FROM SUCCESS RESULT
|
|
859
|
+
* Use this to get the data from a successful API call
|
|
860
|
+
* Will throw if result is not successful
|
|
861
|
+
*/
|
|
862
|
+
export function extractApiData<TData>(result: ApiResult<TData>): TData {
|
|
863
|
+
if (isApiSuccess(result)) {
|
|
864
|
+
return result.data;
|
|
865
|
+
}
|
|
866
|
+
if (isApiPagedSuccess(result)) {
|
|
867
|
+
return result.items as TData;
|
|
868
|
+
}
|
|
869
|
+
throw new ApiBusinessLogicError(
|
|
870
|
+
result.error_code,
|
|
871
|
+
result.message,
|
|
872
|
+
result.operation,
|
|
873
|
+
result.details
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* EXTRACT ITEMS FROM PAGED SUCCESS RESULT
|
|
879
|
+
* Use this to get the items array from a successful paged API call
|
|
880
|
+
* Will throw if result is not successful paged response
|
|
881
|
+
*/
|
|
882
|
+
export function extractApiItems<TData>(result: ApiResult<TData>): TData[] {
|
|
883
|
+
if (isApiPagedSuccess(result)) {
|
|
884
|
+
return result.items as TData[];
|
|
885
|
+
}
|
|
886
|
+
if (isApiSuccess(result)) {
|
|
887
|
+
// If it's a regular success but expected paged, data should be array
|
|
888
|
+
return result.data as TData[];
|
|
889
|
+
}
|
|
890
|
+
throw new ApiBusinessLogicError(
|
|
891
|
+
result.error_code,
|
|
892
|
+
result.message,
|
|
893
|
+
result.operation,
|
|
894
|
+
result.details
|
|
895
|
+
);
|
|
896
|
+
}
|