@payez/next-mvp 4.8.1 → 4.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/POST-INSTALL.md +408 -408
  2. package/README.md +859 -859
  3. package/dist/api-handlers/admin/audit.js +13 -21
  4. package/dist/api-handlers/admin/sessions.js +59 -45
  5. package/dist/api-handlers/admin/stats.js +1 -1
  6. package/dist/api-handlers/admin/users.js +15 -23
  7. package/dist/api-handlers/admin/vibe-data.d.ts +5 -0
  8. package/dist/api-handlers/admin/vibe-data.js +5 -0
  9. package/dist/components/account/MobileNavDrawer.js +18 -18
  10. package/dist/components/account/UserAvatarMenu.js +1 -1
  11. package/dist/components/admin/AlertSettingsTab.js +40 -40
  12. package/dist/components/admin/LoggingSettingsTab.js +28 -28
  13. package/dist/lib/session-store.js +21 -21
  14. package/dist/lib/startup-init.js +19 -19
  15. package/dist/pages/login/page.js +28 -28
  16. package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
  17. package/dist/vibe/client.d.ts +1 -1
  18. package/dist/vibe/client.js +2 -1
  19. package/dist/vibe/filters.d.ts +36 -0
  20. package/dist/vibe/filters.js +47 -0
  21. package/dist/vibe/generic.js +2 -1
  22. package/dist/vibe/sessions.js +28 -3
  23. package/package.json +2 -2
  24. package/scripts/check-publish-eol.mjs +33 -15
  25. package/src/api/auth-handler.ts +550 -550
  26. package/src/api/index.ts +17 -17
  27. package/src/api-handlers/account/change-password.ts +170 -170
  28. package/src/api-handlers/account/masked-info.ts +46 -46
  29. package/src/api-handlers/account/profile.ts +99 -99
  30. package/src/api-handlers/account/recovery/forward.ts +117 -117
  31. package/src/api-handlers/account/recovery/initiate.ts +59 -59
  32. package/src/api-handlers/account/recovery/send-code.ts +21 -21
  33. package/src/api-handlers/account/recovery/verify-code.ts +20 -20
  34. package/src/api-handlers/account/reset-password.ts +87 -87
  35. package/src/api-handlers/account/send-code.ts +70 -70
  36. package/src/api-handlers/account/update-phone.ts +84 -84
  37. package/src/api-handlers/account/validate-password.ts +139 -139
  38. package/src/api-handlers/account/verify-email.ts +134 -134
  39. package/src/api-handlers/account/verify-sms.ts +134 -134
  40. package/src/api-handlers/admin/analytics.ts +452 -452
  41. package/src/api-handlers/admin/audit.ts +223 -232
  42. package/src/api-handlers/admin/index.ts +63 -63
  43. package/src/api-handlers/admin/redis-sessions.ts +259 -259
  44. package/src/api-handlers/admin/sessions.ts +341 -327
  45. package/src/api-handlers/admin/site-logs.ts +388 -388
  46. package/src/api-handlers/admin/stats.ts +260 -260
  47. package/src/api-handlers/admin/users.ts +240 -251
  48. package/src/api-handlers/admin/vibe-data.ts +338 -333
  49. package/src/api-handlers/anon/preferences.ts +123 -123
  50. package/src/api-handlers/auth/credential-login-email-code.ts +92 -92
  51. package/src/api-handlers/auth/credential-login.ts +948 -948
  52. package/src/api-handlers/auth/jwks.ts +31 -31
  53. package/src/api-handlers/auth/login.ts +252 -252
  54. package/src/api-handlers/auth/passkey-signin.ts +116 -116
  55. package/src/api-handlers/auth/refresh.ts +829 -829
  56. package/src/api-handlers/auth/signout.ts +201 -201
  57. package/src/api-handlers/auth/status.ts +23 -23
  58. package/src/api-handlers/auth/update-session.ts +144 -144
  59. package/src/api-handlers/auth/validate.ts +45 -45
  60. package/src/api-handlers/auth/verify-code.ts +116 -116
  61. package/src/api-handlers/mfa/backup-code-verify.ts +170 -170
  62. package/src/api-handlers/mfa/backup-codes.ts +42 -42
  63. package/src/api-handlers/mfa/methods.ts +47 -47
  64. package/src/api-handlers/mfa/totp-confirm.ts +43 -43
  65. package/src/api-handlers/mfa/totp-enroll.ts +43 -43
  66. package/src/api-handlers/mfa/totp-verify.ts +172 -172
  67. package/src/api-handlers/oauth/resume.ts +182 -182
  68. package/src/api-handlers/onboarding/send-email-code.ts +18 -18
  69. package/src/api-handlers/onboarding/set-password.ts +21 -21
  70. package/src/api-handlers/onboarding/verify-code.ts +20 -20
  71. package/src/api-handlers/onboarding/verify-welcome-email.ts +17 -17
  72. package/src/api-handlers/passkeys/list.ts +32 -32
  73. package/src/api-handlers/passkeys/manage.ts +55 -55
  74. package/src/api-handlers/passkeys/register-options.ts +43 -43
  75. package/src/api-handlers/passkeys/register-verify.ts +42 -42
  76. package/src/api-handlers/session/refresh-viability.ts +56 -56
  77. package/src/api-handlers/session/viability.ts +238 -238
  78. package/src/api-handlers/test/force-expire.ts +167 -167
  79. package/src/auth/auth-decision.ts +292 -292
  80. package/src/auth/better-auth.ts +1688 -1688
  81. package/src/auth/idp-oauth.ts +310 -310
  82. package/src/auth/password-reset-bridge.ts +98 -98
  83. package/src/auth/revocation-fields.ts +16 -16
  84. package/src/auth/route-config.ts +236 -236
  85. package/src/auth/safe-callback.ts +199 -199
  86. package/src/auth/session-cookie-clear.ts +30 -30
  87. package/src/auth/session-reset-link.ts +159 -159
  88. package/src/auth/session-response-sanitizer.ts +151 -151
  89. package/src/auth/types/auth-types.ts +555 -555
  90. package/src/auth/types/index.ts +7 -7
  91. package/src/auth/unauthenticated-routes.ts +2 -2
  92. package/src/auth/utils/idp-client.ts +364 -364
  93. package/src/auth/utils/index.ts +6 -6
  94. package/src/auth/utils/token-utils.ts +243 -243
  95. package/src/client/AuthContext.tsx +192 -192
  96. package/src/client/better-auth-client.ts +70 -70
  97. package/src/client/fetch-with-auth.ts +47 -47
  98. package/src/client/fetchWithSession.ts +21 -21
  99. package/src/client/index.ts +13 -13
  100. package/src/client/useAnonSession.ts +131 -131
  101. package/src/components/SessionSync.tsx +138 -138
  102. package/src/components/SignalRHealthCheck.tsx +131 -131
  103. package/src/components/account/MobileNavDrawer.tsx +305 -305
  104. package/src/components/account/UserAvatarMenu.tsx +277 -277
  105. package/src/components/account/index.ts +13 -13
  106. package/src/components/admin/AlertSettingsTab.tsx +728 -728
  107. package/src/components/admin/AnalyticsTab.tsx +703 -703
  108. package/src/components/admin/DataBrowserTab.tsx +508 -508
  109. package/src/components/admin/LoggingSettingsTab.tsx +665 -665
  110. package/src/components/admin/SessionsTab.tsx +421 -421
  111. package/src/components/admin/StatsTab.tsx +379 -379
  112. package/src/components/admin/VibeAdminContext.tsx +87 -87
  113. package/src/components/admin/VibeAdminLayout.tsx +187 -187
  114. package/src/components/admin/index.ts +59 -59
  115. package/src/components/auth/CredentialSignInForm.tsx +240 -240
  116. package/src/components/auth/FederatedAuthSection.tsx +129 -129
  117. package/src/components/auth/ModeAwareLoginPage.tsx +146 -146
  118. package/src/components/auth/ModeAwareSignupPage.tsx +200 -200
  119. package/src/components/auth/PasswordResetBridge.tsx +30 -30
  120. package/src/components/auth/TraditionalAuthSection.tsx +103 -103
  121. package/src/components/recovery/CompleteStep.tsx +36 -36
  122. package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
  123. package/src/components/recovery/RequestedStep.tsx +30 -30
  124. package/src/components/recovery/SelectMethodStep.tsx +73 -73
  125. package/src/components/recovery/SetPasswordStep.tsx +118 -118
  126. package/src/components/recovery/VerifyCodeStep.tsx +90 -90
  127. package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
  128. package/src/components/reserved/ReservedStatusBox.tsx +118 -118
  129. package/src/components/ui/BetaBadge.tsx +58 -58
  130. package/src/components/ui/Footer.tsx +93 -93
  131. package/src/config/env.ts +55 -55
  132. package/src/config/logger.ts +62 -62
  133. package/src/config/logging-config.ts +82 -82
  134. package/src/config/unauthenticated-routes.ts +19 -19
  135. package/src/config/vibe-log-transport.ts +264 -264
  136. package/src/edge/internal-api-url.ts +65 -65
  137. package/src/edge/middleware.ts +42 -42
  138. package/src/hooks/useAuth.ts +117 -117
  139. package/src/hooks/useAuthSettings.ts +109 -109
  140. package/src/hooks/useAvailableProviders.ts +143 -143
  141. package/src/hooks/usePasswordValidation.ts +127 -127
  142. package/src/hooks/useProfile.ts +82 -82
  143. package/src/hooks/usePublicAuthSettings.ts +171 -171
  144. package/src/hooks/useSessionExpiration.ts +101 -101
  145. package/src/hooks/useViabilitySession.ts +337 -337
  146. package/src/index.ts +64 -64
  147. package/src/lib/anon-session.ts +213 -213
  148. package/src/lib/api-handler.ts +643 -643
  149. package/src/lib/app-slug.ts +228 -228
  150. package/src/lib/authorize-resume.ts +172 -172
  151. package/src/lib/bearer-fetch.ts +31 -31
  152. package/src/lib/client-key.ts +88 -88
  153. package/src/lib/code-step-copy.ts +83 -83
  154. package/src/lib/credential-sign-in-client.ts +101 -101
  155. package/src/lib/demo-mode.ts +18 -18
  156. package/src/lib/device-id.ts +67 -67
  157. package/src/lib/ensure-fresh-access-token.ts +509 -509
  158. package/src/lib/geolocation.ts +265 -265
  159. package/src/lib/idp-client-config.ts +1005 -1005
  160. package/src/lib/idp-fetch.ts +169 -169
  161. package/src/lib/idp-url-guard.ts +37 -37
  162. package/src/lib/internal-api.ts +171 -171
  163. package/src/lib/jwt-decode-client.ts +45 -45
  164. package/src/lib/jwt-decode.ts +82 -82
  165. package/src/lib/lock-window.ts +49 -49
  166. package/src/lib/login-error-message.ts +41 -41
  167. package/src/lib/never-in-response.ts +64 -64
  168. package/src/lib/nextauth-secret.ts +168 -168
  169. package/src/lib/onboarding-proxy.ts +57 -57
  170. package/src/lib/password-rules.ts +50 -50
  171. package/src/lib/rate-limit-service.ts +9 -9
  172. package/src/lib/redis.ts +105 -105
  173. package/src/lib/refresh-token-validator.ts +64 -64
  174. package/src/lib/roles.ts +177 -177
  175. package/src/lib/safe-error-response.ts +149 -149
  176. package/src/lib/secret-validation.ts +8 -8
  177. package/src/lib/session-cookie-shape.ts +185 -185
  178. package/src/lib/session-store.ts +1092 -1092
  179. package/src/lib/session.ts +34 -34
  180. package/src/lib/single-flight.ts +64 -64
  181. package/src/lib/site-logger.ts +245 -245
  182. package/src/lib/standardized-client-api.ts +901 -901
  183. package/src/lib/startup-init.ts +276 -276
  184. package/src/lib/test-aware-get-token.ts +41 -41
  185. package/src/lib/token-expiry.ts +40 -40
  186. package/src/lib/token-lifecycle.ts +546 -546
  187. package/src/lib/token-log-id.ts +69 -69
  188. package/src/lib/twofa-rejection.ts +91 -91
  189. package/src/lib/types/api-responses.ts +336 -336
  190. package/src/lib/user-agent-parser.ts +252 -252
  191. package/src/logging/api/admin-analytics.ts +51 -51
  192. package/src/logging/api/audit-log.ts +53 -53
  193. package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
  194. package/src/logging/components/AuditLogViewer.tsx +125 -125
  195. package/src/logging/components/ErrorMetricsCard.tsx +98 -98
  196. package/src/logging/components/HealthMetricsCard.tsx +70 -70
  197. package/src/logging/hooks/useAdminAnalytics.ts +22 -22
  198. package/src/logging/hooks/useAuditLog.ts +24 -24
  199. package/src/logging/hooks/useErrorMetrics.ts +40 -40
  200. package/src/logging/hooks/useHealthMetrics.ts +44 -44
  201. package/src/logging/index.ts +18 -18
  202. package/src/logging/types/analytics.ts +81 -81
  203. package/src/logging/types/audit.ts +31 -31
  204. package/src/logging/types/index.ts +3 -3
  205. package/src/middleware/create-middleware.ts +1141 -1141
  206. package/src/middleware/rbac-check.ts +279 -279
  207. package/src/middleware/twofa-presets.ts +224 -224
  208. package/src/models/DecodedAccessToken.ts +16 -16
  209. package/src/models/SessionModel.ts +331 -331
  210. package/src/pages/admin-login/page.tsx +111 -111
  211. package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
  212. package/src/pages/admin-page-permissions/index.ts +7 -7
  213. package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
  214. package/src/pages/admin-roles/index.ts +9 -9
  215. package/src/pages/admin-roles/modals.tsx +469 -469
  216. package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
  217. package/src/pages/client-admin/index.ts +33 -33
  218. package/src/pages/coming-soon/page.tsx +83 -83
  219. package/src/pages/login/page.tsx +338 -338
  220. package/src/pages/onboarding/index.ts +11 -11
  221. package/src/pages/onboarding/page.tsx +212 -212
  222. package/src/pages/profile/EnhancedProfilePage.tsx +514 -514
  223. package/src/pages/profile/index.ts +9 -9
  224. package/src/pages/profile/page.tsx +166 -166
  225. package/src/pages/recovery/page.tsx +267 -267
  226. package/src/pages/roles/MyRolesPage.tsx +211 -211
  227. package/src/pages/roles/components.tsx +294 -294
  228. package/src/pages/roles/index.ts +17 -17
  229. package/src/pages/security/EnhancedSecurityPage.tsx +660 -660
  230. package/src/pages/security/index.ts +12 -12
  231. package/src/pages/security/page.tsx +507 -507
  232. package/src/pages/service-unavailable/page.tsx +63 -63
  233. package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
  234. package/src/pages/settings/index.ts +9 -9
  235. package/src/pages/settings/page.tsx +47 -47
  236. package/src/pages/showcase/ShowcasePage.tsx +532 -532
  237. package/src/pages/showcase/index.ts +13 -13
  238. package/src/pages/test-env/EmergencyLogoutPage.tsx +192 -192
  239. package/src/pages/test-env/JwtInspectPage.tsx +449 -449
  240. package/src/pages/test-env/RefreshTokenPage.tsx +178 -178
  241. package/src/pages/test-env/TestEnvPage.tsx +130 -130
  242. package/src/pages/test-env/guarded.tsx +81 -81
  243. package/src/pages/test-env/index.ts +77 -77
  244. package/src/pages/test-env/redact-credentials.ts +96 -96
  245. package/src/pages/verify-code/page.tsx +746 -746
  246. package/src/routes/account/index.ts +31 -31
  247. package/src/routes/account/masked-info.ts +36 -36
  248. package/src/routes/account/send-code.ts +39 -39
  249. package/src/routes/account/update-phone.ts +13 -13
  250. package/src/routes/account/verify-email.ts +40 -40
  251. package/src/routes/account/verify-sms.ts +40 -40
  252. package/src/routes/auth/credential-login-email-code.ts +6 -6
  253. package/src/routes/auth/credential-login.ts +6 -6
  254. package/src/routes/auth/index.ts +22 -22
  255. package/src/routes/auth/logout.ts +111 -111
  256. package/src/routes/auth/nextauth.ts +45 -45
  257. package/src/routes/auth/passkey-signin.ts +13 -13
  258. package/src/routes/auth/refresh.ts +77 -77
  259. package/src/routes/auth/session-reset.ts +37 -37
  260. package/src/routes/auth/session.ts +261 -261
  261. package/src/routes/auth/settings.ts +181 -181
  262. package/src/routes/auth/viability.ts +519 -519
  263. package/src/routes/index.ts +17 -17
  264. package/src/routes/mfa/backup-code-verify.ts +1 -1
  265. package/src/routes/mfa/backup-codes.ts +10 -10
  266. package/src/routes/mfa/methods.ts +22 -22
  267. package/src/routes/mfa/totp-confirm.ts +10 -10
  268. package/src/routes/mfa/totp-enroll.ts +10 -10
  269. package/src/routes/mfa/totp-verify.ts +14 -14
  270. package/src/routes/onboarding/index.ts +15 -15
  271. package/src/routes/onboarding/send-email-code.ts +2 -2
  272. package/src/routes/onboarding/set-password.ts +2 -2
  273. package/src/routes/onboarding/verify-code.ts +2 -2
  274. package/src/routes/onboarding/verify-welcome-email.ts +2 -2
  275. package/src/routes/passkeys/list.ts +10 -10
  276. package/src/routes/passkeys/manage.ts +23 -23
  277. package/src/routes/passkeys/register-options.ts +10 -10
  278. package/src/routes/passkeys/register-verify.ts +10 -10
  279. package/src/routes/session/index.ts +7 -7
  280. package/src/routes/session/refresh-viability.ts +17 -17
  281. package/src/server/auth-guard.ts +203 -203
  282. package/src/server/auth.ts +486 -486
  283. package/src/server/decode-session.ts +309 -309
  284. package/src/server/slim-middleware.ts +117 -117
  285. package/src/server/verified-session-token.ts +103 -103
  286. package/src/server/with-auth.ts +93 -93
  287. package/src/services/signalrActivityService.ts +258 -258
  288. package/src/stores/authStore.ts +1900 -1900
  289. package/src/templates/instrumentation.ts +60 -60
  290. package/src/theme/ThemeProvider.tsx +39 -39
  291. package/src/theme/default.ts +33 -33
  292. package/src/theme/index.ts +31 -31
  293. package/src/theme/types.ts +69 -69
  294. package/src/theme/useTheme.ts +57 -57
  295. package/src/theme/utils.ts +40 -40
  296. package/src/types/api.ts +12 -12
  297. package/src/types/auth.d.ts +23 -23
  298. package/src/types/auth.ts +48 -48
  299. package/src/types/logging.ts +11 -11
  300. package/src/types/recovery.ts +54 -54
  301. package/src/utils/api.ts +401 -401
  302. package/src/utils/circuitBreaker.ts +40 -40
  303. package/src/utils/error-message.ts +108 -108
  304. package/src/utils/layout/reservedSpace.ts +124 -124
  305. package/src/utils/logout.ts +30 -30
  306. package/src/vibe/client.ts +878 -877
  307. package/src/vibe/enterprise-auth.ts +208 -208
  308. package/src/vibe/errors.ts +209 -209
  309. package/src/vibe/filters.ts +52 -0
  310. package/src/vibe/generic.ts +340 -339
  311. package/src/vibe/hooks/index.ts +367 -367
  312. package/src/vibe/index.ts +133 -133
  313. package/src/vibe/sessions.ts +925 -899
  314. package/src/vibe/types.ts +577 -577
