@payez/next-mvp 4.1.10 → 4.1.20

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 (181) hide show
  1. package/dist/api-handlers/account/profile.d.ts +38 -0
  2. package/dist/api-handlers/account/profile.js +73 -45
  3. package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
  4. package/dist/api-handlers/account/recovery/send-code.js +52 -18
  5. package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
  6. package/dist/api-handlers/account/recovery/verify-code.js +52 -18
  7. package/dist/api-handlers/account/verify-email.js +15 -1
  8. package/dist/api-handlers/account/verify-sms.js +15 -1
  9. package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
  10. package/dist/api-handlers/auth/passkey-signin.js +97 -0
  11. package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
  12. package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
  13. package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
  14. package/dist/api-handlers/mfa/backup-codes.js +40 -0
  15. package/dist/api-handlers/mfa/methods.d.ts +3 -0
  16. package/dist/api-handlers/mfa/methods.js +41 -0
  17. package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
  18. package/dist/api-handlers/mfa/totp-confirm.js +41 -0
  19. package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
  20. package/dist/api-handlers/mfa/totp-enroll.js +41 -0
  21. package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
  22. package/dist/api-handlers/mfa/totp-verify.js +134 -0
  23. package/dist/api-handlers/passkeys/list.d.ts +7 -0
  24. package/dist/api-handlers/passkeys/list.js +29 -0
  25. package/dist/api-handlers/passkeys/manage.d.ts +3 -0
  26. package/dist/api-handlers/passkeys/manage.js +54 -0
  27. package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
  28. package/dist/api-handlers/passkeys/register-options.js +41 -0
  29. package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
  30. package/dist/api-handlers/passkeys/register-verify.js +40 -0
  31. package/dist/api-handlers/session/viability.js +19 -0
  32. package/dist/auth/better-auth.d.ts +68 -5
  33. package/dist/auth/better-auth.js +271 -45
  34. package/dist/auth/utils/idp-client.d.ts +13 -22
  35. package/dist/auth/utils/idp-client.js +12 -82
  36. package/dist/client/better-auth-client.d.ts +14 -2
  37. package/dist/lib/ensure-fresh-access-token.js +13 -2
  38. package/dist/lib/session-store.js +28 -21
  39. package/dist/lib/startup-init.js +19 -19
  40. package/dist/lib/token-lifecycle.d.ts +2 -0
  41. package/dist/lib/token-lifecycle.js +61 -7
  42. package/dist/models/SessionModel.d.ts +17 -0
  43. package/dist/pages/login/page.d.ts +10 -1
  44. package/dist/pages/login/page.js +69 -80
  45. package/dist/pages/verify-code/page.d.ts +10 -1
  46. package/dist/pages/verify-code/page.js +72 -4
  47. package/dist/routes/auth/passkey-signin.d.ts +13 -0
  48. package/dist/routes/auth/passkey-signin.js +18 -0
  49. package/dist/routes/auth/viability.d.ts +2 -0
  50. package/dist/routes/auth/viability.js +88 -0
  51. package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
  52. package/dist/routes/mfa/backup-code-verify.js +5 -0
  53. package/dist/routes/mfa/backup-codes.d.ts +10 -0
  54. package/dist/routes/mfa/backup-codes.js +14 -0
  55. package/dist/routes/mfa/methods.d.ts +21 -0
  56. package/dist/routes/mfa/methods.js +10 -0
  57. package/dist/routes/mfa/totp-confirm.d.ts +10 -0
  58. package/dist/routes/mfa/totp-confirm.js +14 -0
  59. package/dist/routes/mfa/totp-enroll.d.ts +10 -0
  60. package/dist/routes/mfa/totp-enroll.js +14 -0
  61. package/dist/routes/mfa/totp-verify.d.ts +14 -0
  62. package/dist/routes/mfa/totp-verify.js +18 -0
  63. package/dist/routes/passkeys/list.d.ts +10 -0
  64. package/dist/routes/passkeys/list.js +14 -0
  65. package/dist/routes/passkeys/manage.d.ts +18 -0
  66. package/dist/routes/passkeys/manage.js +13 -0
  67. package/dist/routes/passkeys/register-options.d.ts +10 -0
  68. package/dist/routes/passkeys/register-options.js +14 -0
  69. package/dist/routes/passkeys/register-verify.d.ts +10 -0
  70. package/dist/routes/passkeys/register-verify.js +14 -0
  71. package/dist/server/auth.d.ts +2 -2
  72. package/dist/server/decode-session.js +37 -3
  73. package/package.json +958 -893
  74. package/scripts/check-internal-url-usage.sh +73 -73
  75. package/src/api/index.ts +17 -17
  76. package/src/api-handlers/account/masked-info.ts +45 -45
  77. package/src/api-handlers/account/profile.ts +99 -80
  78. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  79. package/src/api-handlers/account/recovery/send-code.ts +73 -25
  80. package/src/api-handlers/account/recovery/verify-code.ts +73 -25
  81. package/src/api-handlers/account/reset-password.ts +23 -23
  82. package/src/api-handlers/account/send-code.ts +76 -76
  83. package/src/api-handlers/account/validate-password.ts +118 -118
  84. package/src/api-handlers/account/verify-email.ts +15 -1
  85. package/src/api-handlers/account/verify-sms.ts +15 -1
  86. package/src/api-handlers/admin/stats.ts +249 -249
  87. package/src/api-handlers/anon/preferences.ts +123 -123
  88. package/src/api-handlers/auth/jwks.ts +20 -20
  89. package/src/api-handlers/auth/login.ts +240 -240
  90. package/src/api-handlers/auth/passkey-signin.ts +98 -0
  91. package/src/api-handlers/auth/validate.ts +43 -43
  92. package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
  93. package/src/api-handlers/mfa/backup-codes.ts +41 -0
  94. package/src/api-handlers/mfa/methods.ts +46 -0
  95. package/src/api-handlers/mfa/totp-confirm.ts +42 -0
  96. package/src/api-handlers/mfa/totp-enroll.ts +42 -0
  97. package/src/api-handlers/mfa/totp-verify.ts +156 -0
  98. package/src/api-handlers/passkeys/list.ts +31 -0
  99. package/src/api-handlers/passkeys/manage.ts +54 -0
  100. package/src/api-handlers/passkeys/register-options.ts +42 -0
  101. package/src/api-handlers/passkeys/register-verify.ts +41 -0
  102. package/src/api-handlers/session/viability.ts +161 -142
  103. package/src/auth/better-auth.ts +708 -455
  104. package/src/auth/unauthenticated-routes.ts +2 -2
  105. package/src/auth/utils/idp-client.ts +13 -95
  106. package/src/client/better-auth-client.ts +70 -70
  107. package/src/client/fetchWithSession.ts +21 -21
  108. package/src/client/index.ts +13 -13
  109. package/src/client/useAnonSession.ts +131 -131
  110. package/src/components/admin/VibeAdminContext.tsx +87 -87
  111. package/src/config/logging-config.ts +82 -82
  112. package/src/config/unauthenticated-routes.ts +19 -19
  113. package/src/edge/internal-api-url.ts +65 -65
  114. package/src/edge/middleware.ts +42 -42
  115. package/src/hooks/useProfile.ts +75 -75
  116. package/src/lib/anon-session.ts +213 -213
  117. package/src/lib/auth-secret.ts +137 -137
  118. package/src/lib/demo-mode.ts +17 -17
  119. package/src/lib/ensure-fresh-access-token.ts +13 -2
  120. package/src/lib/idp-client-config.ts +559 -559
  121. package/src/lib/idp-fetch.ts +126 -126
  122. package/src/lib/internal-api.ts +171 -171
  123. package/src/lib/jwt-decode-client.ts +45 -45
  124. package/src/lib/rate-limit-service.ts +9 -9
  125. package/src/lib/redis.ts +26 -26
  126. package/src/lib/refresh-token-validator.ts +64 -64
  127. package/src/lib/secret-validation.ts +8 -8
  128. package/src/lib/session-store.ts +702 -695
  129. package/src/lib/session.ts +34 -34
  130. package/src/lib/startup-init.ts +251 -251
  131. package/src/lib/token-expiry.ts +40 -40
  132. package/src/lib/token-lifecycle.ts +533 -470
  133. package/src/lib/types/api-responses.ts +336 -336
  134. package/src/middleware/auth-decision.ts +43 -43
  135. package/src/models/DecodedAccessToken.ts +16 -16
  136. package/src/models/SessionModel.ts +282 -264
  137. package/src/pages/admin-roles/index.ts +9 -9
  138. package/src/pages/login/page.tsx +123 -228
  139. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  140. package/src/pages/verify-code/page.tsx +71 -5
  141. package/src/routes/account/index.ts +31 -31
  142. package/src/routes/account/masked-info.ts +36 -36
  143. package/src/routes/account/send-code.ts +39 -39
  144. package/src/routes/account/verify-email.ts +40 -40
  145. package/src/routes/account/verify-sms.ts +40 -40
  146. package/src/routes/auth/index.ts +22 -22
  147. package/src/routes/auth/passkey-signin.ts +13 -0
  148. package/src/routes/auth/refresh.ts +48 -48
  149. package/src/routes/auth/session.ts +166 -166
  150. package/src/routes/auth/viability.ts +89 -0
  151. package/src/routes/index.ts +17 -17
  152. package/src/routes/mfa/backup-code-verify.ts +1 -0
  153. package/src/routes/mfa/backup-codes.ts +10 -0
  154. package/src/routes/mfa/methods.ts +22 -0
  155. package/src/routes/mfa/totp-confirm.ts +10 -0
  156. package/src/routes/mfa/totp-enroll.ts +10 -0
  157. package/src/routes/mfa/totp-verify.ts +14 -0
  158. package/src/routes/passkeys/list.ts +10 -0
  159. package/src/routes/passkeys/manage.ts +23 -0
  160. package/src/routes/passkeys/register-options.ts +10 -0
  161. package/src/routes/passkeys/register-verify.ts +10 -0
  162. package/src/routes/session/index.ts +7 -7
  163. package/src/routes/session/refresh-viability.ts +17 -17
  164. package/src/server/auth.ts +373 -373
  165. package/src/server/decode-session.ts +235 -202
  166. package/src/services/signalrActivityService.ts +258 -258
  167. package/src/theme/ThemeProvider.tsx +39 -39
  168. package/src/theme/default.ts +33 -33
  169. package/src/theme/index.ts +31 -31
  170. package/src/theme/types.ts +69 -69
  171. package/src/theme/useTheme.ts +57 -57
  172. package/src/theme/utils.ts +40 -40
  173. package/src/types/api.ts +12 -12
  174. package/src/types/auth.d.ts +23 -23
  175. package/src/types/auth.ts +22 -22
  176. package/src/types/logging.ts +11 -11
  177. package/src/types/recovery.ts +54 -54
  178. package/src/utils/api.ts +353 -353
  179. package/src/utils/circuitBreaker.ts +40 -40
  180. package/src/utils/error-message.ts +108 -108
  181. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,258 +1,258 @@
