@payez/next-mvp 4.8.1 → 4.8.2
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/POST-INSTALL.md +408 -408
- package/README.md +859 -859
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/lib/session-store.js +21 -21
- package/dist/lib/startup-init.js +19 -19
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/dist/vibe/sessions.js +28 -3
- package/package.json +2 -2
- package/scripts/check-publish-eol.mjs +33 -15
- package/src/api/auth-handler.ts +550 -550
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/change-password.ts +170 -170
- package/src/api-handlers/account/masked-info.ts +46 -46
- package/src/api-handlers/account/profile.ts +99 -99
- package/src/api-handlers/account/recovery/forward.ts +117 -117
- package/src/api-handlers/account/recovery/initiate.ts +59 -59
- package/src/api-handlers/account/recovery/send-code.ts +21 -21
- package/src/api-handlers/account/recovery/verify-code.ts +20 -20
- package/src/api-handlers/account/reset-password.ts +87 -87
- package/src/api-handlers/account/send-code.ts +70 -70
- package/src/api-handlers/account/update-phone.ts +84 -84
- package/src/api-handlers/account/validate-password.ts +139 -139
- package/src/api-handlers/account/verify-email.ts +134 -134
- package/src/api-handlers/account/verify-sms.ts +134 -134
- package/src/api-handlers/admin/analytics.ts +452 -452
- package/src/api-handlers/admin/audit.ts +232 -232
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +259 -259
- package/src/api-handlers/admin/sessions.ts +327 -327
- package/src/api-handlers/admin/site-logs.ts +388 -388
- package/src/api-handlers/admin/stats.ts +260 -260
- package/src/api-handlers/admin/users.ts +251 -251
- package/src/api-handlers/admin/vibe-data.ts +333 -333
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/credential-login-email-code.ts +92 -92
- package/src/api-handlers/auth/credential-login.ts +948 -948
- package/src/api-handlers/auth/jwks.ts +31 -31
- package/src/api-handlers/auth/login.ts +252 -252
- package/src/api-handlers/auth/passkey-signin.ts +116 -116
- package/src/api-handlers/auth/refresh.ts +829 -829
- package/src/api-handlers/auth/signout.ts +201 -201
- package/src/api-handlers/auth/status.ts +23 -23
- package/src/api-handlers/auth/update-session.ts +144 -144
- package/src/api-handlers/auth/validate.ts +45 -45
- package/src/api-handlers/auth/verify-code.ts +116 -116
- package/src/api-handlers/mfa/backup-code-verify.ts +170 -170
- package/src/api-handlers/mfa/backup-codes.ts +42 -42
- package/src/api-handlers/mfa/methods.ts +47 -47
- package/src/api-handlers/mfa/totp-confirm.ts +43 -43
- package/src/api-handlers/mfa/totp-enroll.ts +43 -43
- package/src/api-handlers/mfa/totp-verify.ts +172 -172
- package/src/api-handlers/oauth/resume.ts +182 -182
- package/src/api-handlers/onboarding/send-email-code.ts +18 -18
- package/src/api-handlers/onboarding/set-password.ts +21 -21
- package/src/api-handlers/onboarding/verify-code.ts +20 -20
- package/src/api-handlers/onboarding/verify-welcome-email.ts +17 -17
- package/src/api-handlers/passkeys/list.ts +32 -32
- package/src/api-handlers/passkeys/manage.ts +55 -55
- package/src/api-handlers/passkeys/register-options.ts +43 -43
- package/src/api-handlers/passkeys/register-verify.ts +42 -42
- package/src/api-handlers/session/refresh-viability.ts +56 -56
- package/src/api-handlers/session/viability.ts +238 -238
- package/src/api-handlers/test/force-expire.ts +167 -167
- package/src/auth/auth-decision.ts +292 -292
- package/src/auth/better-auth.ts +1688 -1688
- package/src/auth/idp-oauth.ts +310 -310
- package/src/auth/password-reset-bridge.ts +98 -98
- package/src/auth/revocation-fields.ts +16 -16
- package/src/auth/route-config.ts +236 -236
- package/src/auth/safe-callback.ts +199 -199
- package/src/auth/session-cookie-clear.ts +30 -30
- package/src/auth/session-reset-link.ts +159 -159
- package/src/auth/session-response-sanitizer.ts +151 -151
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/auth/utils/idp-client.ts +364 -364
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +192 -192
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +277 -277
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +508 -508
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +421 -421
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/CredentialSignInForm.tsx +240 -240
- package/src/components/auth/FederatedAuthSection.tsx +129 -129
- package/src/components/auth/ModeAwareLoginPage.tsx +146 -146
- package/src/components/auth/ModeAwareSignupPage.tsx +200 -200
- package/src/components/auth/PasswordResetBridge.tsx +30 -30
- package/src/components/auth/TraditionalAuthSection.tsx +103 -103
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/RequestedStep.tsx +30 -30
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +118 -118
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +55 -55
- package/src/config/logger.ts +62 -62
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +109 -109
- package/src/hooks/useAvailableProviders.ts +143 -143
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/useProfile.ts +82 -82
- package/src/hooks/usePublicAuthSettings.ts +171 -171
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +337 -337
- package/src/index.ts +64 -64
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +228 -228
- package/src/lib/authorize-resume.ts +172 -172
- package/src/lib/bearer-fetch.ts +31 -31
- package/src/lib/client-key.ts +88 -88
- package/src/lib/code-step-copy.ts +83 -83
- package/src/lib/credential-sign-in-client.ts +101 -101
- package/src/lib/demo-mode.ts +18 -18
- package/src/lib/device-id.ts +67 -67
- package/src/lib/ensure-fresh-access-token.ts +509 -509
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-client-config.ts +1005 -1005
- package/src/lib/idp-fetch.ts +169 -169
- package/src/lib/idp-url-guard.ts +37 -37
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/lock-window.ts +49 -49
- package/src/lib/login-error-message.ts +41 -41
- package/src/lib/never-in-response.ts +64 -64
- package/src/lib/nextauth-secret.ts +168 -168
- package/src/lib/onboarding-proxy.ts +57 -57
- package/src/lib/password-rules.ts +50 -50
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +105 -105
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/roles.ts +177 -177
- package/src/lib/safe-error-response.ts +149 -149
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-cookie-shape.ts +185 -185
- package/src/lib/session-store.ts +1092 -1092
- package/src/lib/session.ts +34 -34
- package/src/lib/single-flight.ts +64 -64
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/startup-init.ts +276 -276
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +546 -546
- package/src/lib/token-log-id.ts +69 -69
- package/src/lib/twofa-rejection.ts +91 -91
- package/src/lib/types/api-responses.ts +336 -336
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +1141 -1141
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +331 -331
- package/src/pages/admin-login/page.tsx +111 -111
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +338 -338
- package/src/pages/onboarding/index.ts +11 -11
- package/src/pages/onboarding/page.tsx +212 -212
- package/src/pages/profile/EnhancedProfilePage.tsx +514 -514
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +267 -267
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +660 -660
- package/src/pages/security/index.ts +12 -12
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/service-unavailable/page.tsx +63 -63
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +192 -192
- package/src/pages/test-env/JwtInspectPage.tsx +449 -449
- package/src/pages/test-env/RefreshTokenPage.tsx +178 -178
- package/src/pages/test-env/TestEnvPage.tsx +130 -130
- package/src/pages/test-env/guarded.tsx +81 -81
- package/src/pages/test-env/index.ts +77 -77
- package/src/pages/test-env/redact-credentials.ts +96 -96
- package/src/pages/verify-code/page.tsx +746 -746
- package/src/routes/account/index.ts +31 -31
- package/src/routes/account/masked-info.ts +36 -36
- package/src/routes/account/send-code.ts +39 -39
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/account/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/credential-login-email-code.ts +6 -6
- package/src/routes/auth/credential-login.ts +6 -6
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/logout.ts +111 -111
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +77 -77
- package/src/routes/auth/session-reset.ts +37 -37
- package/src/routes/auth/session.ts +261 -261
- package/src/routes/auth/settings.ts +181 -181
- package/src/routes/auth/viability.ts +519 -519
- package/src/routes/index.ts +17 -17
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/onboarding/index.ts +15 -15
- package/src/routes/onboarding/send-email-code.ts +2 -2
- package/src/routes/onboarding/set-password.ts +2 -2
- package/src/routes/onboarding/verify-code.ts +2 -2
- package/src/routes/onboarding/verify-welcome-email.ts +2 -2
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth-guard.ts +203 -203
- package/src/server/auth.ts +486 -486
- package/src/server/decode-session.ts +309 -309
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/verified-session-token.ts +103 -103
- package/src/server/with-auth.ts +93 -93
- package/src/services/signalrActivityService.ts +258 -258
- package/src/stores/authStore.ts +1900 -1900
- package/src/templates/instrumentation.ts +60 -60
- package/src/theme/ThemeProvider.tsx +39 -39
- package/src/theme/default.ts +33 -33
- package/src/theme/index.ts +31 -31
- package/src/theme/types.ts +69 -69
- package/src/theme/useTheme.ts +57 -57
- package/src/theme/utils.ts +40 -40
- package/src/types/api.ts +12 -12
- package/src/types/auth.d.ts +23 -23
- package/src/types/auth.ts +48 -48
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +401 -401
- package/src/utils/circuitBreaker.ts +40 -40
- package/src/utils/error-message.ts +108 -108
- package/src/utils/layout/reservedSpace.ts +124 -124
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +877 -877
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +209 -209
- package/src/vibe/generic.ts +339 -339
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +133 -133
- package/src/vibe/sessions.ts +925 -899
- package/src/vibe/types.ts +577 -577
package/src/lib/session.ts
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
export type MinimalSession = {
|
|
2
|
-
user?: {
|
|
3
|
-
id?: string | null;
|
|
4
|
-
email?: string | null;
|
|
5
|
-
} | null;
|
|
6
|
-
accessToken?: string | null;
|
|
7
|
-
expires?: string;
|
|
8
|
-
[k: string]: any;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// AppSession is used by authStore - alias to MinimalSession for compatibility
|
|
12
|
-
export type AppSession = MinimalSession;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Strict session validation for client-side guards.
|
|
16
|
-
* A session is considered valid when:
|
|
17
|
-
* - a user object exists with non-empty id and email
|
|
18
|
-
* - an accessToken string exists
|
|
19
|
-
*/
|
|
20
|
-
export function isValidSession(session: MinimalSession | null | undefined): boolean {
|
|
21
|
-
if (!session) return false;
|
|
22
|
-
const u = session.user as any;
|
|
23
|
-
const hasUser = !!u && typeof u.id === 'string' && u.id.length > 0 && typeof u.email === 'string' && u.email.length > 0;
|
|
24
|
-
const hasAccessToken = typeof session.accessToken === 'string' && session.accessToken.length > 0;
|
|
25
|
-
return hasUser && hasAccessToken;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Sanitize session data - returns null if session is invalid
|
|
30
|
-
*/
|
|
31
|
-
export function sanitizeSession(session: MinimalSession | null | undefined): MinimalSession | null {
|
|
32
|
-
if (!isValidSession(session)) return null;
|
|
33
|
-
return session as MinimalSession;
|
|
34
|
-
}
|
|
1
|
+
export type MinimalSession = {
|
|
2
|
+
user?: {
|
|
3
|
+
id?: string | null;
|
|
4
|
+
email?: string | null;
|
|
5
|
+
} | null;
|
|
6
|
+
accessToken?: string | null;
|
|
7
|
+
expires?: string;
|
|
8
|
+
[k: string]: any;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// AppSession is used by authStore - alias to MinimalSession for compatibility
|
|
12
|
+
export type AppSession = MinimalSession;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Strict session validation for client-side guards.
|
|
16
|
+
* A session is considered valid when:
|
|
17
|
+
* - a user object exists with non-empty id and email
|
|
18
|
+
* - an accessToken string exists
|
|
19
|
+
*/
|
|
20
|
+
export function isValidSession(session: MinimalSession | null | undefined): boolean {
|
|
21
|
+
if (!session) return false;
|
|
22
|
+
const u = session.user as any;
|
|
23
|
+
const hasUser = !!u && typeof u.id === 'string' && u.id.length > 0 && typeof u.email === 'string' && u.email.length > 0;
|
|
24
|
+
const hasAccessToken = typeof session.accessToken === 'string' && session.accessToken.length > 0;
|
|
25
|
+
return hasUser && hasAccessToken;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Sanitize session data - returns null if session is invalid
|
|
30
|
+
*/
|
|
31
|
+
export function sanitizeSession(session: MinimalSession | null | undefined): MinimalSession | null {
|
|
32
|
+
if (!isValidSession(session)) return null;
|
|
33
|
+
return session as MinimalSession;
|
|
34
|
+
}
|
package/src/lib/single-flight.ts
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single-flight memoisation that does NOT memoise failure — card 244549.
|
|
3
|
-
*
|
|
4
|
-
* The bug this exists to prevent: caching an in-flight promise to de-duplicate
|
|
5
|
-
* concurrent callers is correct, but if the cached promise REJECTS and is never
|
|
6
|
-
* cleared, the rejection is memoised for the life of the process. Every later
|
|
7
|
-
* caller re-awaits the same settled rejection and re-throws the same stale
|
|
8
|
-
* error, long after the underlying dependency has recovered. Recovery becomes
|
|
9
|
-
* restart-only, and — because the error text is identical every time — it looks
|
|
10
|
-
* exactly like the dependency is still down.
|
|
11
|
-
*
|
|
12
|
-
* Measured instance: @payez/next-mvp getAuthInstance() memoised a failed IDP
|
|
13
|
-
* config load. Re-injecting correct config into three running rigs on
|
|
14
|
-
* 2026-08-23 did not heal them; the processes were replaying a cached
|
|
15
|
-
* rejection rather than retrying.
|
|
16
|
-
*
|
|
17
|
-
* Contract:
|
|
18
|
-
* - concurrent callers during a successful load share ONE underlying call
|
|
19
|
-
* - a rejection is re-thrown to every caller awaiting that attempt (failures
|
|
20
|
-
* stay LOUD — this never converts an error into a silent retry)
|
|
21
|
-
* - the NEXT call after a failure starts a fresh attempt
|
|
22
|
-
* - a slow failure never clears a newer in-flight attempt
|
|
23
|
-
*
|
|
24
|
-
* Zero imports on purpose: this must be unit-testable without loading the
|
|
25
|
-
* server auth stack, which is why the bug it fixes went untested for so long.
|
|
26
|
-
*/
|
|
27
|
-
export interface SingleFlight<T> {
|
|
28
|
-
/** Run (or join) the in-flight attempt. Rejects if the attempt rejects. */
|
|
29
|
-
get(): Promise<T>;
|
|
30
|
-
/** Test/diagnostic seam: is an attempt currently cached? */
|
|
31
|
-
isPending(): boolean;
|
|
32
|
-
/** Drop any cached attempt. */
|
|
33
|
-
reset(): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function createSingleFlight<T>(factory: () => Promise<T>): SingleFlight<T> {
|
|
37
|
-
let pending: Promise<T> | null = null;
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
get(): Promise<T> {
|
|
41
|
-
if (pending) return pending;
|
|
42
|
-
|
|
43
|
-
const attempt = factory();
|
|
44
|
-
|
|
45
|
-
// Cleanup runs on a SIDE branch so the promise handed to callers is the
|
|
46
|
-
// original one and still rejects. The identity guard means only the
|
|
47
|
-
// attempt that actually failed clears itself: without it, a slow failure
|
|
48
|
-
// could evict a newer attempt a later caller had already installed, and
|
|
49
|
-
// that caller would be de-duplicated against a promise nobody owns.
|
|
50
|
-
attempt.catch(() => {
|
|
51
|
-
if (pending === attempt) pending = null;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
pending = attempt;
|
|
55
|
-
return pending;
|
|
56
|
-
},
|
|
57
|
-
isPending(): boolean {
|
|
58
|
-
return pending !== null;
|
|
59
|
-
},
|
|
60
|
-
reset(): void {
|
|
61
|
-
pending = null;
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight memoisation that does NOT memoise failure — card 244549.
|
|
3
|
+
*
|
|
4
|
+
* The bug this exists to prevent: caching an in-flight promise to de-duplicate
|
|
5
|
+
* concurrent callers is correct, but if the cached promise REJECTS and is never
|
|
6
|
+
* cleared, the rejection is memoised for the life of the process. Every later
|
|
7
|
+
* caller re-awaits the same settled rejection and re-throws the same stale
|
|
8
|
+
* error, long after the underlying dependency has recovered. Recovery becomes
|
|
9
|
+
* restart-only, and — because the error text is identical every time — it looks
|
|
10
|
+
* exactly like the dependency is still down.
|
|
11
|
+
*
|
|
12
|
+
* Measured instance: @payez/next-mvp getAuthInstance() memoised a failed IDP
|
|
13
|
+
* config load. Re-injecting correct config into three running rigs on
|
|
14
|
+
* 2026-08-23 did not heal them; the processes were replaying a cached
|
|
15
|
+
* rejection rather than retrying.
|
|
16
|
+
*
|
|
17
|
+
* Contract:
|
|
18
|
+
* - concurrent callers during a successful load share ONE underlying call
|
|
19
|
+
* - a rejection is re-thrown to every caller awaiting that attempt (failures
|
|
20
|
+
* stay LOUD — this never converts an error into a silent retry)
|
|
21
|
+
* - the NEXT call after a failure starts a fresh attempt
|
|
22
|
+
* - a slow failure never clears a newer in-flight attempt
|
|
23
|
+
*
|
|
24
|
+
* Zero imports on purpose: this must be unit-testable without loading the
|
|
25
|
+
* server auth stack, which is why the bug it fixes went untested for so long.
|
|
26
|
+
*/
|
|
27
|
+
export interface SingleFlight<T> {
|
|
28
|
+
/** Run (or join) the in-flight attempt. Rejects if the attempt rejects. */
|
|
29
|
+
get(): Promise<T>;
|
|
30
|
+
/** Test/diagnostic seam: is an attempt currently cached? */
|
|
31
|
+
isPending(): boolean;
|
|
32
|
+
/** Drop any cached attempt. */
|
|
33
|
+
reset(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createSingleFlight<T>(factory: () => Promise<T>): SingleFlight<T> {
|
|
37
|
+
let pending: Promise<T> | null = null;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
get(): Promise<T> {
|
|
41
|
+
if (pending) return pending;
|
|
42
|
+
|
|
43
|
+
const attempt = factory();
|
|
44
|
+
|
|
45
|
+
// Cleanup runs on a SIDE branch so the promise handed to callers is the
|
|
46
|
+
// original one and still rejects. The identity guard means only the
|
|
47
|
+
// attempt that actually failed clears itself: without it, a slow failure
|
|
48
|
+
// could evict a newer attempt a later caller had already installed, and
|
|
49
|
+
// that caller would be de-duplicated against a promise nobody owns.
|
|
50
|
+
attempt.catch(() => {
|
|
51
|
+
if (pending === attempt) pending = null;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
pending = attempt;
|
|
55
|
+
return pending;
|
|
56
|
+
},
|
|
57
|
+
isPending(): boolean {
|
|
58
|
+
return pending !== null;
|
|
59
|
+
},
|
|
60
|
+
reset(): void {
|
|
61
|
+
pending = null;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|