@@ -1,276 +1,276 @@
1
- /**
2
- * Startup Initialization for MVP
3
- *
4
- * This module ensures that critical initialization tasks are completed
5
- * before the application serves requests.
6
- *
7
- * Uses unified IDP client config for:
8
- * - BETTER_AUTH_SECRET (the Better Auth signing secret)
9
- * - OAuth provider configuration
10
- * - Auth settings (2FA, session timeouts, etc.)
11
- */
12
-
13
- import 'server-only';
14
- import { getIDPClientConfig, clearConfigRedisCache, type IDPClientConfig } from './idp-client-config';
15
- import { slugStateReason } from './app-slug';
16
-
17
- let initializationStarted = false;
18
- let initializationComplete = false;
19
- let initializationFailed = false;
20
- let initializationPromise: Promise<void> | null = null;
21
- let lastInitError: Error | null = null;
22
-
23
- // Cached IDP config for access after initialization
24
- let cachedIDPConfig: IDPClientConfig | null = null;
25
-
26
- // Startup backoff to prevent pod restart storms from hammering IDP
27
- let lastStartupAttemptTime = 0;
28
- const STARTUP_BACKOFF_MS = 30000; // 30 seconds between startup attempts after failure
29
-
30
- /**
31
- * Initialize the application startup sequence (async)
32
- * Handles async initialization like fetching secrets from IDP
33
- */
34
- export async function ensureInitialized(): Promise<void> {
35
- // If already initialized, return immediately
36
- if (initializationComplete) {
37
- return;
38
- }
39
-
40
- // If initialization is in progress, wait for it
41
- if (initializationPromise) {
42
- return initializationPromise;
43
- }
44
-
45
- // Prevent hammering IDP on rapid pod restarts
46
- const now = Date.now();
47
- if (initializationFailed && (now - lastStartupAttemptTime) < STARTUP_BACKOFF_MS) {
48
- const remainingMs = STARTUP_BACKOFF_MS - (now - lastStartupAttemptTime);
49
- console.warn('[STARTUP] In backoff period after previous failure, skipping IDP call', {
50
- remainingMs: Math.round(remainingMs),
51
- lastError: lastInitError?.message
52
- });
53
- // Re-throw last error so callers know we're still in failed state
54
- throw lastInitError || new Error('Initialization in backoff period');
55
- }
56
-
57
- // Track this attempt time
58
- lastStartupAttemptTime = now;
59
-
60
- // Mark as started
61
- initializationStarted = true;
62
-
63
- // Start initialization.
64
- //
65
- // PAY-642: a FAILED attempt must not stay installed. This used to be
66
- // `initializationPromise = performInitialization(); await initializationPromise;` with no reset, and the
67
- // in-progress check above runs BEFORE the backoff check, so a pod that booted while the IdP was down replayed
68
- // its first rejection to every caller forever: the 30 s backoff could never lead to a retry. Same latch, same
69
- // fix, as createSingleFlight (lib/single-flight.ts): callers of the failing attempt still see the rejection
70
- // (loud), then the attempt clears itself, identity-guarded so a slow failure never evicts a newer attempt.
71
- // After a failure the backoff check above governs when the next attempt may start.
72
- const attempt = performInitialization();
73
- initializationPromise = attempt;
74
- attempt.catch(() => {
75
- if (initializationPromise === attempt) initializationPromise = null;
76
- });
77
- await attempt;
78
- }
79
-
80
- /**
81
- * Synchronously log startup status
82
- * Can be called before async initialization is complete
83
- */
84
- export function logStartupStatus(): void {
85
- if (!initializationStarted) {
86
- console.log('\n');
87
- console.log('╔══════════════════════════════════════════════════════════════╗');
88
- console.log('ā•‘ šŸš€ PayEz Next MVP - Starting Up ā•‘');
89
- console.log('ā•‘ ā•‘');
90
- console.log('ā•‘ Async initialization in progress... ā•‘');
91
- console.log('ā•‘ - Resolving BETTER_AUTH_SECRET from IDP ā•‘');
92
- console.log('ā•‘ - Verifying environment configuration ā•‘');
93
- console.log('ā•‘ ā•‘');
94
- console.log('ā•‘ Check logs below for detailed initialization status: ā•‘');
95
- console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
96
- console.log('');
97
- } else if (initializationComplete) {
98
- console.log('\n');
99
- console.log('╔══════════════════════════════════════════════════════════════╗');
100
- console.log('ā•‘ ✨ PayEz Next MVP Ready for Requests ✨ ā•‘');
101
- console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
102
- console.log('');
103
- } else if (lastInitError) {
104
- console.log('\n');
105
- console.log('╔══════════════════════════════════════════════════════════════╗');
106
- console.log('ā•‘ āš ļø Startup error detected - initialization may still retry ā•‘');
107
- console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
108
- console.log('');
109
- }
110
- }
111
-
112
- async function performInitialization(): Promise<void> {
113
- console.log('\n');
114
- console.log('╔══════════════════════════════════════════════════════════════╗');
115
- console.log('ā•‘ PayEz Next MVP - Async Startup ā•‘');
116
- console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
117
- console.log('');
118
-
119
- try {
120
- // Step 1: Fetch full client config from IDP (includes secret, providers, settings)
121
- console.log('[STARTUP] Step 1/2: Fetching client config from IDP...');
122
-
123
- // Clear any stale Redis cache so startup always gets fresh IDP data
124
- await clearConfigRedisCache();
125
-
126
- try {
127
- const config = await getIDPClientConfig(true);
128
- cachedIDPConfig = config;
129
-
130
- console.log('[STARTUP] Client config loaded successfully');
131
- console.log('[STARTUP] - Client ID:', config.clientId);
132
- console.log('[STARTUP] - Client Slug:', config.clientSlug);
133
- console.log('[STARTUP] - Secret length:', config.authSecret?.length || 0, 'chars');
134
- console.log('[STARTUP] - OAuth Providers:', config.oauthProviders?.filter(p => p.enabled).map(p => p.provider).join(', ') || 'none');
135
- console.log('[STARTUP] - Require 2FA:', config.authSettings?.require2FA);
136
- console.log('[STARTUP] - Cache TTL:', config.configCacheTtlSeconds, 'seconds');
137
- console.log('[STARTUP] - Base Client URL:', config.baseClientUrl || '(not set)');
138
-
139
- // Set BETTER_AUTH_SECRET from IDP response if not already set.
140
- // Also mirror to legacy NEXTAUTH_SECRET during the rename transition
141
- // so any consumer code still reading the old name keeps working.
142
- if (config.authSecret && !process.env.BETTER_AUTH_SECRET) {
143
- process.env.BETTER_AUTH_SECRET = config.authSecret;
144
- console.log('[STARTUP] Set BETTER_AUTH_SECRET from IDP config');
145
- }
146
- if (config.authSecret && !process.env.NEXTAUTH_SECRET) {
147
- process.env.NEXTAUTH_SECRET = config.authSecret;
148
- }
149
- } catch (error) {
150
- const errorMsg = error instanceof Error ? error.message : String(error);
151
- console.error('[STARTUP] IDP config fetch failed:', errorMsg);
152
-
153
- // No fallback available — IDP config is the only source for the auth secret
154
- console.error('[STARTUP] No fallback available for auth secret resolution');
155
- }
156
-
157
- // Step 2: Verify BETTER_AUTH_SECRET is available - FAIL FAST if not
158
- console.log('[STARTUP] Step 2/2: Verifying BETTER_AUTH_SECRET...');
159
-
160
- const secret = process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET;
161
- if (!secret || secret.trim() === '') {
162
- console.error('');
163
- console.error('╔══════════════════════════════════════════════════════════════╗');
164
- console.error('ā•‘ āŒ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ā•‘');
165
- console.error('ā•‘ ā•‘');
166
- console.error('ā•‘ The app cannot start without a valid auth signing secret. ā•‘');
167
- console.error('ā•‘ This should be fetched from IDP at startup. ā•‘');
168
- console.error('ā•‘ ā•‘');
169
- console.error('ā•‘ Possible causes: ā•‘');
170
- console.error('ā•‘ • IDP is not running or unreachable ā•‘');
171
- console.error('ā•‘ • CLIENT_ID is not registered in IDP ā•‘');
172
- console.error('ā•‘ • IDP_URL is incorrect ā•‘');
173
- console.error('ā•‘ • Network connectivity issue ā•‘');
174
- console.error('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
175
- console.error('');
176
- throw new Error('FATAL: BETTER_AUTH_SECRET not available - cannot start without valid secret from IDP');
177
- }
178
-
179
- console.log('[STARTUP] BETTER_AUTH_SECRET verified (' + secret.length + ' chars)');
180
-
181
- // Step 3: Validate cookie name consistency
182
- // This catches bugs where getJwtCookieName() returns a different name than
183
- // what auth-options.ts configures, which causes sessions to fail in production
184
- const { validateCookieNameConsistency, getSessionCookieName, assertAppSlugMatchesIdp, assertIdpSlugKnown } = await import('./app-slug');
185
- validateCookieNameConsistency();
186
- // rigpert 60743: refuse to run with two cookie-name sources (IdP clientSlug vs APP_SLUG/CLIENT_ID).
187
- assertIdpSlugKnown(cachedIDPConfig?.clientSlug);
188
- assertAppSlugMatchesIdp(cachedIDPConfig?.clientSlug);
189
- console.log('[STARTUP] Cookie name consistency validated:', getSessionCookieName());
190
-
191
- // All done
192
- console.log('');
193
- console.log('╔══════════════════════════════════════════════════════════════╗');
194
- console.log('ā•‘ PayEz Next MVP Ready for Requests ā•‘');
195
- console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
196
- console.log('');
197
-
198
- initializationComplete = true;
199
- initializationFailed = false;
200
- lastInitError = null;
201
- } catch (error) {
202
- lastInitError = error instanceof Error ? error : new Error(String(error));
203
- initializationFailed = true;
204
-
205
- const errorMsg = lastInitError.message || 'Unknown error';
206
- const isConnectionError = errorMsg.includes('fetch failed') || errorMsg.includes('ECONNREFUSED');
207
- const idpUrl = (process.env.IDP_URL || 'NOT SET').padEnd(46);
208
- const clientId = (process.env.CLIENT_ID || 'NOT SET').padEnd(43);
209
-
210
- const connectionLine = isConnectionError
211
- ? 'ā•‘ šŸ”Œ CONNECTION REFUSED - IDP appears to be down ā•‘\nā•‘ ā•‘\n'
212
- : '';
213
-
214
- console.error(`
215
- ╔══════════════════════════════════════════════════════════════╗
216
- ā•‘ āŒ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ā•‘
217
- ā•‘ ā•‘
218
- ā•‘ The app cannot start without a valid auth signing secret. ā•‘
219
- ā•‘ This should be fetched from IDP at startup. ā•‘
220
- ā•‘ ā•‘
221
- ${connectionLine}ā•‘ Possible causes: ā•‘
222
- ā•‘ • IDP is not running or unreachable ā•‘
223
- ā•‘ • CLIENT_ID is not registered in IDP ā•‘
224
- ā•‘ • IDP_URL is incorrect ā•‘
225
- ā•‘ • Network connectivity issue ā•‘
226
- ā•‘ ā•‘
227
- ā•‘ Current config: ā•‘
228
- ā•‘ • IDP_URL: ${idpUrl}ā•‘
229
- ā•‘ • CLIENT_ID: ${clientId}ā•‘
230
- ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
231
-
232
- [STARTUP] Error: ${errorMsg}
233
- `);
234
-
235
- // Re-throw so callers know initialization failed
236
- throw lastInitError;
237
- }
238
- }
239
-
240
- /**
241
- * Get the cached IDP config after initialization.
242
- * Returns null if not yet initialized.
243
- */
244
- /**
245
- * For health endpoints (rigpert 60765): a guard that does not stop the process is an empty red unless the mismatch is visible
246
- * where operators and promote-fe look. Returns `SLUG_MISMATCH: cookie=<idp slug> gate=<app slug>` while the drift holds, else null;
247
- * a health route should answer non-200 with this as the reason. Null until the IdP config has been fetched.
248
- */
249
- export function getSlugMismatchReason(): string | null {
250
- return slugStateReason(cachedIDPConfig?.clientSlug, initializationComplete || initializationFailed);
251
- }
252
-
253
- export function getStartupIDPConfig(): IDPClientConfig | null {
254
- return cachedIDPConfig;
255
- }
256
-
257
- /**
258
- * Check if initialization failed (auth signing secret couldn't be retrieved)
259
- */
260
- export function isInitializationFailed(): boolean {
261
- return initializationFailed;
262
- }
263
-
264
- /**
265
- * Get the last initialization error
266
- */
267
- export function getInitializationError(): Error | null {
268
- return lastInitError;
269
- }
270
-
271
- /**
272
- * Check if the app is ready to handle auth requests
273
- */
274
- export function isAppReady(): boolean {
275
- return initializationComplete && !initializationFailed;
276
- }
1
+ /**
2
+ * Startup Initialization for MVP
3
+ *
4
+ * This module ensures that critical initialization tasks are completed
5
+ * before the application serves requests.
6
+ *
7
+ * Uses unified IDP client config for:
8
+ * - BETTER_AUTH_SECRET (the Better Auth signing secret)
9
+ * - OAuth provider configuration
10
+ * - Auth settings (2FA, session timeouts, etc.)
11
+ */
12
+
13
+ import 'server-only';
14
+ import { getIDPClientConfig, clearConfigRedisCache, type IDPClientConfig } from './idp-client-config';
15
+ import { slugStateReason } from './app-slug';
16
+
17
+ let initializationStarted = false;
18
+ let initializationComplete = false;
19
+ let initializationFailed = false;
20
+ let initializationPromise: Promise<void> | null = null;
21
+ let lastInitError: Error | null = null;
22
+
23
+ // Cached IDP config for access after initialization
24
+ let cachedIDPConfig: IDPClientConfig | null = null;
25
+
26
+ // Startup backoff to prevent pod restart storms from hammering IDP
27
+ let lastStartupAttemptTime = 0;
28
+ const STARTUP_BACKOFF_MS = 30000; // 30 seconds between startup attempts after failure
29
+
30
+ /**
31
+ * Initialize the application startup sequence (async)
32
+ * Handles async initialization like fetching secrets from IDP
33
+ */
34
+ export async function ensureInitialized(): Promise<void> {
35
+ // If already initialized, return immediately
36
+ if (initializationComplete) {
37
+ return;
38
+ }
39
+
40
+ // If initialization is in progress, wait for it
41
+ if (initializationPromise) {
42
+ return initializationPromise;
43
+ }
44
+
45
+ // Prevent hammering IDP on rapid pod restarts
46
+ const now = Date.now();
47
+ if (initializationFailed && (now - lastStartupAttemptTime) < STARTUP_BACKOFF_MS) {
48
+ const remainingMs = STARTUP_BACKOFF_MS - (now - lastStartupAttemptTime);
49
+ console.warn('[STARTUP] In backoff period after previous failure, skipping IDP call', {
50
+ remainingMs: Math.round(remainingMs),
51
+ lastError: lastInitError?.message
52
+ });
53
+ // Re-throw last error so callers know we're still in failed state
54
+ throw lastInitError || new Error('Initialization in backoff period');
55
+ }
56
+
57
+ // Track this attempt time
58
+ lastStartupAttemptTime = now;
59
+
60
+ // Mark as started
61
+ initializationStarted = true;
62
+
63
+ // Start initialization.
64
+ //
65
+ // PAY-642: a FAILED attempt must not stay installed. This used to be
66
+ // `initializationPromise = performInitialization(); await initializationPromise;` with no reset, and the
67
+ // in-progress check above runs BEFORE the backoff check, so a pod that booted while the IdP was down replayed
68
+ // its first rejection to every caller forever: the 30 s backoff could never lead to a retry. Same latch, same
69
+ // fix, as createSingleFlight (lib/single-flight.ts): callers of the failing attempt still see the rejection
70
+ // (loud), then the attempt clears itself, identity-guarded so a slow failure never evicts a newer attempt.
71
+ // After a failure the backoff check above governs when the next attempt may start.
72
+ const attempt = performInitialization();
73
+ initializationPromise = attempt;
74
+ attempt.catch(() => {
75
+ if (initializationPromise === attempt) initializationPromise = null;
76
+ });
77
+ await attempt;
78
+ }
79
+
80
+ /**
81
+ * Synchronously log startup status
82
+ * Can be called before async initialization is complete
83
+ */
84
+ export function logStartupStatus(): void {
85
+ if (!initializationStarted) {
86
+ console.log('\n');
87
+ console.log('╔══════════════════════════════════════════════════════════════╗');
88
+ console.log('ā•‘ šŸš€ PayEz Next MVP - Starting Up ā•‘');
89
+ console.log('ā•‘ ā•‘');
90
+ console.log('ā•‘ Async initialization in progress... ā•‘');
91
+ console.log('ā•‘ - Resolving BETTER_AUTH_SECRET from IDP ā•‘');
92
+ console.log('ā•‘ - Verifying environment configuration ā•‘');
93
+ console.log('ā•‘ ā•‘');
94
+ console.log('ā•‘ Check logs below for detailed initialization status: ā•‘');
95
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
96
+ console.log('');
97
+ } else if (initializationComplete) {
98
+ console.log('\n');
99
+ console.log('╔══════════════════════════════════════════════════════════════╗');
100
+ console.log('ā•‘ ✨ PayEz Next MVP Ready for Requests ✨ ā•‘');
101
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
102
+ console.log('');
103
+ } else if (lastInitError) {
104
+ console.log('\n');
105
+ console.log('╔══════════════════════════════════════════════════════════════╗');
106
+ console.log('ā•‘ āš ļø Startup error detected - initialization may still retry ā•‘');
107
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
108
+ console.log('');
109
+ }
110
+ }
111
+
112
+ async function performInitialization(): Promise<void> {
113
+ console.log('\n');
114
+ console.log('╔══════════════════════════════════════════════════════════════╗');
115
+ console.log('ā•‘ PayEz Next MVP - Async Startup ā•‘');
116
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
117
+ console.log('');
118
+
119
+ try {
120
+ // Step 1: Fetch full client config from IDP (includes secret, providers, settings)
121
+ console.log('[STARTUP] Step 1/2: Fetching client config from IDP...');
122
+
123
+ // Clear any stale Redis cache so startup always gets fresh IDP data
124
+ await clearConfigRedisCache();
125
+
126
+ try {
127
+ const config = await getIDPClientConfig(true);
128
+ cachedIDPConfig = config;
129
+
130
+ console.log('[STARTUP] Client config loaded successfully');
131
+ console.log('[STARTUP] - Client ID:', config.clientId);
132
+ console.log('[STARTUP] - Client Slug:', config.clientSlug);
133
+ console.log('[STARTUP] - Secret length:', config.authSecret?.length || 0, 'chars');
134
+ console.log('[STARTUP] - OAuth Providers:', config.oauthProviders?.filter(p => p.enabled).map(p => p.provider).join(', ') || 'none');
135
+ console.log('[STARTUP] - Require 2FA:', config.authSettings?.require2FA);
136
+ console.log('[STARTUP] - Cache TTL:', config.configCacheTtlSeconds, 'seconds');
137
+ console.log('[STARTUP] - Base Client URL:', config.baseClientUrl || '(not set)');
138
+
139
+ // Set BETTER_AUTH_SECRET from IDP response if not already set.
140
+ // Also mirror to legacy NEXTAUTH_SECRET during the rename transition
141
+ // so any consumer code still reading the old name keeps working.
142
+ if (config.authSecret && !process.env.BETTER_AUTH_SECRET) {
143
+ process.env.BETTER_AUTH_SECRET = config.authSecret;
144
+ console.log('[STARTUP] Set BETTER_AUTH_SECRET from IDP config');
145
+ }
146
+ if (config.authSecret && !process.env.NEXTAUTH_SECRET) {
147
+ process.env.NEXTAUTH_SECRET = config.authSecret;
148
+ }
149
+ } catch (error) {
150
+ const errorMsg = error instanceof Error ? error.message : String(error);
151
+ console.error('[STARTUP] IDP config fetch failed:', errorMsg);
152
+
153
+ // No fallback available — IDP config is the only source for the auth secret
154
+ console.error('[STARTUP] No fallback available for auth secret resolution');
155
+ }
156
+
157
+ // Step 2: Verify BETTER_AUTH_SECRET is available - FAIL FAST if not
158
+ console.log('[STARTUP] Step 2/2: Verifying BETTER_AUTH_SECRET...');
159
+
160
+ const secret = process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET;
161
+ if (!secret || secret.trim() === '') {
162
+ console.error('');
163
+ console.error('╔══════════════════════════════════════════════════════════════╗');
164
+ console.error('ā•‘ āŒ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ā•‘');
165
+ console.error('ā•‘ ā•‘');
166
+ console.error('ā•‘ The app cannot start without a valid auth signing secret. ā•‘');
167
+ console.error('ā•‘ This should be fetched from IDP at startup. ā•‘');
168
+ console.error('ā•‘ ā•‘');
169
+ console.error('ā•‘ Possible causes: ā•‘');
170
+ console.error('ā•‘ • IDP is not running or unreachable ā•‘');
171
+ console.error('ā•‘ • CLIENT_ID is not registered in IDP ā•‘');
172
+ console.error('ā•‘ • IDP_URL is incorrect ā•‘');
173
+ console.error('ā•‘ • Network connectivity issue ā•‘');
174
+ console.error('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
175
+ console.error('');
176
+ throw new Error('FATAL: BETTER_AUTH_SECRET not available - cannot start without valid secret from IDP');
177
+ }
178
+
179
+ console.log('[STARTUP] BETTER_AUTH_SECRET verified (' + secret.length + ' chars)');
180
+
181
+ // Step 3: Validate cookie name consistency
182
+ // This catches bugs where getJwtCookieName() returns a different name than
183
+ // what auth-options.ts configures, which causes sessions to fail in production
184
+ const { validateCookieNameConsistency, getSessionCookieName, assertAppSlugMatchesIdp, assertIdpSlugKnown } = await import('./app-slug');
185
+ validateCookieNameConsistency();
186
+ // rigpert 60743: refuse to run with two cookie-name sources (IdP clientSlug vs APP_SLUG/CLIENT_ID).
187
+ assertIdpSlugKnown(cachedIDPConfig?.clientSlug);
188
+ assertAppSlugMatchesIdp(cachedIDPConfig?.clientSlug);
189
+ console.log('[STARTUP] Cookie name consistency validated:', getSessionCookieName());
190
+
191
+ // All done
192
+ console.log('');
193
+ console.log('╔══════════════════════════════════════════════════════════════╗');
194
+ console.log('ā•‘ PayEz Next MVP Ready for Requests ā•‘');
195
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
196
+ console.log('');
197
+
198
+ initializationComplete = true;
199
+ initializationFailed = false;
200
+ lastInitError = null;
201
+ } catch (error) {
202
+ lastInitError = error instanceof Error ? error : new Error(String(error));
203
+ initializationFailed = true;
204
+
205
+ const errorMsg = lastInitError.message || 'Unknown error';
206
+ const isConnectionError = errorMsg.includes('fetch failed') || errorMsg.includes('ECONNREFUSED');
207
+ const idpUrl = (process.env.IDP_URL || 'NOT SET').padEnd(46);
208
+ const clientId = (process.env.CLIENT_ID || 'NOT SET').padEnd(43);
209
+
210
+ const connectionLine = isConnectionError
211
+ ? 'ā•‘ šŸ”Œ CONNECTION REFUSED - IDP appears to be down ā•‘\nā•‘ ā•‘\n'
212
+ : '';
213
+
214
+ console.error(`
215
+ ╔══════════════════════════════════════════════════════════════╗
216
+ ā•‘ āŒ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ā•‘
217
+ ā•‘ ā•‘
218
+ ā•‘ The app cannot start without a valid auth signing secret. ā•‘
219
+ ā•‘ This should be fetched from IDP at startup. ā•‘
220
+ ā•‘ ā•‘
221
+ ${connectionLine}ā•‘ Possible causes: ā•‘
222
+ ā•‘ • IDP is not running or unreachable ā•‘
223
+ ā•‘ • CLIENT_ID is not registered in IDP ā•‘
224
+ ā•‘ • IDP_URL is incorrect ā•‘
225
+ ā•‘ • Network connectivity issue ā•‘
226
+ ā•‘ ā•‘
227
+ ā•‘ Current config: ā•‘
228
+ ā•‘ • IDP_URL: ${idpUrl}ā•‘
229
+ ā•‘ • CLIENT_ID: ${clientId}ā•‘
230
+ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
231
+
232
+ [STARTUP] Error: ${errorMsg}
233
+ `);
234
+
235
+ // Re-throw so callers know initialization failed
236
+ throw lastInitError;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Get the cached IDP config after initialization.
242
+ * Returns null if not yet initialized.
243
+ */
244
+ /**
245
+ * For health endpoints (rigpert 60765): a guard that does not stop the process is an empty red unless the mismatch is visible
246
+ * where operators and promote-fe look. Returns `SLUG_MISMATCH: cookie=<idp slug> gate=<app slug>` while the drift holds, else null;
247
+ * a health route should answer non-200 with this as the reason. Null until the IdP config has been fetched.
248
+ */
249
+ export function getSlugMismatchReason(): string | null {
250
+ return slugStateReason(cachedIDPConfig?.clientSlug, initializationComplete || initializationFailed);
251
+ }
252
+
253
+ export function getStartupIDPConfig(): IDPClientConfig | null {
254
+ return cachedIDPConfig;
255
+ }
256
+
257
+ /**
258
+ * Check if initialization failed (auth signing secret couldn't be retrieved)
259
+ */
260
+ export function isInitializationFailed(): boolean {
261
+ return initializationFailed;
262
+ }
263
+
264
+ /**
265
+ * Get the last initialization error
266
+ */
267
+ export function getInitializationError(): Error | null {
268
+ return lastInitError;
269
+ }
270
+
271
+ /**
272
+ * Check if the app is ready to handle auth requests
273
+ */
274
+ export function isAppReady(): boolean {
275
+ return initializationComplete && !initializationFailed;
276
+ }
@@ -1,41 +1,41 @@
1
- import { NextRequest } from 'next/server';
2
- import { logger } from '../config/logger';
3
- import { getSession } from '../server/auth';
4
- import { getSessionCookieName } from './app-slug';
5
-
6
- export async function getTokenTestAware(req: NextRequest): Promise<any> {
7
- if (process.env.TEST_MODE === 'true') {
8
- try {
9
- let secret = process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET;
10
- if (!secret || secret.trim() === '') {
11
- const { getIDPClientConfig } = await import('./idp-client-config');
12
- const idpConfig = await getIDPClientConfig();
13
- secret = idpConfig.authSecret;
14
- }
15
- // Use app-slug prefixed cookie name
16
- const cookieName = getSessionCookieName();
17
- const cookies = req.headers.get('cookie'); if (!cookies) { logger.debug('[GET_TOKEN] No cookies in request'); return null; }
18
- const cookieValue = cookies.split(';').find(c => c.trim().startsWith(`${cookieName}=`))?.split('=')[1];
19
- if (!cookieValue) { logger.debug('[GET_TOKEN] Session token cookie not found'); return null; }
20
- const { jwtVerify } = await import('jose');
21
- const secretKey = new TextEncoder().encode(secret);
22
- const { payload } = await jwtVerify(cookieValue, secretKey);
23
- logger.debug('[GET_TOKEN] TEST_MODE token decoded:', { hasPayload: !!payload, redisSessionId: (payload as any).redisSessionId, sub: (payload as any).sub });
24
- return payload;
25
- } catch (error) { logger.error('[GET_TOKEN] TEST_MODE token decode error:', { error: error instanceof Error ? error.message : String(error) }); return null; }
26
- }
27
- // Production path: Better Auth session
28
- const session = await getSession(req);
29
- if (session?.user && session?.session?.token) {
30
- return {
31
- sub: session.user.id,
32
- email: session.user.email,
33
- name: session.user.name,
34
- sessionToken: session.session.token,
35
- roles: session.user.roles || [],
36
- ...session.user,
37
- };
38
- }
39
-
40
- return null;
41
- }
1
+ import { NextRequest } from 'next/server';
2
+ import { logger } from '../config/logger';
3
+ import { getSession } from '../server/auth';
4
+ import { getSessionCookieName } from './app-slug';
5
+
6
+ export async function getTokenTestAware(req: NextRequest): Promise<any> {
7
+ if (process.env.TEST_MODE === 'true') {
8
+ try {
9
+ let secret = process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET;
10
+ if (!secret || secret.trim() === '') {
11
+ const { getIDPClientConfig } = await import('./idp-client-config');
12
+ const idpConfig = await getIDPClientConfig();
13
+ secret = idpConfig.authSecret;
14
+ }
15
+ // Use app-slug prefixed cookie name
16
+ const cookieName = getSessionCookieName();
17
+ const cookies = req.headers.get('cookie'); if (!cookies) { logger.debug('[GET_TOKEN] No cookies in request'); return null; }
18
+ const cookieValue = cookies.split(';').find(c => c.trim().startsWith(`${cookieName}=`))?.split('=')[1];
19
+ if (!cookieValue) { logger.debug('[GET_TOKEN] Session token cookie not found'); return null; }
20
+ const { jwtVerify } = await import('jose');
21
+ const secretKey = new TextEncoder().encode(secret);
22
+ const { payload } = await jwtVerify(cookieValue, secretKey);
23
+ logger.debug('[GET_TOKEN] TEST_MODE token decoded:', { hasPayload: !!payload, redisSessionId: (payload as any).redisSessionId, sub: (payload as any).sub });
24
+ return payload;
25
+ } catch (error) { logger.error('[GET_TOKEN] TEST_MODE token decode error:', { error: error instanceof Error ? error.message : String(error) }); return null; }
26
+ }
27
+ // Production path: Better Auth session
28
+ const session = await getSession(req);
29
+ if (session?.user && session?.session?.token) {
30
+ return {
31
+ sub: session.user.id,
32
+ email: session.user.email,
33
+ name: session.user.name,
34
+ sessionToken: session.session.token,
35
+ roles: session.user.roles || [],
36
+ ...session.user,
37
+ };
38
+ }
39
+
40
+ return null;
41
+ }