@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,117 +1,117 @@
1
- /**
2
- * Slim Middleware — Cookie-Only Auth Check
3
- *
4
- * Replaces the self-fetching middleware with a cookie existence check.
5
- * All real auth validation happens in server-side layouts (authGuard).
6
- *
7
- * Zero self-fetches. Zero Redis calls. Zero JWT decoding.
8
- * Just: does the session cookie exist? Yes → pass through. No → redirect to login.
9
- */
10
-
11
- import { NextRequest, NextResponse } from 'next/server';
12
- import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
13
-
14
- // =============================================================================
15
- // TYPES
16
- // =============================================================================
17
-
18
- export interface SlimMiddlewareOptions {
19
- /** Routes that don't require authentication (glob-style patterns) */
20
- publicRoutes?: string[];
21
- /** Login page URL (default: /account-auth/login) */
22
- loginUrl?: string;
23
- /** Additional paths to always bypass (e.g., /api/auth/, /api/session/) */
24
- bypassPrefixes?: string[];
25
- }
26
-
27
- // =============================================================================
28
- // DEFAULT BYPASS PATHS
29
- // =============================================================================
30
-
31
- /** Routes that must always bypass middleware (prevent infinite loops) */
32
- const DEFAULT_BYPASS_PREFIXES = [
33
- '/api/auth/',
34
- '/api/session/',
35
- '/_next/',
36
- '/favicon.ico',
37
- ];
38
-
39
- /** Static file extensions to bypass */
40
- const STATIC_EXTENSIONS = /\.(svg|png|jpg|jpeg|gif|webp|ico|css|js|woff|woff2|ttf|eot|map)$/i;
41
-
42
- // =============================================================================
43
- // MAIN
44
- // =============================================================================
45
-
46
- /**
47
- * Create a slim middleware that only checks cookie existence.
48
- * Auth validation is deferred to server-side layouts (authGuard).
49
- */
50
- export function createSlimMiddleware(options?: SlimMiddlewareOptions) {
51
- const publicRoutes = options?.publicRoutes || [];
52
- const loginUrl = options?.loginUrl || '/account-auth/login';
53
- const extraBypass = options?.bypassPrefixes || [];
54
- const allBypass = [...DEFAULT_BYPASS_PREFIXES, ...extraBypass];
55
-
56
- // Pre-compile public route patterns for fast matching
57
- const publicMatchers = publicRoutes.map(pattern => {
58
- if (pattern.endsWith('/*')) {
59
- const prefix = pattern.slice(0, -2);
60
- return (p: string) => p === prefix || p.startsWith(prefix + '/');
61
- }
62
- if (pattern.endsWith('*')) {
63
- const prefix = pattern.slice(0, -1);
64
- return (p: string) => p.startsWith(prefix);
65
- }
66
- if (pattern.startsWith('/*.')) {
67
- const ext = pattern.slice(2);
68
- return (p: string) => p.endsWith(ext);
69
- }
70
- return (p: string) => p === pattern;
71
- });
72
-
73
- return function middleware(request: NextRequest): NextResponse {
74
- const { pathname } = request.nextUrl;
75
-
76
- // 1. Always bypass static/internal routes
77
- if (STATIC_EXTENSIONS.test(pathname)) {
78
- return NextResponse.next();
79
- }
80
- for (const prefix of allBypass) {
81
- if (pathname.startsWith(prefix)) {
82
- return NextResponse.next();
83
- }
84
- }
85
-
86
- // 2. Check if it's a public route → pass through
87
- for (const matcher of publicMatchers) {
88
- if (matcher(pathname)) {
89
- return NextResponse.next();
90
- }
91
- }
92
-
93
- // 3. THE ONLY AUTH CHECK: Does a session cookie exist?
94
- const sessionCookieName = getSessionCookieName();
95
- const secureCookieName = getSecureSessionCookieName();
96
- const hasCookie =
97
- request.cookies.has(sessionCookieName) ||
98
- request.cookies.has(secureCookieName);
99
-
100
- if (!hasCookie) {
101
- // No cookie on a protected route → redirect to login
102
- // API routes get 401 instead of redirect
103
- if (pathname.startsWith('/api/')) {
104
- return NextResponse.json(
105
- { error: 'Unauthorized', message: 'No session' },
106
- { status: 401 }
107
- );
108
- }
109
-
110
- const callbackUrl = encodeURIComponent(pathname);
111
- return NextResponse.redirect(new URL(`${loginUrl}?callbackUrl=${callbackUrl}`, request.url));
112
- }
113
-
114
- // Cookie exists → pass through, layout authGuard does the real validation
115
- return NextResponse.next();
116
- };
117
- }
1
+ /**
2
+ * Slim Middleware — Cookie-Only Auth Check
3
+ *
4
+ * Replaces the self-fetching middleware with a cookie existence check.
5
+ * All real auth validation happens in server-side layouts (authGuard).
6
+ *
7
+ * Zero self-fetches. Zero Redis calls. Zero JWT decoding.
8
+ * Just: does the session cookie exist? Yes → pass through. No → redirect to login.
9
+ */
10
+
11
+ import { NextRequest, NextResponse } from 'next/server';
12
+ import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
13
+
14
+ // =============================================================================
15
+ // TYPES
16
+ // =============================================================================
17
+
18
+ export interface SlimMiddlewareOptions {
19
+ /** Routes that don't require authentication (glob-style patterns) */
20
+ publicRoutes?: string[];
21
+ /** Login page URL (default: /account-auth/login) */
22
+ loginUrl?: string;
23
+ /** Additional paths to always bypass (e.g., /api/auth/, /api/session/) */
24
+ bypassPrefixes?: string[];
25
+ }
26
+
27
+ // =============================================================================
28
+ // DEFAULT BYPASS PATHS
29
+ // =============================================================================
30
+
31
+ /** Routes that must always bypass middleware (prevent infinite loops) */
32
+ const DEFAULT_BYPASS_PREFIXES = [
33
+ '/api/auth/',
34
+ '/api/session/',
35
+ '/_next/',
36
+ '/favicon.ico',
37
+ ];
38
+
39
+ /** Static file extensions to bypass */
40
+ const STATIC_EXTENSIONS = /\.(svg|png|jpg|jpeg|gif|webp|ico|css|js|woff|woff2|ttf|eot|map)$/i;
41
+
42
+ // =============================================================================
43
+ // MAIN
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Create a slim middleware that only checks cookie existence.
48
+ * Auth validation is deferred to server-side layouts (authGuard).
49
+ */
50
+ export function createSlimMiddleware(options?: SlimMiddlewareOptions) {
51
+ const publicRoutes = options?.publicRoutes || [];
52
+ const loginUrl = options?.loginUrl || '/account-auth/login';
53
+ const extraBypass = options?.bypassPrefixes || [];
54
+ const allBypass = [...DEFAULT_BYPASS_PREFIXES, ...extraBypass];
55
+
56
+ // Pre-compile public route patterns for fast matching
57
+ const publicMatchers = publicRoutes.map(pattern => {
58
+ if (pattern.endsWith('/*')) {
59
+ const prefix = pattern.slice(0, -2);
60
+ return (p: string) => p === prefix || p.startsWith(prefix + '/');
61
+ }
62
+ if (pattern.endsWith('*')) {
63
+ const prefix = pattern.slice(0, -1);
64
+ return (p: string) => p.startsWith(prefix);
65
+ }
66
+ if (pattern.startsWith('/*.')) {
67
+ const ext = pattern.slice(2);
68
+ return (p: string) => p.endsWith(ext);
69
+ }
70
+ return (p: string) => p === pattern;
71
+ });
72
+
73
+ return function middleware(request: NextRequest): NextResponse {
74
+ const { pathname } = request.nextUrl;
75
+
76
+ // 1. Always bypass static/internal routes
77
+ if (STATIC_EXTENSIONS.test(pathname)) {
78
+ return NextResponse.next();
79
+ }
80
+ for (const prefix of allBypass) {
81
+ if (pathname.startsWith(prefix)) {
82
+ return NextResponse.next();
83
+ }
84
+ }
85
+
86
+ // 2. Check if it's a public route → pass through
87
+ for (const matcher of publicMatchers) {
88
+ if (matcher(pathname)) {
89
+ return NextResponse.next();
90
+ }
91
+ }
92
+
93
+ // 3. THE ONLY AUTH CHECK: Does a session cookie exist?
94
+ const sessionCookieName = getSessionCookieName();
95
+ const secureCookieName = getSecureSessionCookieName();
96
+ const hasCookie =
97
+ request.cookies.has(sessionCookieName) ||
98
+ request.cookies.has(secureCookieName);
99
+
100
+ if (!hasCookie) {
101
+ // No cookie on a protected route → redirect to login
102
+ // API routes get 401 instead of redirect
103
+ if (pathname.startsWith('/api/')) {
104
+ return NextResponse.json(
105
+ { error: 'Unauthorized', message: 'No session' },
106
+ { status: 401 }
107
+ );
108
+ }
109
+
110
+ const callbackUrl = encodeURIComponent(pathname);
111
+ return NextResponse.redirect(new URL(`${loginUrl}?callbackUrl=${callbackUrl}`, request.url));
112
+ }
113
+
114
+ // Cookie exists → pass through, layout authGuard does the real validation
115
+ return NextResponse.next();
116
+ };
117
+ }
@@ -1,103 +1,103 @@
1
- /**
2
- * PAY-1476 / card 372275 (R941 s1b) - THE ONE SHARED VERIFIED RESOLVER for a session token taken from a cookie.
3
- *
4
- * THE DEFECT (4.4.5, measured at the deployed portal by QAPert-NightHawk, mail 59030):
5
- * Module A server/decode-session.ts the ba-hmac branch took `{token}.{anything}` and used segment 0 unread.
6
- * Module B server/auth.ts Path B took the token from ANY recognised shape and never verified anything;
7
- * a forged JWS resolved there too.
8
- * Both then treated "a Redis record exists" as the validation. So a leaked session token string was a bearer
9
- * credential with no signature needed, including on the portal's admin API gate (api-admin-gate -> decodeSession).
10
- *
11
- * THE RULE THIS MODULE ENFORCES: a cookie yields a session token ONLY after its signature checks out against
12
- * the auth secret. Every caller that turns a cookie into a token for the session store awaits this function.
13
- * lib/session-cookie-shape.ts stays classification-only and secret-free (it cannot verify, by construction).
14
- *
15
- * ba-hmac `{token}.{sig}` as Better Auth mints it (better-call signCookieValue): sig =
16
- * base64(HMAC-SHA256(secret, token)), standard padding, 44 characters. Compared with
17
- * crypto.timingSafeEqual over equal-length buffers.
18
- * jws legacy NextAuth-era cookie: jose jwtVerify with the same secret, then the sessionToken
19
- * (or redisSessionId) claim.
20
- * anything else, a missing secret, or a failed check -> null. Never throws.
21
- *
22
- * THE SECRET is the auth secret from getIDPClientConfig(), which resolves env-first (BETTER_AUTH_SECRET,
23
- * NEXTAUTH_SECRET, then IdP config; PAY-1460). The cookie HMAC key IS the auth secret, so the resolver and the
24
- * config loader cannot disagree about which secret signs sessions (BAPert 59034).
25
- */
26
- import { createHmac, timingSafeEqual } from 'crypto';
27
- import { jwtVerify, type JWTPayload } from 'jose';
28
- import { getIDPClientConfig } from '../lib/idp-client-config';
29
- import { extractSessionCookieValue, extractSessionTokenFromCookieValue } from '../lib/session-cookie-shape';
30
-
31
- /** Better Auth's signature: base64 of a 32-byte HMAC-SHA256, padded = 44 chars ending in '='. */
32
- const BA_SIGNATURE_LENGTH = 44;
33
-
34
- async function authSecret(): Promise<string | null> {
35
- try {
36
- const config = await getIDPClientConfig();
37
- return config?.authSecret ? String(config.authSecret) : null;
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- function verifyBaHmac(rawValue: string, secret: string): string | null {
44
- const dot = rawValue.lastIndexOf('.');
45
- if (dot < 1) return null;
46
- const token = rawValue.slice(0, dot);
47
- let signature = rawValue.slice(dot + 1);
48
- if (signature.includes('%')) {
49
- try {
50
- signature = decodeURIComponent(signature);
51
- } catch {
52
- return null;
53
- }
54
- }
55
- if (signature.length !== BA_SIGNATURE_LENGTH || !signature.endsWith('=')) return null;
56
- const expected = createHmac('sha256', secret).update(token, 'utf8').digest('base64');
57
- const a = Buffer.from(signature, 'utf8');
58
- const b = Buffer.from(expected, 'utf8');
59
- if (a.length !== b.length) return null;
60
- return timingSafeEqual(a, b) ? token : null;
61
- }
62
-
63
- export interface VerifiedSessionCookie {
64
- token: string;
65
- shape: 'ba-hmac' | 'jws';
66
- /** The verified JWS payload (jws shape only). */
67
- payload?: JWTPayload;
68
- }
69
-
70
- async function verifyJws(rawValue: string, secret: string): Promise<VerifiedSessionCookie | null> {
71
- try {
72
- const { payload } = await jwtVerify(rawValue, new TextEncoder().encode(secret));
73
- const claim = (payload as Record<string, unknown>).sessionToken ?? (payload as Record<string, unknown>).redisSessionId;
74
- return typeof claim === 'string' && claim ? { token: claim, shape: 'jws', payload } : null;
75
- } catch {
76
- return null;
77
- }
78
- }
79
-
80
- /** The verified session cookie (token, shape, JWS payload), or null unless its signature verifies. */
81
- export async function resolveVerifiedSessionCookie(rawValue: string | null | undefined): Promise<VerifiedSessionCookie | null> {
82
- if (!rawValue) return null;
83
- const shaped = extractSessionTokenFromCookieValue(rawValue);
84
- if (!shaped) return null;
85
- const secret = await authSecret();
86
- if (!secret) return null;
87
- if (shaped.shape === 'ba-hmac') {
88
- const token = verifyBaHmac(rawValue, secret);
89
- return token ? { token, shape: 'ba-hmac' } : null;
90
- }
91
- return verifyJws(rawValue, secret);
92
- }
93
-
94
- /** The session token carried by a raw session-cookie value, or null unless its signature verifies. */
95
- export async function resolveVerifiedSessionToken(rawValue: string | null | undefined): Promise<string | null> {
96
- const verified = await resolveVerifiedSessionCookie(rawValue);
97
- return verified ? verified.token : null;
98
- }
99
-
100
- /** Same, reading the session cookie (every name it is minted under, chunks joined) from a Request. */
101
- export async function resolveVerifiedSessionTokenFromRequest(request: Request): Promise<string | null> {
102
- return resolveVerifiedSessionToken(extractSessionCookieValue(request));
103
- }
1
+ /**
2
+ * PAY-1476 / card 372275 (R941 s1b) - THE ONE SHARED VERIFIED RESOLVER for a session token taken from a cookie.
3
+ *
4
+ * THE DEFECT (4.4.5, measured at the deployed portal by QAPert-NightHawk, mail 59030):
5
+ * Module A server/decode-session.ts the ba-hmac branch took `{token}.{anything}` and used segment 0 unread.
6
+ * Module B server/auth.ts Path B took the token from ANY recognised shape and never verified anything;
7
+ * a forged JWS resolved there too.
8
+ * Both then treated "a Redis record exists" as the validation. So a leaked session token string was a bearer
9
+ * credential with no signature needed, including on the portal's admin API gate (api-admin-gate -> decodeSession).
10
+ *
11
+ * THE RULE THIS MODULE ENFORCES: a cookie yields a session token ONLY after its signature checks out against
12
+ * the auth secret. Every caller that turns a cookie into a token for the session store awaits this function.
13
+ * lib/session-cookie-shape.ts stays classification-only and secret-free (it cannot verify, by construction).
14
+ *
15
+ * ba-hmac `{token}.{sig}` as Better Auth mints it (better-call signCookieValue): sig =
16
+ * base64(HMAC-SHA256(secret, token)), standard padding, 44 characters. Compared with
17
+ * crypto.timingSafeEqual over equal-length buffers.
18
+ * jws legacy NextAuth-era cookie: jose jwtVerify with the same secret, then the sessionToken
19
+ * (or redisSessionId) claim.
20
+ * anything else, a missing secret, or a failed check -> null. Never throws.
21
+ *
22
+ * THE SECRET is the auth secret from getIDPClientConfig(), which resolves env-first (BETTER_AUTH_SECRET,
23
+ * NEXTAUTH_SECRET, then IdP config; PAY-1460). The cookie HMAC key IS the auth secret, so the resolver and the
24
+ * config loader cannot disagree about which secret signs sessions (BAPert 59034).
25
+ */
26
+ import { createHmac, timingSafeEqual } from 'crypto';
27
+ import { jwtVerify, type JWTPayload } from 'jose';
28
+ import { getIDPClientConfig } from '../lib/idp-client-config';
29
+ import { extractSessionCookieValue, extractSessionTokenFromCookieValue } from '../lib/session-cookie-shape';
30
+
31
+ /** Better Auth's signature: base64 of a 32-byte HMAC-SHA256, padded = 44 chars ending in '='. */
32
+ const BA_SIGNATURE_LENGTH = 44;
33
+
34
+ async function authSecret(): Promise<string | null> {
35
+ try {
36
+ const config = await getIDPClientConfig();
37
+ return config?.authSecret ? String(config.authSecret) : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function verifyBaHmac(rawValue: string, secret: string): string | null {
44
+ const dot = rawValue.lastIndexOf('.');
45
+ if (dot < 1) return null;
46
+ const token = rawValue.slice(0, dot);
47
+ let signature = rawValue.slice(dot + 1);
48
+ if (signature.includes('%')) {
49
+ try {
50
+ signature = decodeURIComponent(signature);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ if (signature.length !== BA_SIGNATURE_LENGTH || !signature.endsWith('=')) return null;
56
+ const expected = createHmac('sha256', secret).update(token, 'utf8').digest('base64');
57
+ const a = Buffer.from(signature, 'utf8');
58
+ const b = Buffer.from(expected, 'utf8');
59
+ if (a.length !== b.length) return null;
60
+ return timingSafeEqual(a, b) ? token : null;
61
+ }
62
+
63
+ export interface VerifiedSessionCookie {
64
+ token: string;
65
+ shape: 'ba-hmac' | 'jws';
66
+ /** The verified JWS payload (jws shape only). */
67
+ payload?: JWTPayload;
68
+ }
69
+
70
+ async function verifyJws(rawValue: string, secret: string): Promise<VerifiedSessionCookie | null> {
71
+ try {
72
+ const { payload } = await jwtVerify(rawValue, new TextEncoder().encode(secret));
73
+ const claim = (payload as Record<string, unknown>).sessionToken ?? (payload as Record<string, unknown>).redisSessionId;
74
+ return typeof claim === 'string' && claim ? { token: claim, shape: 'jws', payload } : null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /** The verified session cookie (token, shape, JWS payload), or null unless its signature verifies. */
81
+ export async function resolveVerifiedSessionCookie(rawValue: string | null | undefined): Promise<VerifiedSessionCookie | null> {
82
+ if (!rawValue) return null;
83
+ const shaped = extractSessionTokenFromCookieValue(rawValue);
84
+ if (!shaped) return null;
85
+ const secret = await authSecret();
86
+ if (!secret) return null;
87
+ if (shaped.shape === 'ba-hmac') {
88
+ const token = verifyBaHmac(rawValue, secret);
89
+ return token ? { token, shape: 'ba-hmac' } : null;
90
+ }
91
+ return verifyJws(rawValue, secret);
92
+ }
93
+
94
+ /** The session token carried by a raw session-cookie value, or null unless its signature verifies. */
95
+ export async function resolveVerifiedSessionToken(rawValue: string | null | undefined): Promise<string | null> {
96
+ const verified = await resolveVerifiedSessionCookie(rawValue);
97
+ return verified ? verified.token : null;
98
+ }
99
+
100
+ /** Same, reading the session cookie (every name it is minted under, chunks joined) from a Request. */
101
+ export async function resolveVerifiedSessionTokenFromRequest(request: Request): Promise<string | null> {
102
+ return resolveVerifiedSessionToken(extractSessionCookieValue(request));
103
+ }
@@ -1,93 +1,93 @@
1
- /**
2
- * Server-Side Auth Wrapper for API Routes & Server Actions
3
- *
4
- * Wraps route handlers with session validation. Uses direct Redis reads.
5
- * Zero HTTP self-fetches.
6
- *
7
- * Usage:
8
- * export const GET = withAuth(async (req, auth) => {
9
- * return NextResponse.json({ userId: auth.userId });
10
- * });
11
- *
12
- * // With role requirement:
13
- * export const POST = withAuth(async (req, auth) => { ... }, { requiredRoles: ['admin'] });
14
- */
15
-
16
- import 'server-only';
17
- import { NextRequest, NextResponse } from 'next/server';
18
- import { decodeSession } from './decode-session';
19
- import type { SessionData } from '../lib/session-store';
20
-
21
- // =============================================================================
22
- // TYPES
23
- // =============================================================================
24
-
25
- export interface ApiAuthResult {
26
- userId: string;
27
- email: string;
28
- roles: string[];
29
- sessionData: SessionData;
30
- accessToken?: string;
31
- }
32
-
33
- export interface WithAuthOptions {
34
- /** Roles required to access the route (any match = allowed) */
35
- requiredRoles?: string[];
36
- }
37
-
38
- // =============================================================================
39
- // MAIN
40
- // =============================================================================
41
-
42
- /**
43
- * Wrap an API route handler with auth validation.
44
- * Returns 401 if not authenticated, 403 if missing required roles.
45
- */
46
- export function withAuth(
47
- handler: (req: NextRequest, auth: ApiAuthResult) => Promise<NextResponse>,
48
- options?: WithAuthOptions
49
- ): (req: NextRequest) => Promise<NextResponse> {
50
- return async (req: NextRequest): Promise<NextResponse> => {
51
- try {
52
- // Decode session from request cookies (direct Redis, no self-fetch)
53
- const decoded = await decodeSession(req.cookies);
54
-
55
- if (!decoded) {
56
- return NextResponse.json(
57
- { error: 'Unauthorized', message: 'No valid session' },
58
- { status: 401 }
59
- );
60
- }
61
-
62
- const { sessionData } = decoded;
63
-
64
- // Check required roles
65
- if (options?.requiredRoles && options.requiredRoles.length > 0) {
66
- const userRoles = sessionData.roles || [];
67
- const hasRole = options.requiredRoles.some(r => userRoles.includes(r));
68
- if (!hasRole) {
69
- return NextResponse.json(
70
- { error: 'Forbidden', message: 'Insufficient permissions' },
71
- { status: 403 }
72
- );
73
- }
74
- }
75
-
76
- const auth: ApiAuthResult = {
77
- userId: sessionData.userId,
78
- email: sessionData.email,
79
- roles: sessionData.roles || [],
80
- sessionData,
81
- accessToken: sessionData.idpAccessToken,
82
- };
83
-
84
- return handler(req, auth);
85
- } catch (error) {
86
- console.error('[WITH-AUTH] Error:', error instanceof Error ? error.message : String(error));
87
- return NextResponse.json(
88
- { error: 'Internal Server Error', message: 'Auth check failed' },
89
- { status: 500 }
90
- );
91
- }
92
- };
93
- }
1
+ /**
2
+ * Server-Side Auth Wrapper for API Routes & Server Actions
3
+ *
4
+ * Wraps route handlers with session validation. Uses direct Redis reads.
5
+ * Zero HTTP self-fetches.
6
+ *
7
+ * Usage:
8
+ * export const GET = withAuth(async (req, auth) => {
9
+ * return NextResponse.json({ userId: auth.userId });
10
+ * });
11
+ *
12
+ * // With role requirement:
13
+ * export const POST = withAuth(async (req, auth) => { ... }, { requiredRoles: ['admin'] });
14
+ */
15
+
16
+ import 'server-only';
17
+ import { NextRequest, NextResponse } from 'next/server';
18
+ import { decodeSession } from './decode-session';
19
+ import type { SessionData } from '../lib/session-store';
20
+
21
+ // =============================================================================
22
+ // TYPES
23
+ // =============================================================================
24
+
25
+ export interface ApiAuthResult {
26
+ userId: string;
27
+ email: string;
28
+ roles: string[];
29
+ sessionData: SessionData;
30
+ accessToken?: string;
31
+ }
32
+
33
+ export interface WithAuthOptions {
34
+ /** Roles required to access the route (any match = allowed) */
35
+ requiredRoles?: string[];
36
+ }
37
+
38
+ // =============================================================================
39
+ // MAIN
40
+ // =============================================================================
41
+
42
+ /**
43
+ * Wrap an API route handler with auth validation.
44
+ * Returns 401 if not authenticated, 403 if missing required roles.
45
+ */
46
+ export function withAuth(
47
+ handler: (req: NextRequest, auth: ApiAuthResult) => Promise<NextResponse>,
48
+ options?: WithAuthOptions
49
+ ): (req: NextRequest) => Promise<NextResponse> {
50
+ return async (req: NextRequest): Promise<NextResponse> => {
51
+ try {
52
+ // Decode session from request cookies (direct Redis, no self-fetch)
53
+ const decoded = await decodeSession(req.cookies);
54
+
55
+ if (!decoded) {
56
+ return NextResponse.json(
57
+ { error: 'Unauthorized', message: 'No valid session' },
58
+ { status: 401 }
59
+ );
60
+ }
61
+
62
+ const { sessionData } = decoded;
63
+
64
+ // Check required roles
65
+ if (options?.requiredRoles && options.requiredRoles.length > 0) {
66
+ const userRoles = sessionData.roles || [];
67
+ const hasRole = options.requiredRoles.some(r => userRoles.includes(r));
68
+ if (!hasRole) {
69
+ return NextResponse.json(
70
+ { error: 'Forbidden', message: 'Insufficient permissions' },
71
+ { status: 403 }
72
+ );
73
+ }
74
+ }
75
+
76
+ const auth: ApiAuthResult = {
77
+ userId: sessionData.userId,
78
+ email: sessionData.email,
79
+ roles: sessionData.roles || [],
80
+ sessionData,
81
+ accessToken: sessionData.idpAccessToken,
82
+ };
83
+
84
+ return handler(req, auth);
85
+ } catch (error) {
86
+ console.error('[WITH-AUTH] Error:', error instanceof Error ? error.message : String(error));
87
+ return NextResponse.json(
88
+ { error: 'Internal Server Error', message: 'Auth check failed' },
89
+ { status: 500 }
90
+ );
91
+ }
92
+ };
93
+ }