@payez/next-mvp 3.3.0 → 3.5.0

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Server-Side Auth Guard for Layouts
3
+ *
4
+ * Replaces middleware's self-fetch auth checks with direct Redis/function calls.
5
+ * Call from server-component layouts to protect routes.
6
+ *
7
+ * Zero HTTP self-fetches. ~8ms total (Redis + in-memory checks).
8
+ */
9
+ import 'server-only';
10
+ import type { SessionData } from '../lib/session-store';
11
+ export interface AuthGuardOptions {
12
+ /** Custom checks to run after standard auth validation */
13
+ checks?: AuthCheck[];
14
+ /** Override login redirect URL (default: /account-auth/login) */
15
+ loginUrl?: string;
16
+ /** Override 2FA redirect URL (default: /account-auth/verify-code) */
17
+ verifyCodeUrl?: string;
18
+ /** Override service unavailable URL (default: /service-unavailable) */
19
+ serviceUnavailableUrl?: string;
20
+ }
21
+ export interface AuthCheck {
22
+ /** Name for logging */
23
+ name: string;
24
+ /** Returns redirect URL if check fails, null if passes */
25
+ check: (session: SessionData, pathname: string) => Promise<string | null>;
26
+ }
27
+ export interface AuthGuardResult {
28
+ userId: string;
29
+ email: string;
30
+ roles: string[];
31
+ sessionData: SessionData;
32
+ accessToken?: string;
33
+ }
34
+ /**
35
+ * Server-side auth guard. Call from async server layouts.
36
+ *
37
+ * Redirects (via next/navigation redirect()) if:
38
+ * - No session cookie / invalid JWT
39
+ * - Session not in Redis (stale)
40
+ * - Session force-invalidated
41
+ * - 2FA required but not completed / expired
42
+ * - Any custom check fails
43
+ *
44
+ * Returns the authenticated user's session data on success.
45
+ */
46
+ export declare function authGuard(options?: AuthGuardOptions): Promise<AuthGuardResult>;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Server-Side Auth Guard for Layouts
4
+ *
5
+ * Replaces middleware's self-fetch auth checks with direct Redis/function calls.
6
+ * Call from server-component layouts to protect routes.
7
+ *
8
+ * Zero HTTP self-fetches. ~8ms total (Redis + in-memory checks).
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.authGuard = authGuard;
12
+ require("server-only");
13
+ const navigation_1 = require("next/navigation");
14
+ const headers_1 = require("next/headers");
15
+ const decode_session_1 = require("./decode-session");
16
+ const idp_client_config_1 = require("../lib/idp-client-config");
17
+ // =============================================================================
18
+ // CONSTANTS
19
+ // =============================================================================
20
+ const LOGIN_PAGE = '/account-auth/login';
21
+ const VERIFY_CODE_PAGE = '/account-auth/verify-code';
22
+ const SERVICE_UNAVAILABLE_PAGE = '/service-unavailable';
23
+ // =============================================================================
24
+ // MAIN
25
+ // =============================================================================
26
+ /**
27
+ * Server-side auth guard. Call from async server layouts.
28
+ *
29
+ * Redirects (via next/navigation redirect()) if:
30
+ * - No session cookie / invalid JWT
31
+ * - Session not in Redis (stale)
32
+ * - Session force-invalidated
33
+ * - 2FA required but not completed / expired
34
+ * - Any custom check fails
35
+ *
36
+ * Returns the authenticated user's session data on success.
37
+ */
38
+ async function authGuard(options) {
39
+ const loginUrl = options?.loginUrl || LOGIN_PAGE;
40
+ const verifyCodeUrl = options?.verifyCodeUrl || VERIFY_CODE_PAGE;
41
+ const serviceUnavailableUrl = options?.serviceUnavailableUrl || SERVICE_UNAVAILABLE_PAGE;
42
+ // Get current pathname from headers (set by Next.js)
43
+ const headerStore = await (0, headers_1.headers)();
44
+ const pathname = headerStore.get('x-next-pathname') ||
45
+ headerStore.get('x-invoke-path') ||
46
+ headerStore.get('x-matched-path') ||
47
+ '/';
48
+ const callbackUrl = encodeURIComponent(pathname);
49
+ // --- Decode session (cookie → JWT → Redis) ---
50
+ let decoded;
51
+ try {
52
+ decoded = await (0, decode_session_1.decodeSession)();
53
+ }
54
+ catch (error) {
55
+ // Redis unreachable or startup failure → fail closed
56
+ console.error('[AUTH-GUARD] Session decode failed (service error):', error instanceof Error ? error.message : String(error));
57
+ (0, navigation_1.redirect)(serviceUnavailableUrl);
58
+ }
59
+ // No session at all → redirect to login
60
+ if (!decoded) {
61
+ (0, navigation_1.redirect)(`${loginUrl}?callbackUrl=${callbackUrl}`);
62
+ }
63
+ const { sessionData } = decoded;
64
+ // --- Force-invalidated session (admin action, password change) ---
65
+ if (sessionData.forceInvalidated) {
66
+ console.warn('[AUTH-GUARD] Session force-invalidated', {
67
+ userId: sessionData.userId,
68
+ pathname,
69
+ });
70
+ (0, navigation_1.redirect)(`${loginUrl}?callbackUrl=${callbackUrl}&reason=invalidated`);
71
+ }
72
+ // --- 2FA check ---
73
+ try {
74
+ const config = await (0, idp_client_config_1.getIDPClientConfig)();
75
+ const requires2FA = config.authSettings?.require2FA ?? true;
76
+ if (requires2FA) {
77
+ const mfaVerified = sessionData.mfaVerified ?? sessionData.twoFactorComplete ?? false;
78
+ const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
79
+ const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
80
+ if (!mfaVerified || mfaExpired) {
81
+ console.log('[AUTH-GUARD] 2FA required', {
82
+ mfaVerified,
83
+ mfaExpired,
84
+ userId: sessionData.userId,
85
+ pathname,
86
+ });
87
+ (0, navigation_1.redirect)(`${verifyCodeUrl}?callbackUrl=${callbackUrl}`);
88
+ }
89
+ }
90
+ }
91
+ catch (error) {
92
+ // If we can't check 2FA config, fail closed
93
+ console.error('[AUTH-GUARD] 2FA config check failed:', error instanceof Error ? error.message : String(error));
94
+ (0, navigation_1.redirect)(serviceUnavailableUrl);
95
+ }
96
+ // --- Custom checks (beta, admin, etc.) ---
97
+ if (options?.checks) {
98
+ for (const check of options.checks) {
99
+ try {
100
+ const redirectUrl = await check.check(sessionData, pathname);
101
+ if (redirectUrl) {
102
+ console.log(`[AUTH-GUARD] Custom check "${check.name}" failed`, {
103
+ userId: sessionData.userId,
104
+ pathname,
105
+ redirectUrl,
106
+ });
107
+ (0, navigation_1.redirect)(redirectUrl);
108
+ }
109
+ }
110
+ catch (error) {
111
+ // If the error is a redirect (from next/navigation), re-throw it
112
+ if (error && typeof error === 'object' && 'digest' in error) {
113
+ throw error;
114
+ }
115
+ console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, error instanceof Error ? error.message : String(error));
116
+ (0, navigation_1.redirect)(serviceUnavailableUrl);
117
+ }
118
+ }
119
+ }
120
+ // --- All checks passed ---
121
+ return {
122
+ userId: sessionData.userId,
123
+ email: sessionData.email,
124
+ roles: sessionData.roles || [],
125
+ sessionData,
126
+ accessToken: sessionData.idpAccessToken,
127
+ };
128
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Server-Side Session Decoder
3
+ *
4
+ * Reads the JWT session cookie, decodes it with jose, and fetches the
5
+ * full session from Redis. Used by authGuard (layouts) and withAuth (API routes).
6
+ *
7
+ * Zero HTTP self-fetches. Direct Redis reads only.
8
+ */
9
+ import 'server-only';
10
+ import { type JWTPayload } from 'jose';
11
+ import { type SessionData } from '../lib/session-store';
12
+ export interface DecodedSession {
13
+ sessionData: SessionData;
14
+ jwtPayload: JWTPayload & {
15
+ sessionToken?: string;
16
+ redisSessionId?: string;
17
+ };
18
+ }
19
+ /**
20
+ * Decode the session from cookies and Redis.
21
+ * Returns null if no valid session exists.
22
+ *
23
+ * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
24
+ * If omitted, uses next/headers cookies() for server components.
25
+ */
26
+ export declare function decodeSession(requestCookies?: {
27
+ get: (name: string) => {
28
+ value: string;
29
+ } | undefined;
30
+ }): Promise<DecodedSession | null>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Server-Side Session Decoder
4
+ *
5
+ * Reads the JWT session cookie, decodes it with jose, and fetches the
6
+ * full session from Redis. Used by authGuard (layouts) and withAuth (API routes).
7
+ *
8
+ * Zero HTTP self-fetches. Direct Redis reads only.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.decodeSession = decodeSession;
12
+ require("server-only");
13
+ const headers_1 = require("next/headers");
14
+ const jose_1 = require("jose");
15
+ const session_store_1 = require("../lib/session-store");
16
+ const idp_client_config_1 = require("../lib/idp-client-config");
17
+ const app_slug_1 = require("../lib/app-slug");
18
+ const startup_init_1 = require("../lib/startup-init");
19
+ /**
20
+ * Decode the session from cookies and Redis.
21
+ * Returns null if no valid session exists.
22
+ *
23
+ * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
24
+ * If omitted, uses next/headers cookies() for server components.
25
+ */
26
+ async function decodeSession(requestCookies) {
27
+ try {
28
+ // Ensure startup initialization is complete (Redis, IDP config, etc.)
29
+ await (0, startup_init_1.ensureInitialized)();
30
+ // Get the JWT cookie value
31
+ const cookieStore = requestCookies || (await (0, headers_1.cookies)());
32
+ const sessionCookieName = (0, app_slug_1.getSessionCookieName)();
33
+ const secureCookieName = (0, app_slug_1.getSecureSessionCookieName)();
34
+ const cookieValue = cookieStore.get(secureCookieName)?.value ||
35
+ cookieStore.get(sessionCookieName)?.value;
36
+ if (!cookieValue) {
37
+ return null;
38
+ }
39
+ // Get the NextAuth secret from IDP config
40
+ const config = await (0, idp_client_config_1.getIDPClientConfig)();
41
+ const secret = config.nextAuthSecret;
42
+ if (!secret) {
43
+ console.error('[DECODE-SESSION] No nextAuthSecret available from IDP config');
44
+ return null;
45
+ }
46
+ // Decode the JWT (same pattern as test-aware-get-token.ts)
47
+ const secretKey = new TextEncoder().encode(secret);
48
+ let payload;
49
+ try {
50
+ const result = await (0, jose_1.jwtVerify)(cookieValue, secretKey);
51
+ payload = result.payload;
52
+ }
53
+ catch (jwtError) {
54
+ // JWT decode failed - cookie may be corrupted or secret rotated
55
+ console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
56
+ return null;
57
+ }
58
+ // Extract the Redis session ID from JWT payload
59
+ const sessionToken = payload.sessionToken || payload.redisSessionId;
60
+ if (!sessionToken) {
61
+ console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
62
+ return null;
63
+ }
64
+ // Fetch session from Redis (direct, no HTTP)
65
+ const sessionData = await (0, session_store_1.getSession)(sessionToken);
66
+ if (!sessionData) {
67
+ return null;
68
+ }
69
+ return {
70
+ sessionData,
71
+ jwtPayload: payload,
72
+ };
73
+ }
74
+ catch (error) {
75
+ console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
76
+ return null;
77
+ }
78
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Slim Middleware — Cookie-Only Auth Check
3
+ *
4
+ * Replaces the self-fetching middleware with a cookie existence check.
5
+ * All real auth validation happens in server-side layouts (authGuard).
6
+ *
7
+ * Zero self-fetches. Zero Redis calls. Zero JWT decoding.
8
+ * Just: does the session cookie exist? Yes → pass through. No → redirect to login.
9
+ */
10
+ import { NextRequest, NextResponse } from 'next/server';
11
+ export interface SlimMiddlewareOptions {
12
+ /** Routes that don't require authentication (glob-style patterns) */
13
+ publicRoutes?: string[];
14
+ /** Login page URL (default: /account-auth/login) */
15
+ loginUrl?: string;
16
+ /** Additional paths to always bypass (e.g., /api/auth/, /api/session/) */
17
+ bypassPrefixes?: string[];
18
+ }
19
+ /**
20
+ * Create a slim middleware that only checks cookie existence.
21
+ * Auth validation is deferred to server-side layouts (authGuard).
22
+ */
23
+ export declare function createSlimMiddleware(options?: SlimMiddlewareOptions): (request: NextRequest) => NextResponse;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Slim Middleware — Cookie-Only Auth Check
4
+ *
5
+ * Replaces the self-fetching middleware with a cookie existence check.
6
+ * All real auth validation happens in server-side layouts (authGuard).
7
+ *
8
+ * Zero self-fetches. Zero Redis calls. Zero JWT decoding.
9
+ * Just: does the session cookie exist? Yes → pass through. No → redirect to login.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.createSlimMiddleware = createSlimMiddleware;
13
+ const server_1 = require("next/server");
14
+ const app_slug_1 = require("../lib/app-slug");
15
+ // =============================================================================
16
+ // DEFAULT BYPASS PATHS
17
+ // =============================================================================
18
+ /** Routes that must always bypass middleware (prevent infinite loops) */
19
+ const DEFAULT_BYPASS_PREFIXES = [
20
+ '/api/auth/',
21
+ '/api/session/',
22
+ '/_next/',
23
+ '/favicon.ico',
24
+ ];
25
+ /** Static file extensions to bypass */
26
+ const STATIC_EXTENSIONS = /\.(svg|png|jpg|jpeg|gif|webp|ico|css|js|woff|woff2|ttf|eot|map)$/i;
27
+ // =============================================================================
28
+ // MAIN
29
+ // =============================================================================
30
+ /**
31
+ * Create a slim middleware that only checks cookie existence.
32
+ * Auth validation is deferred to server-side layouts (authGuard).
33
+ */
34
+ function createSlimMiddleware(options) {
35
+ const publicRoutes = options?.publicRoutes || [];
36
+ const loginUrl = options?.loginUrl || '/account-auth/login';
37
+ const extraBypass = options?.bypassPrefixes || [];
38
+ const allBypass = [...DEFAULT_BYPASS_PREFIXES, ...extraBypass];
39
+ // Pre-compile public route patterns for fast matching
40
+ const publicMatchers = publicRoutes.map(pattern => {
41
+ if (pattern.endsWith('/*')) {
42
+ const prefix = pattern.slice(0, -2);
43
+ return (p) => p === prefix || p.startsWith(prefix + '/');
44
+ }
45
+ if (pattern.endsWith('*')) {
46
+ const prefix = pattern.slice(0, -1);
47
+ return (p) => p.startsWith(prefix);
48
+ }
49
+ if (pattern.startsWith('/*.')) {
50
+ const ext = pattern.slice(2);
51
+ return (p) => p.endsWith(ext);
52
+ }
53
+ return (p) => p === pattern;
54
+ });
55
+ return function middleware(request) {
56
+ const { pathname } = request.nextUrl;
57
+ // 1. Always bypass static/internal routes
58
+ if (STATIC_EXTENSIONS.test(pathname)) {
59
+ return server_1.NextResponse.next();
60
+ }
61
+ for (const prefix of allBypass) {
62
+ if (pathname.startsWith(prefix)) {
63
+ return server_1.NextResponse.next();
64
+ }
65
+ }
66
+ // 2. Check if it's a public route → pass through
67
+ for (const matcher of publicMatchers) {
68
+ if (matcher(pathname)) {
69
+ return server_1.NextResponse.next();
70
+ }
71
+ }
72
+ // 3. THE ONLY AUTH CHECK: Does a session cookie exist?
73
+ const sessionCookieName = (0, app_slug_1.getSessionCookieName)();
74
+ const secureCookieName = (0, app_slug_1.getSecureSessionCookieName)();
75
+ const hasCookie = request.cookies.has(sessionCookieName) ||
76
+ request.cookies.has(secureCookieName);
77
+ if (!hasCookie) {
78
+ // No cookie on a protected route → redirect to login
79
+ // API routes get 401 instead of redirect
80
+ if (pathname.startsWith('/api/')) {
81
+ return server_1.NextResponse.json({ error: 'Unauthorized', message: 'No session' }, { status: 401 });
82
+ }
83
+ const callbackUrl = encodeURIComponent(pathname);
84
+ return server_1.NextResponse.redirect(new URL(`${loginUrl}?callbackUrl=${callbackUrl}`, request.url));
85
+ }
86
+ // Cookie exists → pass through, layout authGuard does the real validation
87
+ return server_1.NextResponse.next();
88
+ };
89
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Server-Side Auth Wrapper for API Routes & Server Actions
3
+ *
4
+ * Wraps route handlers with session validation. Uses direct Redis reads.
5
+ * Zero HTTP self-fetches.
6
+ *
7
+ * Usage:
8
+ * export const GET = withAuth(async (req, auth) => {
9
+ * return NextResponse.json({ userId: auth.userId });
10
+ * });
11
+ *
12
+ * // With role requirement:
13
+ * export const POST = withAuth(async (req, auth) => { ... }, { requiredRoles: ['admin'] });
14
+ */
15
+ import 'server-only';
16
+ import { NextRequest, NextResponse } from 'next/server';
17
+ import type { SessionData } from '../lib/session-store';
18
+ export interface ApiAuthResult {
19
+ userId: string;
20
+ email: string;
21
+ roles: string[];
22
+ sessionData: SessionData;
23
+ accessToken?: string;
24
+ }
25
+ export interface WithAuthOptions {
26
+ /** Roles required to access the route (any match = allowed) */
27
+ requiredRoles?: string[];
28
+ }
29
+ /**
30
+ * Wrap an API route handler with auth validation.
31
+ * Returns 401 if not authenticated, 403 if missing required roles.
32
+ */
33
+ export declare function withAuth(handler: (req: NextRequest, auth: ApiAuthResult) => Promise<NextResponse>, options?: WithAuthOptions): (req: NextRequest) => Promise<NextResponse>;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * Server-Side Auth Wrapper for API Routes & Server Actions
4
+ *
5
+ * Wraps route handlers with session validation. Uses direct Redis reads.
6
+ * Zero HTTP self-fetches.
7
+ *
8
+ * Usage:
9
+ * export const GET = withAuth(async (req, auth) => {
10
+ * return NextResponse.json({ userId: auth.userId });
11
+ * });
12
+ *
13
+ * // With role requirement:
14
+ * export const POST = withAuth(async (req, auth) => { ... }, { requiredRoles: ['admin'] });
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.withAuth = withAuth;
18
+ require("server-only");
19
+ const server_1 = require("next/server");
20
+ const decode_session_1 = require("./decode-session");
21
+ // =============================================================================
22
+ // MAIN
23
+ // =============================================================================
24
+ /**
25
+ * Wrap an API route handler with auth validation.
26
+ * Returns 401 if not authenticated, 403 if missing required roles.
27
+ */
28
+ function withAuth(handler, options) {
29
+ return async (req) => {
30
+ try {
31
+ // Decode session from request cookies (direct Redis, no self-fetch)
32
+ const decoded = await (0, decode_session_1.decodeSession)(req.cookies);
33
+ if (!decoded) {
34
+ return server_1.NextResponse.json({ error: 'Unauthorized', message: 'No valid session' }, { status: 401 });
35
+ }
36
+ const { sessionData } = decoded;
37
+ // Check required roles
38
+ if (options?.requiredRoles && options.requiredRoles.length > 0) {
39
+ const userRoles = sessionData.roles || [];
40
+ const hasRole = options.requiredRoles.some(r => userRoles.includes(r));
41
+ if (!hasRole) {
42
+ return server_1.NextResponse.json({ error: 'Forbidden', message: 'Insufficient permissions' }, { status: 403 });
43
+ }
44
+ }
45
+ const auth = {
46
+ userId: sessionData.userId,
47
+ email: sessionData.email,
48
+ roles: sessionData.roles || [],
49
+ sessionData,
50
+ accessToken: sessionData.idpAccessToken,
51
+ };
52
+ return handler(req, auth);
53
+ }
54
+ catch (error) {
55
+ console.error('[WITH-AUTH] Error:', error instanceof Error ? error.message : String(error));
56
+ return server_1.NextResponse.json({ error: 'Internal Server Error', message: 'Auth check failed' }, { status: 500 });
57
+ }
58
+ };
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -816,6 +816,31 @@
816
816
  "types": "./dist/pages/client-admin/index.d.ts",
817
817
  "require": "./dist/pages/client-admin/index.js",
818
818
  "default": "./dist/pages/client-admin/index.js"
819
+ },
820
+ "./pages/coming-soon": {
821
+ "types": "./dist/pages/coming-soon/page.d.ts",
822
+ "require": "./dist/pages/coming-soon/page.js",
823
+ "default": "./dist/pages/coming-soon/page.js"
824
+ },
825
+ "./server/auth-guard": {
826
+ "types": "./dist/server/auth-guard.d.ts",
827
+ "require": "./dist/server/auth-guard.js",
828
+ "default": "./dist/server/auth-guard.js"
829
+ },
830
+ "./server/with-auth": {
831
+ "types": "./dist/server/with-auth.d.ts",
832
+ "require": "./dist/server/with-auth.js",
833
+ "default": "./dist/server/with-auth.js"
834
+ },
835
+ "./server/slim-middleware": {
836
+ "types": "./dist/server/slim-middleware.d.ts",
837
+ "require": "./dist/server/slim-middleware.js",
838
+ "default": "./dist/server/slim-middleware.js"
839
+ },
840
+ "./server/decode-session": {
841
+ "types": "./dist/server/decode-session.d.ts",
842
+ "require": "./dist/server/decode-session.js",
843
+ "default": "./dist/server/decode-session.js"
819
844
  }
820
845
  },
821
846
  "peerDependencies": {
@@ -83,7 +83,11 @@ export function UserAvatarMenu({
83
83
  return null;
84
84
  }
85
85
 
86
- const userInitial = session.user.email?.charAt(0).toUpperCase() || 'U';
86
+ // Derive display initial from name or email ignore anon/internal IDs
87
+ const userName = (session.user as any)?.name;
88
+ const userEmail = session.user.email;
89
+ const displaySource = userName || userEmail;
90
+ const userInitial = displaySource?.charAt(0).toUpperCase() || '?';
87
91
 
88
92
  const handleNavigation = (path: string) => {
89
93
  setIsOpen(false);
@@ -132,11 +136,23 @@ export function UserAvatarMenu({
132
136
  role="menu"
133
137
  aria-orientation="vertical"
134
138
  >
135
- {/* User email label */}
139
+ {/* User identity label */}
136
140
  <div className="px-4 py-3 border-b border-gray-200 dark:border-slate-700">
137
- <p className="text-sm text-gray-500 dark:text-slate-400 truncate">
138
- {session.user.email}
139
- </p>
141
+ {userName && (
142
+ <p className="text-sm font-medium text-gray-700 dark:text-slate-200 truncate">
143
+ {userName}
144
+ </p>
145
+ )}
146
+ {userEmail && (
147
+ <p className="text-sm text-gray-500 dark:text-slate-400 truncate">
148
+ {userEmail}
149
+ </p>
150
+ )}
151
+ {!userName && !userEmail && (
152
+ <p className="text-sm text-gray-500 dark:text-slate-400">
153
+ Signed in
154
+ </p>
155
+ )}
140
156
  </div>
141
157
 
142
158
  {/* Menu items */}
@@ -11,15 +11,29 @@
11
11
  * - Graceful degradation on failure
12
12
  */
13
13
 
14
- import Transport from 'winston-transport';
15
14
  import { getRedis } from '../lib/redis';
16
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
+
17
29
  /** Redis key for pending log entries */
18
30
  const REDIS_LOG_KEY = 'vibe:logs:pending';
19
31
  /** TTL in seconds: 1 week */
20
32
  const REDIS_LOG_TTL = 7 * 24 * 60 * 60;
21
33
 
22
- export interface VibeLogTransportOptions extends Transport.TransportStreamOptions {
34
+ export interface VibeLogTransportOptions {
35
+ /** Winston transport level */
36
+ level?: string;
23
37
  /** Redis URL (optional, uses REDIS_URL env var by default) */
24
38
  redisUrl?: string;
25
39
  /** Vibe client ID (for log metadata) */
@@ -55,7 +69,7 @@ const LEVEL_ORDER: Record<string, number> = {
55
69
  /**
56
70
  * Winston transport that buffers logs to Redis for Vibe drain processing
57
71
  */
58
- export class VibeLogTransport extends Transport {
72
+ export class VibeLogTransport extends TransportBase {
59
73
  private vibeClientId: string;
60
74
  private appSlug: string;
61
75
  private minLevelNum: number;
@@ -87,6 +87,14 @@ export async function getSession(sessionToken: string): Promise<SessionData | nu
87
87
  }
88
88
  }
89
89
 
90
+ /**
91
+ * Refresh session TTL without reading/writing data (sliding window expiry).
92
+ */
93
+ export async function touchSession(token: string): Promise<void> {
94
+ const key = getSessionKey(token);
95
+ await redis.expire(key, SESSION_TTL);
96
+ }
97
+
90
98
  /**
91
99
  * Retrieves a session along with a version identifier for optimistic locking.
92
100
  * @param sessionToken The session token to look up.