@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,131 +1,131 @@
1
- /**
2
- * useAnonSession - React hook for anonymous session management
3
- *
4
- * Provides access to anonymous session preferences stored in Redis.
5
- * Works before user logs in, preferences persist across visits.
6
- */
7
-
8
- 'use client';
9
-
10
- import { useState, useEffect, useCallback } from 'react';
11
-
12
- export interface AnonPreferences {
13
- theme?: string;
14
- locale?: string;
15
- [key: string]: any;
16
- }
17
-
18
- export interface AnonMetrics {
19
- resumeGenerationCount?: number;
20
- firstVisit?: number;
21
- lastVisit?: number;
22
- visitCount?: number;
23
- [key: string]: any;
24
- }
25
-
26
- export interface AnonSession {
27
- id: string;
28
- preferences: AnonPreferences;
29
- metrics: AnonMetrics;
30
- }
31
-
32
- export interface UseAnonSessionReturn {
33
- session: AnonSession | null;
34
- isLoading: boolean;
35
- error: string | null;
36
- updatePreferences: (preferences: Partial<AnonPreferences>) => Promise<void>;
37
- setTheme: (theme: string) => Promise<void>;
38
- refresh: () => Promise<void>;
39
- }
40
-
41
- /**
42
- * Hook to manage anonymous session state
43
- */
44
- export function useAnonSession(): UseAnonSessionReturn {
45
- const [session, setSession] = useState<AnonSession | null>(null);
46
- const [isLoading, setIsLoading] = useState(true);
47
- const [error, setError] = useState<string | null>(null);
48
-
49
- // Fetch session on mount
50
- const fetchSession = useCallback(async () => {
51
- try {
52
- setIsLoading(true);
53
- setError(null);
54
-
55
- const response = await fetch('/api/anon/preferences', {
56
- method: 'GET',
57
- credentials: 'include', // Important for cookies
58
- });
59
-
60
- if (!response.ok) {
61
- throw new Error('Failed to fetch preferences');
62
- }
63
-
64
- const data = await response.json();
65
- if (data.success && data.data) {
66
- setSession({
67
- id: data.data.id,
68
- preferences: data.data.preferences || {},
69
- metrics: data.data.metrics || {},
70
- });
71
- }
72
- } catch (err) {
73
- console.error('[useAnonSession] Error fetching session:', err);
74
- setError(err instanceof Error ? err.message : 'Unknown error');
75
- } finally {
76
- setIsLoading(false);
77
- }
78
- }, []);
79
-
80
- useEffect(() => {
81
- fetchSession();
82
- }, [fetchSession]);
83
-
84
- // Update preferences
85
- const updatePreferences = useCallback(async (preferences: Partial<AnonPreferences>) => {
86
- try {
87
- setError(null);
88
-
89
- const response = await fetch('/api/anon/preferences', {
90
- method: 'POST',
91
- credentials: 'include',
92
- headers: {
93
- 'Content-Type': 'application/json',
94
- },
95
- body: JSON.stringify({ preferences }),
96
- });
97
-
98
- if (!response.ok) {
99
- throw new Error('Failed to update preferences');
100
- }
101
-
102
- const data = await response.json();
103
- if (data.success && data.data) {
104
- setSession(prev => prev ? {
105
- ...prev,
106
- preferences: data.data.preferences,
107
- } : null);
108
- }
109
- } catch (err) {
110
- console.error('[useAnonSession] Error updating preferences:', err);
111
- setError(err instanceof Error ? err.message : 'Unknown error');
112
- throw err;
113
- }
114
- }, []);
115
-
116
- // Convenience method to set theme
117
- const setTheme = useCallback(async (theme: string) => {
118
- await updatePreferences({ theme });
119
- }, [updatePreferences]);
120
-
121
- return {
122
- session,
123
- isLoading,
124
- error,
125
- updatePreferences,
126
- setTheme,
127
- refresh: fetchSession,
128
- };
129
- }
130
-
131
- export default useAnonSession;
1
+ /**
2
+ * useAnonSession - React hook for anonymous session management
3
+ *
4
+ * Provides access to anonymous session preferences stored in Redis.
5
+ * Works before user logs in, preferences persist across visits.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import { useState, useEffect, useCallback } from 'react';
11
+
12
+ export interface AnonPreferences {
13
+ theme?: string;
14
+ locale?: string;
15
+ [key: string]: any;
16
+ }
17
+
18
+ export interface AnonMetrics {
19
+ resumeGenerationCount?: number;
20
+ firstVisit?: number;
21
+ lastVisit?: number;
22
+ visitCount?: number;
23
+ [key: string]: any;
24
+ }
25
+
26
+ export interface AnonSession {
27
+ id: string;
28
+ preferences: AnonPreferences;
29
+ metrics: AnonMetrics;
30
+ }
31
+
32
+ export interface UseAnonSessionReturn {
33
+ session: AnonSession | null;
34
+ isLoading: boolean;
35
+ error: string | null;
36
+ updatePreferences: (preferences: Partial<AnonPreferences>) => Promise<void>;
37
+ setTheme: (theme: string) => Promise<void>;
38
+ refresh: () => Promise<void>;
39
+ }
40
+
41
+ /**
42
+ * Hook to manage anonymous session state
43
+ */
44
+ export function useAnonSession(): UseAnonSessionReturn {
45
+ const [session, setSession] = useState<AnonSession | null>(null);
46
+ const [isLoading, setIsLoading] = useState(true);
47
+ const [error, setError] = useState<string | null>(null);
48
+
49
+ // Fetch session on mount
50
+ const fetchSession = useCallback(async () => {
51
+ try {
52
+ setIsLoading(true);
53
+ setError(null);
54
+
55
+ const response = await fetch('/api/anon/preferences', {
56
+ method: 'GET',
57
+ credentials: 'include', // Important for cookies
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error('Failed to fetch preferences');
62
+ }
63
+
64
+ const data = await response.json();
65
+ if (data.success && data.data) {
66
+ setSession({
67
+ id: data.data.id,
68
+ preferences: data.data.preferences || {},
69
+ metrics: data.data.metrics || {},
70
+ });
71
+ }
72
+ } catch (err) {
73
+ console.error('[useAnonSession] Error fetching session:', err);
74
+ setError(err instanceof Error ? err.message : 'Unknown error');
75
+ } finally {
76
+ setIsLoading(false);
77
+ }
78
+ }, []);
79
+
80
+ useEffect(() => {
81
+ fetchSession();
82
+ }, [fetchSession]);
83
+
84
+ // Update preferences
85
+ const updatePreferences = useCallback(async (preferences: Partial<AnonPreferences>) => {
86
+ try {
87
+ setError(null);
88
+
89
+ const response = await fetch('/api/anon/preferences', {
90
+ method: 'POST',
91
+ credentials: 'include',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ },
95
+ body: JSON.stringify({ preferences }),
96
+ });
97
+
98
+ if (!response.ok) {
99
+ throw new Error('Failed to update preferences');
100
+ }
101
+
102
+ const data = await response.json();
103
+ if (data.success && data.data) {
104
+ setSession(prev => prev ? {
105
+ ...prev,
106
+ preferences: data.data.preferences,
107
+ } : null);
108
+ }
109
+ } catch (err) {
110
+ console.error('[useAnonSession] Error updating preferences:', err);
111
+ setError(err instanceof Error ? err.message : 'Unknown error');
112
+ throw err;
113
+ }
114
+ }, []);
115
+
116
+ // Convenience method to set theme
117
+ const setTheme = useCallback(async (theme: string) => {
118
+ await updatePreferences({ theme });
119
+ }, [updatePreferences]);
120
+
121
+ return {
122
+ session,
123
+ isLoading,
124
+ error,
125
+ updatePreferences,
126
+ setTheme,
127
+ refresh: fetchSession,
128
+ };
129
+ }
130
+
131
+ export default useAnonSession;
@@ -1,87 +1,87 @@
1
- 'use client';
2
-
3
- import React, { createContext, useContext, ReactNode } from 'react';
4
-
5
- // -----------------------------------------------------------------------------
6
- // TYPES
7
- // -----------------------------------------------------------------------------
8
-
9
- export interface VibeAdminConfig {
10
- /** Collection name for this tenant (e.g., "ideal_resume", "idealvibe_app") */
11
- collectionName: string;
12
- /** Display name for the app (e.g., "IdealResume", "IdealVibe") */
13
- appName: string;
14
- /** Optional logo URL */
15
- logoUrl?: string;
16
- /** Base path for admin routes (default: /vibe-admin) */
17
- basePath?: string;
18
- /** Custom tabs to add to the admin panel */
19
- customTabs?: AdminTab[];
20
- /** Theme mode override */
21
- isDarkMode?: boolean;
22
- }
23
-
24
- export interface AdminTab {
25
- id: string;
26
- label: string;
27
- icon?: React.ElementType;
28
- component: React.ComponentType;
29
- }
30
-
31
- export interface VibeAdminContextValue extends VibeAdminConfig {
32
- /** All tabs including defaults and custom */
33
- allTabs: AdminTab[];
34
- }
35
-
36
- // -----------------------------------------------------------------------------
37
- // CONTEXT
38
- // -----------------------------------------------------------------------------
39
-
40
- const VibeAdminContext = createContext<VibeAdminContextValue | null>(null);
41
-
42
- // -----------------------------------------------------------------------------
43
- // HOOK
44
- // -----------------------------------------------------------------------------
45
-
46
- export function useVibeAdmin(): VibeAdminContextValue {
47
- const context = useContext(VibeAdminContext);
48
- if (!context) {
49
- throw new Error('useVibeAdmin must be used within a VibeAdminProvider');
50
- }
51
- return context;
52
- }
53
-
54
- // -----------------------------------------------------------------------------
55
- // PROVIDER
56
- // -----------------------------------------------------------------------------
57
-
58
- interface VibeAdminProviderProps {
59
- config: VibeAdminConfig;
60
- children: ReactNode;
61
- }
62
-
63
- export function VibeAdminProvider({ config, children }: VibeAdminProviderProps) {
64
- // Default tabs - will be populated with actual components
65
- const defaultTabs: AdminTab[] = [
66
- { id: 'stats', label: 'Stats', component: () => null },
67
- { id: 'users', label: 'Users', component: () => null },
68
- { id: 'sessions', label: 'Sessions', component: () => null },
69
- { id: 'analytics', label: 'Analytics', component: () => null },
70
- ];
71
-
72
- const allTabs = [...defaultTabs, ...(config.customTabs || [])];
73
-
74
- const value: VibeAdminContextValue = {
75
- ...config,
76
- basePath: config.basePath || '/vibe-admin',
77
- allTabs,
78
- };
79
-
80
- return (
81
- <VibeAdminContext.Provider value={value}>
82
- {children}
83
- </VibeAdminContext.Provider>
84
- );
85
- }
86
-
87
- export default VibeAdminProvider;
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, ReactNode } from 'react';
4
+
5
+ // -----------------------------------------------------------------------------
6
+ // TYPES
7
+ // -----------------------------------------------------------------------------
8
+
9
+ export interface VibeAdminConfig {
10
+ /** Collection name for this tenant (e.g., "ideal_resume", "idealvibe_app") */
11
+ collectionName: string;
12
+ /** Display name for the app (e.g., "IdealResume", "IdealVibe") */
13
+ appName: string;
14
+ /** Optional logo URL */
15
+ logoUrl?: string;
16
+ /** Base path for admin routes (default: /vibe-admin) */
17
+ basePath?: string;
18
+ /** Custom tabs to add to the admin panel */
19
+ customTabs?: AdminTab[];
20
+ /** Theme mode override */
21
+ isDarkMode?: boolean;
22
+ }
23
+
24
+ export interface AdminTab {
25
+ id: string;
26
+ label: string;
27
+ icon?: React.ElementType;
28
+ component: React.ComponentType;
29
+ }
30
+
31
+ export interface VibeAdminContextValue extends VibeAdminConfig {
32
+ /** All tabs including defaults and custom */
33
+ allTabs: AdminTab[];
34
+ }
35
+
36
+ // -----------------------------------------------------------------------------
37
+ // CONTEXT
38
+ // -----------------------------------------------------------------------------
39
+
40
+ const VibeAdminContext = createContext<VibeAdminContextValue | null>(null);
41
+
42
+ // -----------------------------------------------------------------------------
43
+ // HOOK
44
+ // -----------------------------------------------------------------------------
45
+
46
+ export function useVibeAdmin(): VibeAdminContextValue {
47
+ const context = useContext(VibeAdminContext);
48
+ if (!context) {
49
+ throw new Error('useVibeAdmin must be used within a VibeAdminProvider');
50
+ }
51
+ return context;
52
+ }
53
+
54
+ // -----------------------------------------------------------------------------
55
+ // PROVIDER
56
+ // -----------------------------------------------------------------------------
57
+
58
+ interface VibeAdminProviderProps {
59
+ config: VibeAdminConfig;
60
+ children: ReactNode;
61
+ }
62
+
63
+ export function VibeAdminProvider({ config, children }: VibeAdminProviderProps) {
64
+ // Default tabs - will be populated with actual components
65
+ const defaultTabs: AdminTab[] = [
66
+ { id: 'stats', label: 'Stats', component: () => null },
67
+ { id: 'users', label: 'Users', component: () => null },
68
+ { id: 'sessions', label: 'Sessions', component: () => null },
69
+ { id: 'analytics', label: 'Analytics', component: () => null },
70
+ ];
71
+
72
+ const allTabs = [...defaultTabs, ...(config.customTabs || [])];
73
+
74
+ const value: VibeAdminContextValue = {
75
+ ...config,
76
+ basePath: config.basePath || '/vibe-admin',
77
+ allTabs,
78
+ };
79
+
80
+ return (
81
+ <VibeAdminContext.Provider value={value}>
82
+ {children}
83
+ </VibeAdminContext.Provider>
84
+ );
85
+ }
86
+
87
+ export default VibeAdminProvider;
@@ -1,82 +1,82 @@
1
- import { promises as fs } from 'fs';
2
- import { watch, existsSync } from 'fs';
3
- import { LoggingConfig, LogLevel } from '../types/logging';
4
-
5
- const isEdgeRuntime = typeof (globalThis as any).EdgeRuntime !== 'undefined' || process.env.NEXT_RUNTIME === 'edge';
6
-
7
- class LoggingConfigManager {
8
- private config: LoggingConfig | null = null;
9
- private configPath: string;
10
- private watchers: Set<(config: LoggingConfig) => void> = new Set();
11
- private fileWatcher: ReturnType<typeof watch> | null = null;
12
- private isEdgeRuntime: boolean;
13
- private isBrowser: boolean;
14
-
15
- constructor() {
16
- this.isBrowser = typeof window !== 'undefined';
17
- this.isEdgeRuntime = typeof (globalThis as any).EdgeRuntime !== 'undefined' || process.env.NEXT_RUNTIME === 'edge';
18
- if (this.isEdgeRuntime || this.isBrowser) {
19
- this.configPath = '/config/logging.json';
20
- this.config = this.getDefaultConfig();
21
- } else {
22
- const cwd = process.cwd().replace(/\\/g, '/');
23
- this.configPath = cwd + '/config/logging.json';
24
- }
25
- }
26
-
27
- async loadConfig(): Promise<LoggingConfig> {
28
- if (this.isEdgeRuntime || this.isBrowser) {
29
- if (!this.config) { this.config = this.getDefaultConfig(); }
30
- return this.config;
31
- }
32
- try {
33
- const configContent = await fs.readFile(this.configPath, 'utf-8');
34
- const rawConfig = JSON.parse(configContent);
35
- const config = this.validateAndMergeConfig(rawConfig);
36
- this.config = config; this.notifyWatchers(config); return config;
37
- } catch (error) {
38
- console.warn(`Failed to load logging config from ${this.configPath}:`, error);
39
- return this.getDefaultConfig();
40
- }
41
- }
42
-
43
- async getConfig(): Promise<LoggingConfig> { if (!this.config) { return await this.loadConfig(); } return this.config; }
44
- getConfigSync(): LoggingConfig { return this.config || this.getDefaultConfig(); }
45
-
46
- startWatching(): void { if (this.isEdgeRuntime || this.isBrowser) { return; } if (this.fileWatcher) { return; } try { if (!existsSync(this.configPath)) { console.warn(`Logging config file not found at ${this.configPath} — watch disabled`); return; } this.fileWatcher = watch(this.configPath, { persistent: false }, (eventType) => { if (eventType === 'change') { this.loadConfig().catch(console.error); } }); } catch (error) { console.warn('Failed to start watching logging config file:', error); } }
47
- stopWatching(): void { if (this.isEdgeRuntime || this.isBrowser) { return; } if (this.fileWatcher) { this.fileWatcher.close(); this.fileWatcher = null; } }
48
- addWatcher(callback: (config: LoggingConfig) => void): void { this.watchers.add(callback); }
49
- removeWatcher(callback: (config: LoggingConfig) => void): void { this.watchers.delete(callback); }
50
- private notifyWatchers(config: LoggingConfig): void { this.watchers.forEach(cb => { try { cb(config); } catch (error) { console.error('Error in logging config watcher:', error); } }); }
51
-
52
- private validateAndMergeConfig(rawConfig: any): LoggingConfig {
53
- const defaults = this.getDefaultConfig();
54
- const config: LoggingConfig = {
55
- logLevel: this.validateLogLevel(rawConfig.logLevel) || this.validateLogLevel(process.env.LOG_LEVEL) || defaults.logLevel,
56
- console: { enabled: rawConfig.console?.enabled ?? (process.env.LOG_CONSOLE === 'true' || process.env.NODE_ENV === 'development'), colors: rawConfig.console?.colors ?? defaults.console.colors },
57
- graylog: { enabled: false, host: defaults.graylog.host, port: defaults.graylog.port, facility: defaults.graylog.facility, staticMeta: { ...defaults.graylog.staticMeta } },
58
- components: { ...defaults.components, ...rawConfig.components },
59
- levels: { ...defaults.levels, ...rawConfig.levels }
60
- };
61
- return config;
62
- }
63
- private validateLogLevel(level: any): LogLevel | null { const validLevels: LogLevel[] = ['error', 'warn', 'info', 'http', 'debug']; return validLevels.includes(level) ? level : null; }
64
- private getHostname(): string { if (this.isEdgeRuntime) return 'edge-runtime'; if (this.isBrowser) return 'browser-client'; try { return require('os').hostname(); } catch { return 'unknown'; } }
65
- private getDefaultConfig(): LoggingConfig {
66
- return {
67
- logLevel: 'info',
68
- console: { enabled: process.env.NODE_ENV === 'development', colors: true },
69
- graylog: { enabled: false, host: '127.0.0.1', port: 12201, facility: 'next-mvp', staticMeta: { service: 'next-mvp', application: 'next-mvp', version: '1.0.0', environment: process.env.NODE_ENV || 'development', hostname: this.getHostname(), instance: process.env.NEXT_INSTANCE_ID || 'unknown' } },
70
- components: { auth: { enabled: true, prefix: '[AUTH]' }, idp: { enabled: true, prefix: '[IDP]' }, api: { enabled: true, prefix: '[API]' }, circuitBreaker: { enabled: true, prefix: '[CIRCUIT-BREAKER]' }, token: { enabled: true, prefix: '[TOKEN]' }, tokenSync: { enabled: true, prefix: '[TOKEN-SYNC]' }, tokenRefresh: { enabled: true, prefix: '[TOKEN-REFRESH]' }, redis: { enabled: true, prefix: '[REDIS]' } },
71
- levels: { error: 0, warn: 1, info: 2, http: 3, debug: 4 }
72
- };
73
- }
74
- }
75
-
76
- export const loggingConfigManager = new LoggingConfigManager();
77
- export const getLoggingConfig = () => loggingConfigManager.getConfig();
78
- export const getLoggingConfigSync = () => loggingConfigManager.getConfigSync();
79
- export const startConfigWatching = () => loggingConfigManager.startWatching();
80
- export const stopConfigWatching = () => loggingConfigManager.stopWatching();
81
- export const addConfigWatcher = (callback: (config: LoggingConfig) => void) => loggingConfigManager.addWatcher(callback);
82
- export const removeConfigWatcher = (callback: (config: LoggingConfig) => void) => loggingConfigManager.removeWatcher(callback);
1
+ import { promises as fs } from 'fs';
2
+ import { watch, existsSync } from 'fs';
3
+ import { LoggingConfig, LogLevel } from '../types/logging';
4
+
5
+ const isEdgeRuntime = typeof (globalThis as any).EdgeRuntime !== 'undefined' || process.env.NEXT_RUNTIME === 'edge';
6
+
7
+ class LoggingConfigManager {
8
+ private config: LoggingConfig | null = null;
9
+ private configPath: string;
10
+ private watchers: Set<(config: LoggingConfig) => void> = new Set();
11
+ private fileWatcher: ReturnType<typeof watch> | null = null;
12
+ private isEdgeRuntime: boolean;
13
+ private isBrowser: boolean;
14
+
15
+ constructor() {
16
+ this.isBrowser = typeof window !== 'undefined';
17
+ this.isEdgeRuntime = typeof (globalThis as any).EdgeRuntime !== 'undefined' || process.env.NEXT_RUNTIME === 'edge';
18
+ if (this.isEdgeRuntime || this.isBrowser) {
19
+ this.configPath = '/config/logging.json';
20
+ this.config = this.getDefaultConfig();
21
+ } else {
22
+ const cwd = process.cwd().replace(/\\/g, '/');
23
+ this.configPath = cwd + '/config/logging.json';
24
+ }
25
+ }
26
+
27
+ async loadConfig(): Promise<LoggingConfig> {
28
+ if (this.isEdgeRuntime || this.isBrowser) {
29
+ if (!this.config) { this.config = this.getDefaultConfig(); }
30
+ return this.config;
31
+ }
32
+ try {
33
+ const configContent = await fs.readFile(this.configPath, 'utf-8');
34
+ const rawConfig = JSON.parse(configContent);
35
+ const config = this.validateAndMergeConfig(rawConfig);
36
+ this.config = config; this.notifyWatchers(config); return config;
37
+ } catch (error) {
38
+ console.warn(`Failed to load logging config from ${this.configPath}:`, error);
39
+ return this.getDefaultConfig();
40
+ }
41
+ }
42
+
43
+ async getConfig(): Promise<LoggingConfig> { if (!this.config) { return await this.loadConfig(); } return this.config; }
44
+ getConfigSync(): LoggingConfig { return this.config || this.getDefaultConfig(); }
45
+
46
+ startWatching(): void { if (this.isEdgeRuntime || this.isBrowser) { return; } if (this.fileWatcher) { return; } try { if (!existsSync(this.configPath)) { console.warn(`Logging config file not found at ${this.configPath} — watch disabled`); return; } this.fileWatcher = watch(this.configPath, { persistent: false }, (eventType) => { if (eventType === 'change') { this.loadConfig().catch(console.error); } }); } catch (error) { console.warn('Failed to start watching logging config file:', error); } }
47
+ stopWatching(): void { if (this.isEdgeRuntime || this.isBrowser) { return; } if (this.fileWatcher) { this.fileWatcher.close(); this.fileWatcher = null; } }
48
+ addWatcher(callback: (config: LoggingConfig) => void): void { this.watchers.add(callback); }
49
+ removeWatcher(callback: (config: LoggingConfig) => void): void { this.watchers.delete(callback); }
50
+ private notifyWatchers(config: LoggingConfig): void { this.watchers.forEach(cb => { try { cb(config); } catch (error) { console.error('Error in logging config watcher:', error); } }); }
51
+
52
+ private validateAndMergeConfig(rawConfig: any): LoggingConfig {
53
+ const defaults = this.getDefaultConfig();
54
+ const config: LoggingConfig = {
55
+ logLevel: this.validateLogLevel(rawConfig.logLevel) || this.validateLogLevel(process.env.LOG_LEVEL) || defaults.logLevel,
56
+ console: { enabled: rawConfig.console?.enabled ?? (process.env.LOG_CONSOLE === 'true' || process.env.NODE_ENV === 'development'), colors: rawConfig.console?.colors ?? defaults.console.colors },
57
+ graylog: { enabled: false, host: defaults.graylog.host, port: defaults.graylog.port, facility: defaults.graylog.facility, staticMeta: { ...defaults.graylog.staticMeta } },
58
+ components: { ...defaults.components, ...rawConfig.components },
59
+ levels: { ...defaults.levels, ...rawConfig.levels }
60
+ };
61
+ return config;
62
+ }
63
+ private validateLogLevel(level: any): LogLevel | null { const validLevels: LogLevel[] = ['error', 'warn', 'info', 'http', 'debug']; return validLevels.includes(level) ? level : null; }
64
+ private getHostname(): string { if (this.isEdgeRuntime) return 'edge-runtime'; if (this.isBrowser) return 'browser-client'; try { return require('os').hostname(); } catch { return 'unknown'; } }
65
+ private getDefaultConfig(): LoggingConfig {
66
+ return {
67
+ logLevel: 'info',
68
+ console: { enabled: process.env.NODE_ENV === 'development', colors: true },
69
+ graylog: { enabled: false, host: '127.0.0.1', port: 12201, facility: 'next-mvp', staticMeta: { service: 'next-mvp', application: 'next-mvp', version: '1.0.0', environment: process.env.NODE_ENV || 'development', hostname: this.getHostname(), instance: process.env.NEXT_INSTANCE_ID || 'unknown' } },
70
+ components: { auth: { enabled: true, prefix: '[AUTH]' }, idp: { enabled: true, prefix: '[IDP]' }, api: { enabled: true, prefix: '[API]' }, circuitBreaker: { enabled: true, prefix: '[CIRCUIT-BREAKER]' }, token: { enabled: true, prefix: '[TOKEN]' }, tokenSync: { enabled: true, prefix: '[TOKEN-SYNC]' }, tokenRefresh: { enabled: true, prefix: '[TOKEN-REFRESH]' }, redis: { enabled: true, prefix: '[REDIS]' } },
71
+ levels: { error: 0, warn: 1, info: 2, http: 3, debug: 4 }
72
+ };
73
+ }
74
+ }
75
+
76
+ export const loggingConfigManager = new LoggingConfigManager();
77
+ export const getLoggingConfig = () => loggingConfigManager.getConfig();
78
+ export const getLoggingConfigSync = () => loggingConfigManager.getConfigSync();
79
+ export const startConfigWatching = () => loggingConfigManager.startWatching();
80
+ export const stopConfigWatching = () => loggingConfigManager.stopWatching();
81
+ export const addConfigWatcher = (callback: (config: LoggingConfig) => void) => loggingConfigManager.addWatcher(callback);
82
+ export const removeConfigWatcher = (callback: (config: LoggingConfig) => void) => loggingConfigManager.removeWatcher(callback);
@@ -1,19 +1,19 @@
1
- export interface UnauthenticatedRouteConfig { pattern: string; description: string; allowDuringCircuitBreakerOpen?: boolean; requiresRateLimit?: boolean; }
2
- export const UNAUTHENTICATED_ROUTES: UnauthenticatedRouteConfig[] = [
3
- { pattern: '/account-auth/*', description: 'All authentication pages', allowDuringCircuitBreakerOpen: true },
4
- { pattern: '/account-auth/login', description: 'Login page', allowDuringCircuitBreakerOpen: true },
5
- { pattern: '/account-auth/verify-code', description: '2FA page', allowDuringCircuitBreakerOpen: true },
6
- { pattern: '/api/auth/*', description: 'Auth API endpoints', allowDuringCircuitBreakerOpen: true, requiresRateLimit: true },
7
- { pattern: '/api/session/refresh-viability', description: 'Refresh viability check', allowDuringCircuitBreakerOpen: true },
8
- { pattern: '/landing', description: 'Public landing', allowDuringCircuitBreakerOpen: true },
9
- { pattern: '/service-unavailable', description: 'Service unavailable', allowDuringCircuitBreakerOpen: true },
10
- { pattern: '/favicon.ico', description: 'Favicon', allowDuringCircuitBreakerOpen: true },
11
- { pattern: '/robots.txt', description: 'Robots.txt', allowDuringCircuitBreakerOpen: true },
12
- { pattern: '/sitemap.xml', description: 'Sitemap', allowDuringCircuitBreakerOpen: true },
13
- { pattern: '/_next/*', description: 'Next.js assets', allowDuringCircuitBreakerOpen: true },
14
- { pattern: '/public/*', description: 'Public assets', allowDuringCircuitBreakerOpen: true }
15
- ];
16
- export function isUnauthenticatedRoute(pathname: string): boolean { return UNAUTHENTICATED_ROUTES.some(route => route.pattern.endsWith('*') ? pathname.startsWith(route.pattern.slice(0, -1)) : pathname === route.pattern); }
17
- export function isAllowedDuringCircuitBreakerOpen(pathname: string): boolean { const route = UNAUTHENTICATED_ROUTES.find(route => route.pattern.endsWith('*') ? pathname.startsWith(route.pattern.slice(0, -1)) : pathname === route.pattern); return route?.allowDuringCircuitBreakerOpen === true; }
18
- export function requiresRateLimit(pathname: string): boolean { const route = UNAUTHENTICATED_ROUTES.find(route => route.pattern.endsWith('*') ? pathname.startsWith(route.pattern.slice(0, -1)) : pathname === route.pattern); return route?.requiresRateLimit === true; }
19
- export default { UNAUTHENTICATED_ROUTES, isUnauthenticatedRoute, isAllowedDuringCircuitBreakerOpen, requiresRateLimit };
1
+ export interface UnauthenticatedRouteConfig { pattern: string; description: string; allowDuringCircuitBreakerOpen?: boolean; requiresRateLimit?: boolean; }
2
+ export const UNAUTHENTICATED_ROUTES: UnauthenticatedRouteConfig[] = [
3
+ { pattern: '/account-auth/*', description: 'All authentication pages', allowDuringCircuitBreakerOpen: true },
4
+ { pattern: '/account-auth/login', description: 'Login page', allowDuringCircuitBreakerOpen: true },
5
+ { pattern: '/account-auth/verify-code', description: '2FA page', allowDuringCircuitBreakerOpen: true },
6
+ { pattern: '/api/auth/*', description: 'Auth API endpoints', allowDuringCircuitBreakerOpen: true, requiresRateLimit: true },
7
+ { pattern: '/api/session/refresh-viability', description: 'Refresh viability check', allowDuringCircuitBreakerOpen: true },
8
+ { pattern: '/landing', description: 'Public landing', allowDuringCircuitBreakerOpen: true },
9
+ { pattern: '/service-unavailable', description: 'Service unavailable', allowDuringCircuitBreakerOpen: true },
10
+ { pattern: '/favicon.ico', description: 'Favicon', allowDuringCircuitBreakerOpen: true },
11
+ { pattern: '/robots.txt', description: 'Robots.txt', allowDuringCircuitBreakerOpen: true },
12
+ { pattern: '/sitemap.xml', description: 'Sitemap', allowDuringCircuitBreakerOpen: true },
13
+ { pattern: '/_next/*', description: 'Next.js assets', allowDuringCircuitBreakerOpen: true },
14
+ { pattern: '/public/*', description: 'Public assets', allowDuringCircuitBreakerOpen: true }
15
+ ];
16
+ export function isUnauthenticatedRoute(pathname: string): boolean { return UNAUTHENTICATED_ROUTES.some(route => route.pattern.endsWith('*') ? pathname.startsWith(route.pattern.slice(0, -1)) : pathname === route.pattern); }
17
+ export function isAllowedDuringCircuitBreakerOpen(pathname: string): boolean { const route = UNAUTHENTICATED_ROUTES.find(route => route.pattern.endsWith('*') ? pathname.startsWith(route.pattern.slice(0, -1)) : pathname === route.pattern); return route?.allowDuringCircuitBreakerOpen === true; }
18
+ export function requiresRateLimit(pathname: string): boolean { const route = UNAUTHENTICATED_ROUTES.find(route => route.pattern.endsWith('*') ? pathname.startsWith(route.pattern.slice(0, -1)) : pathname === route.pattern); return route?.requiresRateLimit === true; }
19
+ export default { UNAUTHENTICATED_ROUTES, isUnauthenticatedRoute, isAllowedDuringCircuitBreakerOpen, requiresRateLimit };