1
- import * as signalR from '@microsoft/signalr';
2
-
3
- export interface HealthStatus {
4
- isHealthy: boolean;
5
- message: string;
6
- lastHeartbeat: Date | null;
7
- connectionId: string | null | undefined;
8
- responseTime?: string;
9
- }
10
-
11
- export type HealthStatusCallback = (status: HealthStatus) => void;
12
-
13
- /**
14
- * SignalR-based health service following Occam's Razor principle:
15
- * - If SignalR connection is alive = Service is working
16
- * - If SignalR connection is dead = Service is not working
17
- * - No complex orchestration, just connection state monitoring
18
- */
19
- class SignalRActivityService {
20
- private connection: signalR.HubConnection | null = null;
21
- private subscribers: Set<HealthStatusCallback> = new Set();
22
- private currentStatus: HealthStatus = {
23
- isHealthy: false,
24
- message: 'Disconnected',
25
- lastHeartbeat: null,
26
- connectionId: null
27
- };
28
- private heartbeatTimeout: NodeJS.Timeout | null = null;
29
- private readonly heartbeatTimeoutMs = 45000; // 45 seconds (server sends every 30s)
30
-
31
- /**
32
- * Start the health monitoring connection
33
- * @param idpBaseUrl - The base URL of the IDP server (e.g., 'http://localhost:32785')
34
- */
35
- async start(idpBaseUrl: string): Promise<void> {
36
- // If we already have a connected or connecting connection, don't start again
37
- if (this.connection &&
38
- (this.connection.state === signalR.HubConnectionState.Connected ||
39
- this.connection.state === signalR.HubConnectionState.Connecting)) {
40
- console.info('[SignalRHealth] Connection already active, skipping start');
41
- return;
42
- }
43
-
44
- // Stop existing connection if it exists
45
- if (this.connection) {
46
- await this.stop();
47
- }
48
-
49
- try {
50
- if (!idpBaseUrl) {
51
- throw new Error('IDP base URL is required for health monitoring');
52
- }
53
-
54
- // Construct absolute hub URL safely
55
- const activityHubUrl = new URL('/healthHub', idpBaseUrl).toString();
56
- console.info('[SignalRHealth] Using hub URL:', activityHubUrl);
57
-
58
- this.connection = new signalR.HubConnectionBuilder()
59
- .withUrl(activityHubUrl, {
60
- withCredentials: false,
61
- transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.ServerSentEvents | signalR.HttpTransportType.LongPolling,
62
- })
63
- .withAutomaticReconnect({
64
- nextRetryDelayInMilliseconds: (retryContext) => {
65
- // More conservative backoff: 5s, 15s, 45s, then stop trying
66
- const delays = [5000, 15000, 45000];
67
- if (retryContext.previousRetryCount >= delays.length) {
68
- return null; // Stop automatic reconnection
69
- }
70
- return delays[retryContext.previousRetryCount];
71
- }
72
- })
73
- .configureLogging(signalR.LogLevel.Critical) // Only critical errors, hide connection noise
74
- .build();
75
-
76
- // Handle connection events
77
- this.connection.onclose(() => {
78
- this.updateStatus({
79
- isHealthy: false,
80
- message: 'Service unavailable',
81
- lastHeartbeat: null,
82
- connectionId: null
83
- });
84
- this.clearHeartbeatTimeout();
85
- });
86
-
87
- this.connection.onreconnecting(() => {
88
- this.updateStatus({
89
- isHealthy: false,
90
- message: 'Service unavailable',
91
- lastHeartbeat: this.currentStatus.lastHeartbeat,
92
- connectionId: null
93
- });
94
- });
95
-
96
- this.connection.onreconnected((connectionId) => {
97
- this.updateStatus({
98
- isHealthy: true,
99
- message: 'Service operational',
100
- lastHeartbeat: new Date(),
101
- connectionId
102
- });
103
- this.resetHeartbeatTimeout();
104
- });
105
-
106
- // Handle heartbeat messages - this is the core health indicator
107
- this.connection.on('Heartbeat', (data) => {
108
- this.updateStatus({
109
- isHealthy: true,
110
- message: 'Service operational',
111
- lastHeartbeat: new Date(),
112
- connectionId: this.connection?.connectionId || null
113
- });
114
- this.resetHeartbeatTimeout();
115
- });
116
-
117
- // Handle initial health status
118
- this.connection.on('HealthStatus', (data) => {
119
- this.updateStatus({
120
- isHealthy: data.status === 'healthy',
121
- message: data.message || 'Service connected',
122
- lastHeartbeat: new Date(),
123
- connectionId: this.connection?.connectionId || null
124
- });
125
- this.resetHeartbeatTimeout();
126
- });
127
-
128
- // Start the connection
129
- await this.connection.start();
130
- console.info('[SignalRHealth] Connection started, connectionId:', this.connection.connectionId);
131
-
132
- this.updateStatus({
133
- isHealthy: true,
134
- message: 'Service connected',
135
- lastHeartbeat: new Date(),
136
- connectionId: this.connection.connectionId
137
- });
138
-
139
- this.resetHeartbeatTimeout();
140
-
141
- } catch (error) {
142
- // Reduce console noise for expected connection failures
143
- const errorMessage = error instanceof Error ? error.message : String(error);
144
- console.warn('[SignalRHealth] Connection start failed:', errorMessage);
145
- const isConnectionRefused = errorMessage.includes('ERR_CONNECTION_REFUSED') ||
146
- errorMessage.includes('Failed to fetch') ||
147
- errorMessage.includes('Failed to complete negotiation');
148
-
149
- if (isConnectionRefused) {
150
- // Service is down - this is expected, log at info level
151
- console.info('SignalR Health Service: Backend service unavailable');
152
- } else {
153
- // Unexpected error - log as error
154
- console.error('SignalR Health Service failed to start:', error);
155
- }
156
-
157
- this.updateStatus({
158
- isHealthy: false,
159
- message: 'Service unavailable',
160
- lastHeartbeat: null,
161
- connectionId: null
162
- });
163
- }
164
- }
165
-
166
- /**
167
- * Stop the health monitoring connection
168
- */
169
- async stop(): Promise<void> {
170
- this.clearHeartbeatTimeout();
171
-
172
- if (this.connection) {
173
- try {
174
- // Check if connection is in a state that can be stopped
175
- if (this.connection.state !== signalR.HubConnectionState.Disconnected) {
176
- await this.connection.stop();
177
- }
178
- } catch (error) {
179
- // Ignore "connection was stopped before the hub handshake could complete" errors
180
- // as these are expected during rapid start/stop cycles
181
- const errorMessage = error instanceof Error ? error.message : String(error);
182
- if (!errorMessage.includes('stopped before the hub handshake could complete')) {
183
- console.error('Error stopping SignalR health connection:', error);
184
- }
185
- }
186
- this.connection = null;
187
- }
188
-
189
- this.updateStatus({
190
- isHealthy: false,
191
- message: 'Disconnected',
192
- lastHeartbeat: null,
193
- connectionId: null
194
- });
195
- }
196
-
197
- /**
198
- * Subscribe to health status changes
199
- */
200
- subscribe(callback: HealthStatusCallback): () => void {
201
- this.subscribers.add(callback);
202
-
203
- // Immediately notify with current status
204
- callback(this.currentStatus);
205
-
206
- // Return unsubscribe function
207
- return () => {
208
- this.subscribers.delete(callback);
209
- };
210
- }
211
-
212
- /**
213
- * Get current health status
214
- */
215
- getCurrentStatus(): HealthStatus {
216
- return { ...this.currentStatus };
217
- }
218
-
219
- private updateStatus(newStatus: HealthStatus): void {
220
- this.currentStatus = newStatus;
221
- this.notifySubscribers();
222
- }
223
-
224
- private notifySubscribers(): void {
225
- this.subscribers.forEach(callback => {
226
- try {
227
- callback(this.currentStatus);
228
- } catch (error) {
229
- console.error('Error in health status subscriber:', error);
230
- }
231
- });
232
- }
233
-
234
- private resetHeartbeatTimeout(): void {
235
- this.clearHeartbeatTimeout();
236
-
237
- // If we don't receive a heartbeat within the timeout period, consider service unhealthy
238
- this.heartbeatTimeout = setTimeout(() => {
239
- this.updateStatus({
240
- isHealthy: false,
241
- message: 'Service unavailable',
242
- lastHeartbeat: this.currentStatus.lastHeartbeat,
243
- connectionId: this.currentStatus.connectionId
244
- });
245
- }, this.heartbeatTimeoutMs);
246
- }
247
-
248
- private clearHeartbeatTimeout(): void {
249
- if (this.heartbeatTimeout) {
250
- clearTimeout(this.heartbeatTimeout);
251
- this.heartbeatTimeout = null;
252
- }
253
- }
254
-
255
- }
256
-
257
- // Export singleton instance
258
- export const signalRActivityService = new SignalRActivityService();
1
+ import * as signalR from '@microsoft/signalr';
2
+
3
+ export interface HealthStatus {
4
+ isHealthy: boolean;
5
+ message: string;
6
+ lastHeartbeat: Date | null;
7
+ connectionId: string | null | undefined;
8
+ responseTime?: string;
9
+ }
10
+
11
+ export type HealthStatusCallback = (status: HealthStatus) => void;
12
+
13
+ /**
14
+ * SignalR-based health service following Occam's Razor principle:
15
+ * - If SignalR connection is alive = Service is working
16
+ * - If SignalR connection is dead = Service is not working
17
+ * - No complex orchestration, just connection state monitoring
18
+ */
19
+ class SignalRActivityService {
20
+ private connection: signalR.HubConnection | null = null;
21
+ private subscribers: Set<HealthStatusCallback> = new Set();
22
+ private currentStatus: HealthStatus = {
23
+ isHealthy: false,
24
+ message: 'Disconnected',
25
+ lastHeartbeat: null,
26
+ connectionId: null
27
+ };
28
+ private heartbeatTimeout: NodeJS.Timeout | null = null;
29
+ private readonly heartbeatTimeoutMs = 45000; // 45 seconds (server sends every 30s)
30
+
31
+ /**
32
+ * Start the health monitoring connection
33
+ * @param idpBaseUrl - The base URL of the IDP server (e.g., 'http://localhost:32785')
34
+ */
35
+ async start(idpBaseUrl: string): Promise<void> {
36
+ // If we already have a connected or connecting connection, don't start again
37
+ if (this.connection &&
38
+ (this.connection.state === signalR.HubConnectionState.Connected ||
39
+ this.connection.state === signalR.HubConnectionState.Connecting)) {
40
+ console.info('[SignalRHealth] Connection already active, skipping start');
41
+ return;
42
+ }
43
+
44
+ // Stop existing connection if it exists
45
+ if (this.connection) {
46
+ await this.stop();
47
+ }
48
+
49
+ try {
50
+ if (!idpBaseUrl) {
51
+ throw new Error('IDP base URL is required for health monitoring');
52
+ }
53
+
54
+ // Construct absolute hub URL safely
55
+ const activityHubUrl = new URL('/healthHub', idpBaseUrl).toString();
56
+ console.info('[SignalRHealth] Using hub URL:', activityHubUrl);
57
+
58
+ this.connection = new signalR.HubConnectionBuilder()
59
+ .withUrl(activityHubUrl, {
60
+ withCredentials: false,
61
+ transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.ServerSentEvents | signalR.HttpTransportType.LongPolling,
62
+ })
63
+ .withAutomaticReconnect({
64
+ nextRetryDelayInMilliseconds: (retryContext) => {
65
+ // More conservative backoff: 5s, 15s, 45s, then stop trying
66
+ const delays = [5000, 15000, 45000];
67
+ if (retryContext.previousRetryCount >= delays.length) {
68
+ return null; // Stop automatic reconnection
69
+ }
70
+ return delays[retryContext.previousRetryCount];
71
+ }
72
+ })
73
+ .configureLogging(signalR.LogLevel.Critical) // Only critical errors, hide connection noise
74
+ .build();
75
+
76
+ // Handle connection events
77
+ this.connection.onclose(() => {
78
+ this.updateStatus({
79
+ isHealthy: false,
80
+ message: 'Service unavailable',
81
+ lastHeartbeat: null,
82
+ connectionId: null
83
+ });
84
+ this.clearHeartbeatTimeout();
85
+ });
86
+
87
+ this.connection.onreconnecting(() => {
88
+ this.updateStatus({
89
+ isHealthy: false,
90
+ message: 'Service unavailable',
91
+ lastHeartbeat: this.currentStatus.lastHeartbeat,
92
+ connectionId: null
93
+ });
94
+ });
95
+
96
+ this.connection.onreconnected((connectionId) => {
97
+ this.updateStatus({
98
+ isHealthy: true,
99
+ message: 'Service operational',
100
+ lastHeartbeat: new Date(),
101
+ connectionId
102
+ });
103
+ this.resetHeartbeatTimeout();
104
+ });
105
+
106
+ // Handle heartbeat messages - this is the core health indicator
107
+ this.connection.on('Heartbeat', (data) => {
108
+ this.updateStatus({
109
+ isHealthy: true,
110
+ message: 'Service operational',
111
+ lastHeartbeat: new Date(),
112
+ connectionId: this.connection?.connectionId || null
113
+ });
114
+ this.resetHeartbeatTimeout();
115
+ });
116
+
117
+ // Handle initial health status
118
+ this.connection.on('HealthStatus', (data) => {
119
+ this.updateStatus({
120
+ isHealthy: data.status === 'healthy',
121
+ message: data.message || 'Service connected',
122
+ lastHeartbeat: new Date(),
123
+ connectionId: this.connection?.connectionId || null
124
+ });
125
+ this.resetHeartbeatTimeout();
126
+ });
127
+
128
+ // Start the connection
129
+ await this.connection.start();
130
+ console.info('[SignalRHealth] Connection started, connectionId:', this.connection.connectionId);
131
+
132
+ this.updateStatus({
133
+ isHealthy: true,
134
+ message: 'Service connected',
135
+ lastHeartbeat: new Date(),
136
+ connectionId: this.connection.connectionId
137
+ });
138
+
139
+ this.resetHeartbeatTimeout();
140
+
141
+ } catch (error) {
142
+ // Reduce console noise for expected connection failures
143
+ const errorMessage = error instanceof Error ? error.message : String(error);
144
+ console.warn('[SignalRHealth] Connection start failed:', errorMessage);
145
+ const isConnectionRefused = errorMessage.includes('ERR_CONNECTION_REFUSED') ||
146
+ errorMessage.includes('Failed to fetch') ||
147
+ errorMessage.includes('Failed to complete negotiation');
148
+
149
+ if (isConnectionRefused) {
150
+ // Service is down - this is expected, log at info level
151
+ console.info('SignalR Health Service: Backend service unavailable');
152
+ } else {
153
+ // Unexpected error - log as error
154
+ console.error('SignalR Health Service failed to start:', error);
155
+ }
156
+
157
+ this.updateStatus({
158
+ isHealthy: false,
159
+ message: 'Service unavailable',
160
+ lastHeartbeat: null,
161
+ connectionId: null
162
+ });
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Stop the health monitoring connection
168
+ */
169
+ async stop(): Promise<void> {
170
+ this.clearHeartbeatTimeout();
171
+
172
+ if (this.connection) {
173
+ try {
174
+ // Check if connection is in a state that can be stopped
175
+ if (this.connection.state !== signalR.HubConnectionState.Disconnected) {
176
+ await this.connection.stop();
177
+ }
178
+ } catch (error) {
179
+ // Ignore "connection was stopped before the hub handshake could complete" errors
180
+ // as these are expected during rapid start/stop cycles
181
+ const errorMessage = error instanceof Error ? error.message : String(error);
182
+ if (!errorMessage.includes('stopped before the hub handshake could complete')) {
183
+ console.error('Error stopping SignalR health connection:', error);
184
+ }
185
+ }
186
+ this.connection = null;
187
+ }
188
+
189
+ this.updateStatus({
190
+ isHealthy: false,
191
+ message: 'Disconnected',
192
+ lastHeartbeat: null,
193
+ connectionId: null
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Subscribe to health status changes
199
+ */
200
+ subscribe(callback: HealthStatusCallback): () => void {
201
+ this.subscribers.add(callback);
202
+
203
+ // Immediately notify with current status
204
+ callback(this.currentStatus);
205
+
206
+ // Return unsubscribe function
207
+ return () => {
208
+ this.subscribers.delete(callback);
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Get current health status
214
+ */
215
+ getCurrentStatus(): HealthStatus {
216
+ return { ...this.currentStatus };
217
+ }
218
+
219
+ private updateStatus(newStatus: HealthStatus): void {
220
+ this.currentStatus = newStatus;
221
+ this.notifySubscribers();
222
+ }
223
+
224
+ private notifySubscribers(): void {
225
+ this.subscribers.forEach(callback => {
226
+ try {
227
+ callback(this.currentStatus);
228
+ } catch (error) {
229
+ console.error('Error in health status subscriber:', error);
230
+ }
231
+ });
232
+ }
233
+
234
+ private resetHeartbeatTimeout(): void {
235
+ this.clearHeartbeatTimeout();
236
+
237
+ // If we don't receive a heartbeat within the timeout period, consider service unhealthy
238
+ this.heartbeatTimeout = setTimeout(() => {
239
+ this.updateStatus({
240
+ isHealthy: false,
241
+ message: 'Service unavailable',
242
+ lastHeartbeat: this.currentStatus.lastHeartbeat,
243
+ connectionId: this.currentStatus.connectionId
244
+ });
245
+ }, this.heartbeatTimeoutMs);
246
+ }
247
+
248
+ private clearHeartbeatTimeout(): void {
249
+ if (this.heartbeatTimeout) {
250
+ clearTimeout(this.heartbeatTimeout);
251
+ this.heartbeatTimeout = null;
252
+ }
253
+ }
254
+
255
+ }
256
+
257
+ // Export singleton instance
258
+ export const signalRActivityService = new SignalRActivityService();
@@ -1,39 +1,39 @@
1
- /**
2
- * Theme Provider for @payez/next-mvp
3
- *
4
- * Provides theme configuration to all child components via React Context.
5
- * Consumer apps wrap their app with this provider and pass custom theme config.
6
- */
7
-
8
- 'use client';
9
-
10
- import React, { createContext, useContext } from 'react';
11
- import { ThemeConfig } from './types';
12
- import { defaultTheme } from './default';
13
- import { mergeDeep } from './utils';
14
-
15
- const ThemeContext = createContext<ThemeConfig>(defaultTheme);
16
-
17
- export interface ThemeProviderProps {
18
- children: React.ReactNode;
19
- theme?: Partial<ThemeConfig>;
20
- }
21
-
22
- export function ThemeProvider({ children, theme }: ThemeProviderProps) {
23
- // Deep merge consumer theme with defaults
24
- const mergedTheme = mergeDeep(defaultTheme, theme || {});
25
-
26
- return (
27
- <ThemeContext.Provider value={mergedTheme}>
28
- {children}
29
- </ThemeContext.Provider>
30
- );
31
- }
32
-
33
- export function useTheme() {
34
- const context = useContext(ThemeContext);
35
- if (!context) {
36
- throw new Error('useTheme must be used within ThemeProvider');
37
- }
38
- return context;
39
- }
1
+ /**
2
+ * Theme Provider for @payez/next-mvp
3
+ *
4
+ * Provides theme configuration to all child components via React Context.
5
+ * Consumer apps wrap their app with this provider and pass custom theme config.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React, { createContext, useContext } from 'react';
11
+ import { ThemeConfig } from './types';
12
+ import { defaultTheme } from './default';
13
+ import { mergeDeep } from './utils';
14
+
15
+ const ThemeContext = createContext<ThemeConfig>(defaultTheme);
16
+
17
+ export interface ThemeProviderProps {
18
+ children: React.ReactNode;
19
+ theme?: Partial<ThemeConfig>;
20
+ }
21
+
22
+ export function ThemeProvider({ children, theme }: ThemeProviderProps) {
23
+ // Deep merge consumer theme with defaults
24
+ const mergedTheme = mergeDeep(defaultTheme, theme || {});
25
+
26
+ return (
27
+ <ThemeContext.Provider value={mergedTheme}>
28
+ {children}
29
+ </ThemeContext.Provider>
30
+ );
31
+ }
32
+
33
+ export function useTheme() {
34
+ const context = useContext(ThemeContext);
35
+ if (!context) {
36
+ throw new Error('useTheme must be used within ThemeProvider');
37
+ }
38
+ return context;
39
+ }
@@ -1,33 +1,33 @@
1
- /**
2
- * Default Theme Configuration for @payez/next-mvp
3
- *
4
- * This is the fallback theme when no custom theme is provided.
5
- * It provides sensible defaults that match the current unbranded styling.
6
- */
7
-
8
- import { ThemeConfig } from './types';
9
-
10
- export const defaultTheme: ThemeConfig = {
11
- branding: {
12
- logo: {
13
- light: "/logo.svg",
14
- dark: "/logo.svg",
15
- alt: "Logo",
16
- height: "h-10",
17
- width: "w-auto",
18
- },
19
- appName: "App",
20
- },
21
- colors: {
22
- primary: "#3b82f6", // blue-500
23
- background: "bg-gray-50",
24
- card: "bg-white",
25
- muted: "text-gray-600",
26
- border: "border-gray-200",
27
- },
28
- layout: {
29
- maxWidth: "max-w-4xl",
30
- padding: "p-6",
31
- spacing: "space-y-6",
32
- },
33
- };
1
+ /**
2
+ * Default Theme Configuration for @payez/next-mvp
3
+ *
4
+ * This is the fallback theme when no custom theme is provided.
5
+ * It provides sensible defaults that match the current unbranded styling.
6
+ */
7
+
8
+ import { ThemeConfig } from './types';
9
+
10
+ export const defaultTheme: ThemeConfig = {
11
+ branding: {
12
+ logo: {
13
+ light: "/logo.svg",
14
+ dark: "/logo.svg",
15
+ alt: "Logo",
16
+ height: "h-10",
17
+ width: "w-auto",
18
+ },
19
+ appName: "App",
20
+ },
21
+ colors: {
22
+ primary: "#3b82f6", // blue-500
23
+ background: "bg-gray-50",
24
+ card: "bg-white",
25
+ muted: "text-gray-600",
26
+ border: "border-gray-200",
27
+ },
28
+ layout: {
29
+ maxWidth: "max-w-4xl",
30
+ padding: "p-6",
31
+ spacing: "space-y-6",
32
+ },
33
+ };