@payez/next-mvp 4.1.10 → 4.1.12

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 (89) hide show
  1. package/dist/api-handlers/auth/refresh.js +15 -38
  2. package/dist/auth/better-auth.js +9 -18
  3. package/dist/client/better-auth-client.d.ts +14 -2
  4. package/dist/lib/ensure-fresh-access-token.js +7 -30
  5. package/dist/lib/session-store.d.ts +0 -1
  6. package/dist/lib/session-store.js +29 -33
  7. package/dist/lib/startup-init.js +19 -19
  8. package/package.json +4 -3
  9. package/scripts/check-internal-url-usage.sh +73 -73
  10. package/src/api/index.ts +17 -17
  11. package/src/api-handlers/account/masked-info.ts +45 -45
  12. package/src/api-handlers/account/profile.ts +80 -80
  13. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  14. package/src/api-handlers/account/recovery/send-code.ts +25 -25
  15. package/src/api-handlers/account/recovery/verify-code.ts +25 -25
  16. package/src/api-handlers/account/reset-password.ts +23 -23
  17. package/src/api-handlers/account/send-code.ts +76 -76
  18. package/src/api-handlers/account/validate-password.ts +118 -118
  19. package/src/api-handlers/admin/stats.ts +249 -249
  20. package/src/api-handlers/anon/preferences.ts +123 -123
  21. package/src/api-handlers/auth/jwks.ts +20 -20
  22. package/src/api-handlers/auth/login.ts +240 -240
  23. package/src/api-handlers/auth/refresh.ts +15 -38
  24. package/src/api-handlers/auth/validate.ts +43 -43
  25. package/src/api-handlers/session/viability.ts +142 -142
  26. package/src/auth/better-auth.ts +9 -14
  27. package/src/auth/unauthenticated-routes.ts +2 -2
  28. package/src/client/better-auth-client.ts +70 -70
  29. package/src/client/fetchWithSession.ts +21 -21
  30. package/src/client/index.ts +13 -13
  31. package/src/client/useAnonSession.ts +131 -131
  32. package/src/components/admin/VibeAdminContext.tsx +87 -87
  33. package/src/config/logging-config.ts +82 -82
  34. package/src/config/unauthenticated-routes.ts +19 -19
  35. package/src/edge/internal-api-url.ts +65 -65
  36. package/src/edge/middleware.ts +42 -42
  37. package/src/hooks/useProfile.ts +75 -75
  38. package/src/lib/anon-session.ts +213 -213
  39. package/src/lib/auth-secret.ts +137 -137
  40. package/src/lib/demo-mode.ts +17 -17
  41. package/src/lib/ensure-fresh-access-token.ts +7 -30
  42. package/src/lib/idp-client-config.ts +559 -559
  43. package/src/lib/idp-fetch.ts +126 -126
  44. package/src/lib/internal-api.ts +171 -171
  45. package/src/lib/jwt-decode-client.ts +45 -45
  46. package/src/lib/rate-limit-service.ts +9 -9
  47. package/src/lib/redis.ts +26 -26
  48. package/src/lib/refresh-token-validator.ts +64 -64
  49. package/src/lib/secret-validation.ts +8 -8
  50. package/src/lib/session-store.ts +692 -695
  51. package/src/lib/session.ts +34 -34
  52. package/src/lib/startup-init.ts +251 -251
  53. package/src/lib/token-expiry.ts +40 -40
  54. package/src/lib/token-lifecycle.ts +470 -470
  55. package/src/lib/types/api-responses.ts +336 -336
  56. package/src/middleware/auth-decision.ts +43 -43
  57. package/src/models/DecodedAccessToken.ts +16 -16
  58. package/src/models/SessionModel.ts +264 -264
  59. package/src/pages/admin-roles/index.ts +9 -9
  60. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  61. package/src/routes/account/index.ts +31 -31
  62. package/src/routes/account/masked-info.ts +36 -36
  63. package/src/routes/account/send-code.ts +39 -39
  64. package/src/routes/account/verify-email.ts +40 -40
  65. package/src/routes/account/verify-sms.ts +40 -40
  66. package/src/routes/auth/index.ts +22 -22
  67. package/src/routes/auth/refresh.ts +48 -48
  68. package/src/routes/auth/session.ts +166 -166
  69. package/src/routes/index.ts +17 -17
  70. package/src/routes/session/index.ts +7 -7
  71. package/src/routes/session/refresh-viability.ts +17 -17
  72. package/src/server/auth.ts +373 -373
  73. package/src/server/decode-session.ts +202 -202
  74. package/src/services/signalrActivityService.ts +258 -258
  75. package/src/theme/ThemeProvider.tsx +39 -39
  76. package/src/theme/default.ts +33 -33
  77. package/src/theme/index.ts +31 -31
  78. package/src/theme/types.ts +69 -69
  79. package/src/theme/useTheme.ts +57 -57
  80. package/src/theme/utils.ts +40 -40
  81. package/src/types/api.ts +12 -12
  82. package/src/types/auth.d.ts +23 -23
  83. package/src/types/auth.ts +22 -22
  84. package/src/types/logging.ts +11 -11
  85. package/src/types/recovery.ts +54 -54
  86. package/src/utils/api.ts +353 -353
  87. package/src/utils/circuitBreaker.ts +40 -40
  88. package/src/utils/error-message.ts +108 -108
  89. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,45 +1,45 @@
