@payez/next-mvp 4.1.7 → 4.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +792 -792
- package/dist/auth/better-auth.js +15 -3
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/package.json +1 -1
- package/scripts/dev-broker.ps1 +35 -35
- package/scripts/dev-local.ps1 +45 -45
- package/src/api/auth-handler.ts +548 -548
- package/src/api-handlers/account/change-password.ts +142 -142
- package/src/api-handlers/account/update-phone.ts +79 -79
- package/src/api-handlers/account/verify-email.ts +125 -125
- package/src/api-handlers/account/verify-sms.ts +125 -125
- package/src/api-handlers/admin/analytics.ts +443 -443
- package/src/api-handlers/admin/audit.ts +223 -223
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +251 -251
- package/src/api-handlers/admin/sessions.ts +318 -318
- package/src/api-handlers/admin/site-logs.ts +365 -365
- package/src/api-handlers/admin/users.ts +242 -242
- package/src/api-handlers/admin/vibe-data.ts +324 -324
- package/src/api-handlers/auth/refresh.ts +682 -682
- package/src/api-handlers/auth/signout.ts +199 -199
- package/src/api-handlers/auth/status.ts +20 -20
- package/src/api-handlers/auth/update-session.ts +110 -110
- package/src/api-handlers/auth/verify-code.ts +112 -112
- package/src/api-handlers/session/refresh-viability.ts +36 -36
- package/src/api-handlers/test/force-expire.ts +60 -60
- package/src/auth/auth-decision.ts +229 -229
- package/src/auth/better-auth.ts +15 -3
- package/src/auth/route-config.ts +219 -219
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/utils/idp-client.ts +445 -445
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +143 -143
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +266 -266
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +505 -505
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +414 -414
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/FederatedAuthSection.tsx +95 -95
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
- package/src/components/auth/TraditionalAuthSection.tsx +99 -99
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +97 -97
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +57 -57
- package/src/config/logger.ts +62 -62
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +97 -97
- package/src/hooks/useAvailableProviders.ts +120 -120
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/usePublicAuthSettings.ts +149 -149
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +336 -336
- package/src/index.ts +60 -60
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +178 -178
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-fetch.ts +110 -110
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/roles.ts +177 -177
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +663 -663
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/pages/admin-login/page.tsx +221 -221
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +456 -456
- package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +234 -234
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +565 -565
- package/src/pages/security/index.ts +9 -9
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +180 -180
- package/src/pages/test-env/JwtInspectPage.tsx +456 -456
- package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
- package/src/pages/test-env/TestEnvPage.tsx +118 -118
- package/src/pages/test-env/index.ts +25 -25
- package/src/pages/verify-code/page.tsx +652 -652
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/auth/logout.ts +108 -108
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/settings.ts +75 -75
- package/src/routes/auth/viability.ts +224 -224
- package/src/server/auth-guard.ts +170 -170
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/with-auth.ts +93 -93
- package/src/stores/authStore.ts +1899 -1899
- package/src/templates/instrumentation.ts +41 -41
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +590 -590
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +185 -185
- package/src/vibe/generic.ts +431 -431
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +132 -132
- package/src/vibe/sessions.ts +551 -551
- package/src/vibe/types.ts +577 -577
|
@@ -1,264 +1,264 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vibe Log Transport for Winston
|
|
3
|
-
*
|
|
4
|
-
* Buffers log entries to Redis for async processing by Vibe log drain.
|
|
5
|
-
* This avoids the "log database errors to database" chicken-and-egg problem.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Async batch writing (doesn't block main thread)
|
|
9
|
-
* - Configurable minimum log level
|
|
10
|
-
* - Redis buffering with 1-week TTL
|
|
11
|
-
* - Graceful degradation on failure
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { getRedis } from '../lib/redis';
|
|
15
|
-
|
|
16
|
-
// Dynamic import — winston is a peerDependency and may not be installed.
|
|
17
|
-
// We resolve the base class lazily so builds don't break without it.
|
|
18
|
-
let TransportBase: any;
|
|
19
|
-
try {
|
|
20
|
-
TransportBase = require('winston-transport');
|
|
21
|
-
} catch {
|
|
22
|
-
// Fallback: minimal base class for when winston is not installed
|
|
23
|
-
TransportBase = class {
|
|
24
|
-
constructor(_opts?: any) {}
|
|
25
|
-
emit(_event: string, _info: any) {}
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Redis key for pending log entries */
|
|
30
|
-
const REDIS_LOG_KEY = 'vibe:logs:pending';
|
|
31
|
-
/** TTL in seconds: 1 week */
|
|
32
|
-
const REDIS_LOG_TTL = 7 * 24 * 60 * 60;
|
|
33
|
-
|
|
34
|
-
export interface VibeLogTransportOptions {
|
|
35
|
-
/** Winston transport level */
|
|
36
|
-
level?: string;
|
|
37
|
-
/** Redis URL (optional, uses REDIS_URL env var by default) */
|
|
38
|
-
redisUrl?: string;
|
|
39
|
-
/** Vibe client ID (for log metadata) */
|
|
40
|
-
vibeClientId?: string;
|
|
41
|
-
/** App slug for identification */
|
|
42
|
-
appSlug?: string;
|
|
43
|
-
/** Minimum level to send to Vibe (default: 'warn') */
|
|
44
|
-
minLevel?: string;
|
|
45
|
-
/** Batch size before flush (default: 10) */
|
|
46
|
-
batchSize?: number;
|
|
47
|
-
/** Flush interval in ms (default: 5000) */
|
|
48
|
-
flushInterval?: number;
|
|
49
|
-
/** Enable/disable the transport (default: true if Redis available) */
|
|
50
|
-
enabled?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface LogEntry {
|
|
54
|
-
level: string;
|
|
55
|
-
message: string;
|
|
56
|
-
timestamp: string;
|
|
57
|
-
category?: string;
|
|
58
|
-
meta?: Record<string, any>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const LEVEL_ORDER: Record<string, number> = {
|
|
62
|
-
error: 0,
|
|
63
|
-
warn: 1,
|
|
64
|
-
info: 2,
|
|
65
|
-
http: 3,
|
|
66
|
-
debug: 4,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Winston transport that buffers logs to Redis for Vibe drain processing
|
|
71
|
-
*/
|
|
72
|
-
export class VibeLogTransport extends TransportBase {
|
|
73
|
-
private vibeClientId: string;
|
|
74
|
-
private appSlug: string;
|
|
75
|
-
private minLevelNum: number;
|
|
76
|
-
private batchSize: number;
|
|
77
|
-
private flushInterval: number;
|
|
78
|
-
private enabled: boolean;
|
|
79
|
-
private redisAvailable: boolean = false;
|
|
80
|
-
|
|
81
|
-
private batch: LogEntry[] = [];
|
|
82
|
-
private flushTimer: NodeJS.Timeout | null = null;
|
|
83
|
-
private isFlushing = false;
|
|
84
|
-
|
|
85
|
-
constructor(opts: VibeLogTransportOptions = {}) {
|
|
86
|
-
super(opts);
|
|
87
|
-
|
|
88
|
-
this.vibeClientId = opts.vibeClientId || process.env.VIBE_CLIENT_ID || '';
|
|
89
|
-
this.appSlug = opts.appSlug || process.env.APP_SLUG || process.env.CLIENT_ID || 'mvp-app';
|
|
90
|
-
this.minLevelNum = LEVEL_ORDER[opts.minLevel || 'warn'] ?? 1;
|
|
91
|
-
this.batchSize = opts.batchSize || 10;
|
|
92
|
-
this.flushInterval = opts.flushInterval || 5000;
|
|
93
|
-
|
|
94
|
-
// Enable if Redis URL is configured
|
|
95
|
-
const redisUrl = opts.redisUrl || process.env.REDIS_URL || '';
|
|
96
|
-
this.enabled = opts.enabled !== false && !!redisUrl;
|
|
97
|
-
this.redisAvailable = !!redisUrl;
|
|
98
|
-
|
|
99
|
-
if (this.enabled) {
|
|
100
|
-
this.startFlushTimer();
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Winston transport log method
|
|
106
|
-
*/
|
|
107
|
-
log(info: any, callback: () => void): void {
|
|
108
|
-
setImmediate(() => this.emit('logged', info));
|
|
109
|
-
|
|
110
|
-
if (!this.enabled) {
|
|
111
|
-
callback();
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const level = info.level?.replace(/\u001b\[\d+m/g, '') || 'info'; // Strip ANSI codes
|
|
116
|
-
const levelNum = LEVEL_ORDER[level] ?? 2;
|
|
117
|
-
|
|
118
|
-
// Only batch if at or above minimum level
|
|
119
|
-
if (levelNum <= this.minLevelNum) {
|
|
120
|
-
const entry: LogEntry = {
|
|
121
|
-
level,
|
|
122
|
-
message: info.message || '',
|
|
123
|
-
timestamp: info.timestamp || new Date().toISOString(),
|
|
124
|
-
category: this.extractCategory(info.message),
|
|
125
|
-
meta: this.extractMeta(info),
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
this.batch.push(entry);
|
|
129
|
-
|
|
130
|
-
// Flush immediately if batch is full or if error/fatal
|
|
131
|
-
if (this.batch.length >= this.batchSize || level === 'error') {
|
|
132
|
-
this.flush().catch(() => {}); // Ignore flush errors
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
callback();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Extract category from message prefix like [AUTH], [IDP], etc.
|
|
141
|
-
*/
|
|
142
|
-
private extractCategory(message: string): string {
|
|
143
|
-
const match = message.match(/^\[([A-Z-]+)\]/);
|
|
144
|
-
return match ? match[1].toLowerCase() : 'app';
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Extract metadata from winston info object
|
|
149
|
-
*/
|
|
150
|
-
private extractMeta(info: any): Record<string, any> {
|
|
151
|
-
const { level, message, timestamp, ...rest } = info;
|
|
152
|
-
return Object.keys(rest).length > 0 ? rest : {};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Start the periodic flush timer
|
|
157
|
-
*/
|
|
158
|
-
private startFlushTimer(): void {
|
|
159
|
-
if (this.flushTimer) return;
|
|
160
|
-
|
|
161
|
-
this.flushTimer = setInterval(() => {
|
|
162
|
-
if (this.batch.length > 0) {
|
|
163
|
-
this.flush().catch(() => {});
|
|
164
|
-
}
|
|
165
|
-
}, this.flushInterval);
|
|
166
|
-
|
|
167
|
-
// Don't prevent process exit
|
|
168
|
-
if (this.flushTimer.unref) {
|
|
169
|
-
this.flushTimer.unref();
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Flush batch to Vibe
|
|
175
|
-
*/
|
|
176
|
-
async flush(): Promise<void> {
|
|
177
|
-
if (this.isFlushing || this.batch.length === 0) return;
|
|
178
|
-
|
|
179
|
-
this.isFlushing = true;
|
|
180
|
-
const entries = [...this.batch];
|
|
181
|
-
this.batch = [];
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
await this.sendToVibe(entries);
|
|
185
|
-
} catch (err) {
|
|
186
|
-
// On failure, put entries back (up to limit to prevent memory issues)
|
|
187
|
-
if (this.batch.length < 100) {
|
|
188
|
-
this.batch = [...entries, ...this.batch].slice(0, 100);
|
|
189
|
-
}
|
|
190
|
-
// Log to console as fallback
|
|
191
|
-
console.error('[VibeLogTransport] Failed to flush logs:', err);
|
|
192
|
-
} finally {
|
|
193
|
-
this.isFlushing = false;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Push log entries to Redis for async drain processing
|
|
199
|
-
*/
|
|
200
|
-
private async sendToVibe(entries: LogEntry[]): Promise<void> {
|
|
201
|
-
if (entries.length === 0) return;
|
|
202
|
-
|
|
203
|
-
const redis = getRedis();
|
|
204
|
-
|
|
205
|
-
// Format entries for the drain to process
|
|
206
|
-
const logRecords = entries.map(e => JSON.stringify({
|
|
207
|
-
level: e.level,
|
|
208
|
-
message: e.message,
|
|
209
|
-
timestamp: e.timestamp,
|
|
210
|
-
source: 'next-mvp',
|
|
211
|
-
app_slug: this.appSlug,
|
|
212
|
-
vibe_client_id: this.vibeClientId,
|
|
213
|
-
category: e.category,
|
|
214
|
-
error_code: e.meta?.errorCode || e.meta?.error_code,
|
|
215
|
-
stack: e.meta?.stack,
|
|
216
|
-
request_id: e.meta?.requestId || e.meta?.request_id,
|
|
217
|
-
user_id: e.meta?.userId || e.meta?.user_id,
|
|
218
|
-
path: e.meta?.path,
|
|
219
|
-
method: e.meta?.method,
|
|
220
|
-
status_code: e.meta?.statusCode || e.meta?.status_code,
|
|
221
|
-
duration_ms: e.meta?.durationMs || e.meta?.duration_ms,
|
|
222
|
-
meta: e.meta,
|
|
223
|
-
queued_at: new Date().toISOString(),
|
|
224
|
-
}));
|
|
225
|
-
|
|
226
|
-
// LPUSH all entries (newest first, drain will RPOP for FIFO)
|
|
227
|
-
await redis.lpush(REDIS_LOG_KEY, ...logRecords);
|
|
228
|
-
|
|
229
|
-
// Reset TTL on each write (1 week from last activity)
|
|
230
|
-
await redis.expire(REDIS_LOG_KEY, REDIS_LOG_TTL);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Close transport and flush remaining logs
|
|
235
|
-
*/
|
|
236
|
-
close(): void {
|
|
237
|
-
if (this.flushTimer) {
|
|
238
|
-
clearInterval(this.flushTimer);
|
|
239
|
-
this.flushTimer = null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Final flush (best effort)
|
|
243
|
-
if (this.batch.length > 0) {
|
|
244
|
-
this.flush().catch(() => {});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Create a configured VibeLogTransport instance
|
|
251
|
-
* Returns null if Redis is not configured (REDIS_URL env var)
|
|
252
|
-
*/
|
|
253
|
-
export function createVibeLogTransport(opts?: VibeLogTransportOptions): VibeLogTransport | null {
|
|
254
|
-
const transport = new VibeLogTransport(opts);
|
|
255
|
-
|
|
256
|
-
// Return null if transport is disabled (no Redis)
|
|
257
|
-
if (!transport['enabled']) {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return transport;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export default VibeLogTransport;
|
|
1
|
+
/**
|
|
2
|
+
* Vibe Log Transport for Winston
|
|
3
|
+
*
|
|
4
|
+
* Buffers log entries to Redis for async processing by Vibe log drain.
|
|
5
|
+
* This avoids the "log database errors to database" chicken-and-egg problem.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Async batch writing (doesn't block main thread)
|
|
9
|
+
* - Configurable minimum log level
|
|
10
|
+
* - Redis buffering with 1-week TTL
|
|
11
|
+
* - Graceful degradation on failure
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getRedis } from '../lib/redis';
|
|
15
|
+
|
|
16
|
+
// Dynamic import — winston is a peerDependency and may not be installed.
|
|
17
|
+
// We resolve the base class lazily so builds don't break without it.
|
|
18
|
+
let TransportBase: any;
|
|
19
|
+
try {
|
|
20
|
+
TransportBase = require('winston-transport');
|
|
21
|
+
} catch {
|
|
22
|
+
// Fallback: minimal base class for when winston is not installed
|
|
23
|
+
TransportBase = class {
|
|
24
|
+
constructor(_opts?: any) {}
|
|
25
|
+
emit(_event: string, _info: any) {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Redis key for pending log entries */
|
|
30
|
+
const REDIS_LOG_KEY = 'vibe:logs:pending';
|
|
31
|
+
/** TTL in seconds: 1 week */
|
|
32
|
+
const REDIS_LOG_TTL = 7 * 24 * 60 * 60;
|
|
33
|
+
|
|
34
|
+
export interface VibeLogTransportOptions {
|
|
35
|
+
/** Winston transport level */
|
|
36
|
+
level?: string;
|
|
37
|
+
/** Redis URL (optional, uses REDIS_URL env var by default) */
|
|
38
|
+
redisUrl?: string;
|
|
39
|
+
/** Vibe client ID (for log metadata) */
|
|
40
|
+
vibeClientId?: string;
|
|
41
|
+
/** App slug for identification */
|
|
42
|
+
appSlug?: string;
|
|
43
|
+
/** Minimum level to send to Vibe (default: 'warn') */
|
|
44
|
+
minLevel?: string;
|
|
45
|
+
/** Batch size before flush (default: 10) */
|
|
46
|
+
batchSize?: number;
|
|
47
|
+
/** Flush interval in ms (default: 5000) */
|
|
48
|
+
flushInterval?: number;
|
|
49
|
+
/** Enable/disable the transport (default: true if Redis available) */
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface LogEntry {
|
|
54
|
+
level: string;
|
|
55
|
+
message: string;
|
|
56
|
+
timestamp: string;
|
|
57
|
+
category?: string;
|
|
58
|
+
meta?: Record<string, any>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const LEVEL_ORDER: Record<string, number> = {
|
|
62
|
+
error: 0,
|
|
63
|
+
warn: 1,
|
|
64
|
+
info: 2,
|
|
65
|
+
http: 3,
|
|
66
|
+
debug: 4,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Winston transport that buffers logs to Redis for Vibe drain processing
|
|
71
|
+
*/
|
|
72
|
+
export class VibeLogTransport extends TransportBase {
|
|
73
|
+
private vibeClientId: string;
|
|
74
|
+
private appSlug: string;
|
|
75
|
+
private minLevelNum: number;
|
|
76
|
+
private batchSize: number;
|
|
77
|
+
private flushInterval: number;
|
|
78
|
+
private enabled: boolean;
|
|
79
|
+
private redisAvailable: boolean = false;
|
|
80
|
+
|
|
81
|
+
private batch: LogEntry[] = [];
|
|
82
|
+
private flushTimer: NodeJS.Timeout | null = null;
|
|
83
|
+
private isFlushing = false;
|
|
84
|
+
|
|
85
|
+
constructor(opts: VibeLogTransportOptions = {}) {
|
|
86
|
+
super(opts);
|
|
87
|
+
|
|
88
|
+
this.vibeClientId = opts.vibeClientId || process.env.VIBE_CLIENT_ID || '';
|
|
89
|
+
this.appSlug = opts.appSlug || process.env.APP_SLUG || process.env.CLIENT_ID || 'mvp-app';
|
|
90
|
+
this.minLevelNum = LEVEL_ORDER[opts.minLevel || 'warn'] ?? 1;
|
|
91
|
+
this.batchSize = opts.batchSize || 10;
|
|
92
|
+
this.flushInterval = opts.flushInterval || 5000;
|
|
93
|
+
|
|
94
|
+
// Enable if Redis URL is configured
|
|
95
|
+
const redisUrl = opts.redisUrl || process.env.REDIS_URL || '';
|
|
96
|
+
this.enabled = opts.enabled !== false && !!redisUrl;
|
|
97
|
+
this.redisAvailable = !!redisUrl;
|
|
98
|
+
|
|
99
|
+
if (this.enabled) {
|
|
100
|
+
this.startFlushTimer();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Winston transport log method
|
|
106
|
+
*/
|
|
107
|
+
log(info: any, callback: () => void): void {
|
|
108
|
+
setImmediate(() => this.emit('logged', info));
|
|
109
|
+
|
|
110
|
+
if (!this.enabled) {
|
|
111
|
+
callback();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const level = info.level?.replace(/\u001b\[\d+m/g, '') || 'info'; // Strip ANSI codes
|
|
116
|
+
const levelNum = LEVEL_ORDER[level] ?? 2;
|
|
117
|
+
|
|
118
|
+
// Only batch if at or above minimum level
|
|
119
|
+
if (levelNum <= this.minLevelNum) {
|
|
120
|
+
const entry: LogEntry = {
|
|
121
|
+
level,
|
|
122
|
+
message: info.message || '',
|
|
123
|
+
timestamp: info.timestamp || new Date().toISOString(),
|
|
124
|
+
category: this.extractCategory(info.message),
|
|
125
|
+
meta: this.extractMeta(info),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
this.batch.push(entry);
|
|
129
|
+
|
|
130
|
+
// Flush immediately if batch is full or if error/fatal
|
|
131
|
+
if (this.batch.length >= this.batchSize || level === 'error') {
|
|
132
|
+
this.flush().catch(() => {}); // Ignore flush errors
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
callback();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract category from message prefix like [AUTH], [IDP], etc.
|
|
141
|
+
*/
|
|
142
|
+
private extractCategory(message: string): string {
|
|
143
|
+
const match = message.match(/^\[([A-Z-]+)\]/);
|
|
144
|
+
return match ? match[1].toLowerCase() : 'app';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Extract metadata from winston info object
|
|
149
|
+
*/
|
|
150
|
+
private extractMeta(info: any): Record<string, any> {
|
|
151
|
+
const { level, message, timestamp, ...rest } = info;
|
|
152
|
+
return Object.keys(rest).length > 0 ? rest : {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Start the periodic flush timer
|
|
157
|
+
*/
|
|
158
|
+
private startFlushTimer(): void {
|
|
159
|
+
if (this.flushTimer) return;
|
|
160
|
+
|
|
161
|
+
this.flushTimer = setInterval(() => {
|
|
162
|
+
if (this.batch.length > 0) {
|
|
163
|
+
this.flush().catch(() => {});
|
|
164
|
+
}
|
|
165
|
+
}, this.flushInterval);
|
|
166
|
+
|
|
167
|
+
// Don't prevent process exit
|
|
168
|
+
if (this.flushTimer.unref) {
|
|
169
|
+
this.flushTimer.unref();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Flush batch to Vibe
|
|
175
|
+
*/
|
|
176
|
+
async flush(): Promise<void> {
|
|
177
|
+
if (this.isFlushing || this.batch.length === 0) return;
|
|
178
|
+
|
|
179
|
+
this.isFlushing = true;
|
|
180
|
+
const entries = [...this.batch];
|
|
181
|
+
this.batch = [];
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await this.sendToVibe(entries);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
// On failure, put entries back (up to limit to prevent memory issues)
|
|
187
|
+
if (this.batch.length < 100) {
|
|
188
|
+
this.batch = [...entries, ...this.batch].slice(0, 100);
|
|
189
|
+
}
|
|
190
|
+
// Log to console as fallback
|
|
191
|
+
console.error('[VibeLogTransport] Failed to flush logs:', err);
|
|
192
|
+
} finally {
|
|
193
|
+
this.isFlushing = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Push log entries to Redis for async drain processing
|
|
199
|
+
*/
|
|
200
|
+
private async sendToVibe(entries: LogEntry[]): Promise<void> {
|
|
201
|
+
if (entries.length === 0) return;
|
|
202
|
+
|
|
203
|
+
const redis = getRedis();
|
|
204
|
+
|
|
205
|
+
// Format entries for the drain to process
|
|
206
|
+
const logRecords = entries.map(e => JSON.stringify({
|
|
207
|
+
level: e.level,
|
|
208
|
+
message: e.message,
|
|
209
|
+
timestamp: e.timestamp,
|
|
210
|
+
source: 'next-mvp',
|
|
211
|
+
app_slug: this.appSlug,
|
|
212
|
+
vibe_client_id: this.vibeClientId,
|
|
213
|
+
category: e.category,
|
|
214
|
+
error_code: e.meta?.errorCode || e.meta?.error_code,
|
|
215
|
+
stack: e.meta?.stack,
|
|
216
|
+
request_id: e.meta?.requestId || e.meta?.request_id,
|
|
217
|
+
user_id: e.meta?.userId || e.meta?.user_id,
|
|
218
|
+
path: e.meta?.path,
|
|
219
|
+
method: e.meta?.method,
|
|
220
|
+
status_code: e.meta?.statusCode || e.meta?.status_code,
|
|
221
|
+
duration_ms: e.meta?.durationMs || e.meta?.duration_ms,
|
|
222
|
+
meta: e.meta,
|
|
223
|
+
queued_at: new Date().toISOString(),
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
// LPUSH all entries (newest first, drain will RPOP for FIFO)
|
|
227
|
+
await redis.lpush(REDIS_LOG_KEY, ...logRecords);
|
|
228
|
+
|
|
229
|
+
// Reset TTL on each write (1 week from last activity)
|
|
230
|
+
await redis.expire(REDIS_LOG_KEY, REDIS_LOG_TTL);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Close transport and flush remaining logs
|
|
235
|
+
*/
|
|
236
|
+
close(): void {
|
|
237
|
+
if (this.flushTimer) {
|
|
238
|
+
clearInterval(this.flushTimer);
|
|
239
|
+
this.flushTimer = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Final flush (best effort)
|
|
243
|
+
if (this.batch.length > 0) {
|
|
244
|
+
this.flush().catch(() => {});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create a configured VibeLogTransport instance
|
|
251
|
+
* Returns null if Redis is not configured (REDIS_URL env var)
|
|
252
|
+
*/
|
|
253
|
+
export function createVibeLogTransport(opts?: VibeLogTransportOptions): VibeLogTransport | null {
|
|
254
|
+
const transport = new VibeLogTransport(opts);
|
|
255
|
+
|
|
256
|
+
// Return null if transport is disabled (no Redis)
|
|
257
|
+
if (!transport['enabled']) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return transport;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default VibeLogTransport;
|