1
- /**
2
- * Client-safe JWT decode (no Node.js dependencies)
3
- * This is a lightweight version for browser usage
4
- */
5
-
6
- // Decode base64url
7
- function base64urlDecode(base64url: string): string {
8
- try {
9
- // Convert base64url to base64
10
- let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
11
- // Add padding if needed
12
- while (base64.length % 4) {
13
- base64 += '=';
14
- }
15
- return atob(base64);
16
- } catch (e) {
17
- throw new Error('Invalid base64url encoding');
18
- }
19
- }
20
-
21
- /**
22
- * Simple JWT decode for client-side use (no signature verification)
23
- * @param token - JWT token string
24
- * @returns Decoded payload or null if invalid
25
- */
26
- export function jwtDecode<T = any>(token: string): T | null {
27
- if (!token) return null;
28
-
29
- try {
30
- const parts = token.split('.');
31
- if (parts.length < 2) {
32
- console.error('[JWT] Invalid token format');
33
- return null;
34
- }
35
-
36
- const payload = parts[1];
37
- const decoded = base64urlDecode(payload);
38
- const parsedPayload = JSON.parse(decoded);
39
-
40
- return parsedPayload;
41
- } catch (e) {
42
- console.error('[JWT] Decode failed:', e instanceof Error ? e.message : 'Unknown error');
43
- return null;
44
- }
45
- }
1
+ /**
2
+ * Client-safe JWT decode (no Node.js dependencies)
3
+ * This is a lightweight version for browser usage
4
+ */
5
+
6
+ // Decode base64url
7
+ function base64urlDecode(base64url: string): string {
8
+ try {
9
+ // Convert base64url to base64
10
+ let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
11
+ // Add padding if needed
12
+ while (base64.length % 4) {
13
+ base64 += '=';
14
+ }
15
+ return atob(base64);
16
+ } catch (e) {
17
+ throw new Error('Invalid base64url encoding');
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Simple JWT decode for client-side use (no signature verification)
23
+ * @param token - JWT token string
24
+ * @returns Decoded payload or null if invalid
25
+ */
26
+ export function jwtDecode<T = any>(token: string): T | null {
27
+ if (!token) return null;
28
+
29
+ try {
30
+ const parts = token.split('.');
31
+ if (parts.length < 2) {
32
+ console.error('[JWT] Invalid token format');
33
+ return null;
34
+ }
35
+
36
+ const payload = parts[1];
37
+ const decoded = base64urlDecode(payload);
38
+ const parsedPayload = JSON.parse(decoded);
39
+
40
+ return parsedPayload;
41
+ } catch (e) {
42
+ console.error('[JWT] Decode failed:', e instanceof Error ? e.message : 'Unknown error');
43
+ return null;
44
+ }
45
+ }
@@ -1,9 +1,9 @@
1
- import redis from '../lib/redis';
2
- import { logger } from '../config/logger';
3
-
4
- export interface RateLimitRule { endpoint: string; period: string; limit: number; }
5
- export interface RateLimitResult { isAllowed: boolean; requestCount: number; limit: number; retryAfterSeconds?: number; failedAttempts?: number; }
6
-
7
- export function createPayEzRateLimitResponse(retryAfterSeconds: number, remainingAttempts: number = 0) {
8
- return { success: false, message: 'Too many failed attempts', user_info: null, errors: [ { code: 'RateLimitExceeded', message: 'Too many failed authentication attempts', resolution: `Please try again in ${retryAfterSeconds} seconds`, remainingAttempts } ] };
9
- }
1
+ import redis from '../lib/redis';
2
+ import { logger } from '../config/logger';
3
+
4
+ export interface RateLimitRule { endpoint: string; period: string; limit: number; }
5
+ export interface RateLimitResult { isAllowed: boolean; requestCount: number; limit: number; retryAfterSeconds?: number; failedAttempts?: number; }
6
+
7
+ export function createPayEzRateLimitResponse(retryAfterSeconds: number, remainingAttempts: number = 0) {
8
+ return { success: false, message: 'Too many failed attempts', user_info: null, errors: [ { code: 'RateLimitExceeded', message: 'Too many failed authentication attempts', resolution: `Please try again in ${retryAfterSeconds} seconds`, remainingAttempts } ] };
9
+ }
package/src/lib/redis.ts CHANGED
@@ -1,27 +1,27 @@
1
- // E:\Repos\PayEz-Next-MVP\packages\next-mvp\src\lib\redis.ts
2
- import Redis, { RedisOptions } from 'ioredis';
3
-
4
- let client: Redis | null = null;
5
-
6
- function createClient(): Redis {
7
- const url = process.env.REDIS_URL;
8
-
9
- if (url && url.trim() !== '') {
10
- // Use a standard configuration for better Docker compatibility
11
- return new Redis(url);
12
- }
13
-
14
- // No REDIS_URL set, create a client that will fail fast.
15
- return new Redis({ lazyConnect: true });
16
- }
17
-
18
- export function getRedis(): Redis {
19
- if (!client) {
20
- client = createClient();
21
- }
22
- return client;
23
- }
24
-
25
- const redis = getRedis();
26
- export { redis };
1
+ // E:\Repos\PayEz-Next-MVP\packages\next-mvp\src\lib\redis.ts
2
+ import Redis, { RedisOptions } from 'ioredis';
3
+
4
+ let client: Redis | null = null;
5
+
6
+ function createClient(): Redis {
7
+ const url = process.env.REDIS_URL;
8
+
9
+ if (url && url.trim() !== '') {
10
+ // Use a standard configuration for better Docker compatibility
11
+ return new Redis(url);
12
+ }
13
+
14
+ // No REDIS_URL set, create a client that will fail fast.
15
+ return new Redis({ lazyConnect: true });
16
+ }
17
+
18
+ export function getRedis(): Redis {
19
+ if (!client) {
20
+ client = createClient();
21
+ }
22
+ return client;
23
+ }
24
+
25
+ const redis = getRedis();
26
+ export { redis };
27
27
  export default redis;
@@ -1,64 +1,64 @@
1
- import { jwtDecode } from './jwt-decode';
2
- import { tokenRefreshLogger } from '../config/logger';
3
-
4
- interface RefreshTokenPayload { exp: number; iat: number; jti: string; user_id: string; token_type: string; binding: string; }
5
-
6
- export function isRefreshTokenValid(token: string): boolean {
7
- if (!token) return false;
8
- try { const decoded = jwtDecode<RefreshTokenPayload>(token); if (!decoded) return false; const now = Math.floor(Date.now() / 1000); if (decoded.exp < now) return false; if (decoded.token_type !== 'refresh_token') return false; return true; } catch { return false; }
9
- }
10
-
11
- export function isRefreshTokenExpiring(token: string, bufferMinutes: number = 60): boolean {
12
- if (!token) return true;
13
- try { const decoded = jwtDecode<RefreshTokenPayload>(token); if (!decoded?.exp) return true; const now = Math.floor(Date.now() / 1000); const buffer = bufferMinutes * 60; return decoded.exp <= (now + buffer); } catch { return true; }
14
- }
15
-
16
- export function getRefreshTokenExpiration(token: string): number | null {
17
- if (!token) return null; try { const decoded = jwtDecode<RefreshTokenPayload>(token); if (!decoded?.exp) return null; return decoded.exp * 1000; } catch { return null; }
18
- }
19
-
20
- export function getRefreshTokenTimeRemaining(token: string): number | null {
21
- if (!token) return null; try { const decoded = jwtDecode<RefreshTokenPayload>(token); if (!decoded?.exp) return null; const now = Math.floor(Date.now() / 1000); const timeRemaining = decoded.exp - now; return timeRemaining > 0 ? timeRemaining : null; } catch { return null; }
22
- }
23
-
24
- export interface RefreshViabilityCheck {
25
- canRefresh: boolean;
26
- reason: 'valid_refresh_token' | 'no_refresh_token' | 'refresh_token_expired' | 'session_missing';
27
- timeRemaining?: number;
28
- expiresAt?: string;
29
- accessTokenExpired?: boolean;
30
- accessTokenTimeRemaining?: number;
31
- }
32
-
33
- export function checkRefreshViability(sessionData: any): RefreshViabilityCheck {
34
- if (!sessionData) return { canRefresh: false, reason: 'session_missing' };
35
- let accessTokenExpired = false; let accessTokenTimeRemaining: number | undefined;
36
- if (sessionData.idpAccessTokenExpires) {
37
- const now = Date.now(); let expiresAtMs = sessionData.idpAccessTokenExpires;
38
- if (typeof expiresAtMs === 'string') expiresAtMs = parseInt(expiresAtMs, 10);
39
- if (expiresAtMs < 1000000000000) expiresAtMs = expiresAtMs * 1000;
40
- accessTokenTimeRemaining = Math.floor((expiresAtMs - now) / 1000);
41
- const bufferSec = 5 * 60; // 5 minutes pre-expiry buffer
42
- accessTokenExpired = accessTokenTimeRemaining <= bufferSec;
43
- tokenRefreshLogger.debug('[REFRESH_VIABILITY] Access token expiration check', { now, expiresAtMs, accessTokenTimeRemaining, bufferSec, accessTokenExpired });
44
- }
45
- if (!sessionData.idpRefreshToken) return { canRefresh: false, reason: 'no_refresh_token', accessTokenExpired, accessTokenTimeRemaining };
46
- if (sessionData.idpRefreshTokenExpires) {
47
- let refreshExpMs = sessionData.idpRefreshTokenExpires;
48
- if (typeof refreshExpMs === 'string') refreshExpMs = parseInt(refreshExpMs, 10);
49
- if (refreshExpMs < 1000000000000) refreshExpMs = refreshExpMs * 1000;
50
- const nowMs = Date.now(); const timeRemainingSec = Math.floor((refreshExpMs - nowMs) / 1000);
51
- if (timeRemainingSec <= 0) return { canRefresh: false, reason: 'refresh_token_expired', accessTokenExpired, accessTokenTimeRemaining };
52
- return { canRefresh: true, reason: 'valid_refresh_token', timeRemaining: timeRemainingSec, expiresAt: new Date(refreshExpMs).toISOString(), accessTokenExpired, accessTokenTimeRemaining };
53
- }
54
- try {
55
- const decoded = jwtDecode<RefreshTokenPayload>(sessionData.idpRefreshToken); const nowSec = Math.floor(Date.now() / 1000);
56
- if (!decoded?.exp || decoded.token_type !== 'refresh_token') return { canRefresh: false, reason: 'refresh_token_expired', accessTokenExpired, accessTokenTimeRemaining };
57
- const timeRemaining = decoded.exp - nowSec; if (timeRemaining <= 0) return { canRefresh: false, reason: 'refresh_token_expired', accessTokenExpired, accessTokenTimeRemaining };
58
- const expiresAtIso = new Date(decoded.exp * 1000).toISOString();
59
- return { canRefresh: true, reason: 'valid_refresh_token', timeRemaining, expiresAt: expiresAtIso, accessTokenExpired, accessTokenTimeRemaining };
60
- } catch (error) {
61
- tokenRefreshLogger.debug('[REFRESH_VIABILITY] Failed to decode refresh token for viability', { error: error instanceof Error ? error.message : String(error) });
62
- return { canRefresh: false, reason: 'refresh_token_expired', accessTokenExpired, accessTokenTimeRemaining };
63
- }
64
- }
1
+ import { jwtDecode } from './jwt-decode';
2
+ import { tokenRefreshLogger } from '../config/logger';
3
+
4
+ interface RefreshTokenPayload { exp: number; iat: number; jti: string; user_id: string; token_type: string; binding: string; }
5
+
6
+ export function isRefreshTokenValid(token: string): boolean {
7
+ if (!token) return false;
8
+ try { const decoded = jwtDecode<RefreshTokenPayload>(token); if (!decoded) return false; const now = Math.floor(Date.now() / 1000); if (decoded.exp < now) return false; if (decoded.token_type !== 'refresh_token') return false; return true; } catch { return false; }
9
+ }
10
+
11
+ export function isRefreshTokenExpiring(token: string, bufferMinutes: number = 60): boolean {
12
+ if (!token) return true;
13
+ try { const decoded = jwtDecode<RefreshTokenPayload>(token); if (!decoded?.exp) return true; const now = Math.floor(Date.now() / 1000); const buffer = bufferMinutes * 60; return decoded.exp <= (now + buffer); } catch { return true; }
14
+ }
15
+
16
+ export function getRefreshTokenExpiration(token: string): number | null {
17
+ if (!token) return null; try { const decoded = jwtDecode<RefreshTokenPayload>(token); if (!decoded?.exp) return null; return decoded.exp * 1000; } catch { return null; }
18
+ }
19
+
20
+ export function getRefreshTokenTimeRemaining(token: string): number | null {
21
+ if (!token) return null; try { const decoded = jwtDecode<RefreshTokenPayload>(token); if (!decoded?.exp) return null; const now = Math.floor(Date.now() / 1000); const timeRemaining = decoded.exp - now; return timeRemaining > 0 ? timeRemaining : null; } catch { return null; }
22
+ }
23
+
24
+ export interface RefreshViabilityCheck {
25
+ canRefresh: boolean;
26
+ reason: 'valid_refresh_token' | 'no_refresh_token' | 'refresh_token_expired' | 'session_missing';
27
+ timeRemaining?: number;
28
+ expiresAt?: string;
29
+ accessTokenExpired?: boolean;
30
+ accessTokenTimeRemaining?: number;
31
+ }
32
+
33
+ export function checkRefreshViability(sessionData: any): RefreshViabilityCheck {
34
+ if (!sessionData) return { canRefresh: false, reason: 'session_missing' };
35
+ let accessTokenExpired = false; let accessTokenTimeRemaining: number | undefined;
36
+ if (sessionData.idpAccessTokenExpires) {
37
+ const now = Date.now(); let expiresAtMs = sessionData.idpAccessTokenExpires;
38
+ if (typeof expiresAtMs === 'string') expiresAtMs = parseInt(expiresAtMs, 10);
39
+ if (expiresAtMs < 1000000000000) expiresAtMs = expiresAtMs * 1000;
40
+ accessTokenTimeRemaining = Math.floor((expiresAtMs - now) / 1000);
41
+ const bufferSec = 5 * 60; // 5 minutes pre-expiry buffer
42
+ accessTokenExpired = accessTokenTimeRemaining <= bufferSec;
43
+ tokenRefreshLogger.debug('[REFRESH_VIABILITY] Access token expiration check', { now, expiresAtMs, accessTokenTimeRemaining, bufferSec, accessTokenExpired });
44
+ }
45
+ if (!sessionData.idpRefreshToken) return { canRefresh: false, reason: 'no_refresh_token', accessTokenExpired, accessTokenTimeRemaining };
46
+ if (sessionData.idpRefreshTokenExpires) {
47
+ let refreshExpMs = sessionData.idpRefreshTokenExpires;
48
+ if (typeof refreshExpMs === 'string') refreshExpMs = parseInt(refreshExpMs, 10);
49
+ if (refreshExpMs < 1000000000000) refreshExpMs = refreshExpMs * 1000;
50
+ const nowMs = Date.now(); const timeRemainingSec = Math.floor((refreshExpMs - nowMs) / 1000);
51
+ if (timeRemainingSec <= 0) return { canRefresh: false, reason: 'refresh_token_expired', accessTokenExpired, accessTokenTimeRemaining };
52
+ return { canRefresh: true, reason: 'valid_refresh_token', timeRemaining: timeRemainingSec, expiresAt: new Date(refreshExpMs).toISOString(), accessTokenExpired, accessTokenTimeRemaining };
53
+ }
54
+ try {
55
+ const decoded = jwtDecode<RefreshTokenPayload>(sessionData.idpRefreshToken); const nowSec = Math.floor(Date.now() / 1000);
56
+ if (!decoded?.exp || decoded.token_type !== 'refresh_token') return { canRefresh: false, reason: 'refresh_token_expired', accessTokenExpired, accessTokenTimeRemaining };
57
+ const timeRemaining = decoded.exp - nowSec; if (timeRemaining <= 0) return { canRefresh: false, reason: 'refresh_token_expired', accessTokenExpired, accessTokenTimeRemaining };
58
+ const expiresAtIso = new Date(decoded.exp * 1000).toISOString();
59
+ return { canRefresh: true, reason: 'valid_refresh_token', timeRemaining, expiresAt: expiresAtIso, accessTokenExpired, accessTokenTimeRemaining };
60
+ } catch (error) {
61
+ tokenRefreshLogger.debug('[REFRESH_VIABILITY] Failed to decode refresh token for viability', { error: error instanceof Error ? error.message : String(error) });
62
+ return { canRefresh: false, reason: 'refresh_token_expired', accessTokenExpired, accessTokenTimeRemaining };
63
+ }
64
+ }
@@ -1,8 +1,8 @@
1
- export function validateAuthSecret(secret: string): { valid: boolean; reason?: string } {
2
- if (!secret || typeof secret !== 'string') return { valid: false, reason: 'missing' };
3
- if (secret.length < 32) return { valid: false, reason: 'too_short' };
4
- const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^a-zA-Z0-9]/];
5
- const score = classes.reduce((acc, re) => acc + (re.test(secret) ? 1 : 0), 0);
6
- if (score < 3) return { valid: false, reason: 'weak' };
7
- return { valid: true };
8
- }
1
+ export function validateAuthSecret(secret: string): { valid: boolean; reason?: string } {
2
+ if (!secret || typeof secret !== 'string') return { valid: false, reason: 'missing' };
3
+ if (secret.length < 32) return { valid: false, reason: 'too_short' };
4
+ const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^a-zA-Z0-9]/];
5
+ const score = classes.reduce((acc, re) => acc + (re.test(secret) ? 1 : 0), 0);
6
+ if (score < 3) return { valid: false, reason: 'weak' };
7
+ return { valid: true };
8
+ }