@payez/next-mvp 4.0.14 → 4.0.16

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.
@@ -53,6 +53,7 @@ function JwtInspectPage() {
53
53
  const [isDarkMode, setIsDarkMode] = (0, react_1.useState)(false);
54
54
  const [jwtHeader, setJwtHeader] = (0, react_1.useState)(null);
55
55
  const [jwtPayload, setJwtPayload] = (0, react_1.useState)(null);
56
+ const [serverSession, setServerSession] = (0, react_1.useState)(null);
56
57
  // Detect dark mode
57
58
  (0, react_1.useEffect)(() => {
58
59
  const checkDarkMode = () => {
@@ -65,14 +66,26 @@ function JwtInspectPage() {
65
66
  mediaQuery.addEventListener('change', checkDarkMode);
66
67
  return () => mediaQuery.removeEventListener('change', checkDarkMode);
67
68
  }, []);
68
- // Decode JWT header and payload when accessToken changes
69
+ // Fetch enriched session from server (has IDP tokens, roles, userId)
69
70
  (0, react_1.useEffect)(() => {
70
- const ext = session;
71
- if (ext?.accessToken) {
72
- setJwtHeader(decodeJwtHeader(ext.accessToken));
73
- setJwtPayload(decodeJwtPayload(ext.accessToken));
74
- }
71
+ if (!session)
72
+ return;
73
+ fetch('/api/test-env/session-data')
74
+ .then(r => r.ok ? r.json() : null)
75
+ .then(d => {
76
+ if (d)
77
+ setServerSession(d);
78
+ })
79
+ .catch(() => { });
75
80
  }, [session]);
81
+ // Decode JWT header and payload from server session's IDP access token
82
+ (0, react_1.useEffect)(() => {
83
+ const token = serverSession?.idpAccessToken || session?.accessToken;
84
+ if (token) {
85
+ setJwtHeader(decodeJwtHeader(token));
86
+ setJwtPayload(decodeJwtPayload(token));
87
+ }
88
+ }, [serverSession, session]);
76
89
  const copyToClipboard = (text, label) => {
77
90
  navigator.clipboard.writeText(text);
78
91
  setCopied(label);
@@ -84,16 +97,40 @@ function JwtInspectPage() {
84
97
  if (status === 'unauthenticated') {
85
98
  return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-red-900/30 border border-red-800' : 'bg-red-50 border border-red-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-2", children: "Not Authenticated" }), (0, jsx_runtime_1.jsx)("p", { children: "Please log in to inspect session data." })] }) }));
86
99
  }
87
- // Extended session with all custom fields
100
+ // Merge server-side enriched session with client session
101
+ const ss = serverSession || {};
88
102
  const ext = session;
89
- const user = ext?.user || {};
103
+ const user = {
104
+ id: ss.userId || ext?.user?.id,
105
+ email: ss.email || ext?.user?.email,
106
+ name: ss.name || ext?.user?.name,
107
+ roles: ss.roles || [],
108
+ oauthProvider: ss.oauthProvider,
109
+ idpClientId: ss.idpClientId,
110
+ merchantId: ss.merchantId,
111
+ mfaVerified: ss.mfaVerified,
112
+ requiresTwoFactor: false,
113
+ authenticationMethods: ss.authenticationMethods,
114
+ authenticationLevel: ss.authenticationLevel,
115
+ mfaCompletedAt: ss.mfaCompletedAt,
116
+ mfaExpiresAt: ss.mfaExpiresAt,
117
+ twoFactorSessionVerified: ss.mfaVerified,
118
+ };
119
+ // Token fields for display
120
+ const displayExt = {
121
+ ...ext,
122
+ sessionToken: ss.sessionToken || ext?.sessionToken,
123
+ accessToken: ss.idpAccessToken || ext?.accessToken,
124
+ refreshToken: ss.idpRefreshToken || ext?.refreshToken,
125
+ accessTokenExpires: ss.idpAccessTokenExpires || ext?.accessTokenExpires,
126
+ };
90
127
  // Card styling helpers
91
128
  const cardClass = isDarkMode ? 'bg-slate-900 border border-slate-700' : 'bg-white border border-gray-200';
92
129
  const labelClass = isDarkMode ? 'text-slate-400' : 'text-gray-500';
93
130
  const valueClass = isDarkMode ? 'text-white' : 'text-gray-900';
94
- return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-4xl mx-auto space-y-6", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold", children: "Session Inspector" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${labelClass}`, children: "Session data from Redis (via NextAuth session callback)" }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "User Identity" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "User ID", value: user.id, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Email", value: user.email, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Name", value: user.name, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "OAuth Provider", value: user.oauthProvider, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "IDP Client ID", value: user.idpClientId, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Merchant ID", value: user.merchantId, labelClass: labelClass, valueClass: valueClass })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-purple-900/30 border border-purple-700' : 'bg-purple-50 border border-purple-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Roles" }), user.roles && user.roles.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-2", children: user.roles.map((role) => ((0, jsx_runtime_1.jsx)("span", { className: `px-3 py-1 rounded-full text-sm font-medium ${isDarkMode ? 'bg-purple-800 text-purple-100' : 'bg-purple-200 text-purple-800'}`, children: role }, role))) })) : ((0, jsx_runtime_1.jsx)("p", { className: labelClass, children: "No roles assigned" }))] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-yellow-900/30 border border-yellow-700' : 'bg-yellow-50 border border-yellow-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "2FA Status" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "2FA Verified:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: user.twoFactorSessionVerified, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Requires 2FA:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: user.requiresTwoFactor, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode, invertColors: true })] }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Auth Methods (AMR)", value: user.authenticationMethods?.join(', '), labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Auth Level (ACR)", value: user.authenticationLevel, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "MFA Completed At", value: user.mfaCompletedAt ? new Date(user.mfaCompletedAt).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "MFA Expires At", value: user.mfaExpiresAt ? new Date(user.mfaExpiresAt).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-blue-900/30 border border-blue-700' : 'bg-blue-50 border border-blue-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Tokens" }), (0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Session Token (Redis Key):" }), ext.sessionToken && ((0, jsx_runtime_1.jsx)(CopyButton, { onClick: () => copyToClipboard(ext.sessionToken, 'session'), copied: copied === 'session', isDarkMode: isDarkMode }))] }), (0, jsx_runtime_1.jsx)("code", { className: `block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: ext.sessionToken || 'N/A' })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Access Token (IDP):" }), ext.accessToken && ((0, jsx_runtime_1.jsx)(CopyButton, { onClick: () => copyToClipboard(ext.accessToken, 'access'), copied: copied === 'access', isDarkMode: isDarkMode }))] }), (0, jsx_runtime_1.jsx)("code", { className: `block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: ext.accessToken || 'N/A' })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Has Refresh Token:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: !!ext.refreshToken, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode })] }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Access Token Expires", value: ext.accessTokenExpires ? new Date(ext.accessTokenExpires).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass })] })] })] }), jwtHeader && ((0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-orange-900/30 border border-orange-700' : 'bg-orange-50 border border-orange-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "JWT Header" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Algorithm (alg)", value: jwtHeader.alg, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Type (typ)", value: jwtHeader.typ, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Key ID (kid):" }), ' ', (0, jsx_runtime_1.jsx)("span", { className: `font-mono ${jwtHeader.kid ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`, children: jwtHeader.kid || 'NOT PRESENT' })] }), Object.entries(jwtHeader)
131
+ return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-4xl mx-auto space-y-6", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold", children: "Session Inspector" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${labelClass}`, children: "Better Auth session + IDP tokens from Redis" }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "User Identity" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "User ID", value: user.id, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Email", value: user.email, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Name", value: user.name, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "OAuth Provider", value: user.oauthProvider, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "IDP Client ID", value: user.idpClientId, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Merchant ID", value: user.merchantId, labelClass: labelClass, valueClass: valueClass })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-purple-900/30 border border-purple-700' : 'bg-purple-50 border border-purple-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Roles" }), user.roles && user.roles.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-2", children: user.roles.map((role) => ((0, jsx_runtime_1.jsx)("span", { className: `px-3 py-1 rounded-full text-sm font-medium ${isDarkMode ? 'bg-purple-800 text-purple-100' : 'bg-purple-200 text-purple-800'}`, children: role }, role))) })) : ((0, jsx_runtime_1.jsx)("p", { className: labelClass, children: "No roles assigned" }))] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-yellow-900/30 border border-yellow-700' : 'bg-yellow-50 border border-yellow-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "2FA Status" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "2FA Verified:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: user.twoFactorSessionVerified, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Requires 2FA:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: user.requiresTwoFactor, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode, invertColors: true })] }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Auth Methods (AMR)", value: user.authenticationMethods?.join(', '), labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Auth Level (ACR)", value: user.authenticationLevel, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "MFA Completed At", value: user.mfaCompletedAt ? new Date(user.mfaCompletedAt).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "MFA Expires At", value: user.mfaExpiresAt ? new Date(user.mfaExpiresAt).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-blue-900/30 border border-blue-700' : 'bg-blue-50 border border-blue-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Tokens" }), (0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Session Token (Redis Key):" }), displayExt.sessionToken && ((0, jsx_runtime_1.jsx)(CopyButton, { onClick: () => copyToClipboard(displayExt.sessionToken, 'session'), copied: copied === 'session', isDarkMode: isDarkMode }))] }), (0, jsx_runtime_1.jsx)("code", { className: `block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: displayExt.sessionToken || 'N/A' })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Access Token (IDP):" }), displayExt.accessToken && ((0, jsx_runtime_1.jsx)(CopyButton, { onClick: () => copyToClipboard(displayExt.accessToken, 'access'), copied: copied === 'access', isDarkMode: isDarkMode }))] }), (0, jsx_runtime_1.jsx)("code", { className: `block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: displayExt.accessToken || 'N/A' })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Has Refresh Token:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: !!displayExt.refreshToken, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode })] }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Access Token Expires", value: displayExt.accessTokenExpires ? new Date(displayExt.accessTokenExpires).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass })] })] })] }), jwtHeader && ((0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-orange-900/30 border border-orange-700' : 'bg-orange-50 border border-orange-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "JWT Header" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Algorithm (alg)", value: jwtHeader.alg, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Type (typ)", value: jwtHeader.typ, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Key ID (kid):" }), ' ', (0, jsx_runtime_1.jsx)("span", { className: `font-mono ${jwtHeader.kid ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`, children: jwtHeader.kid || 'NOT PRESENT' })] }), Object.entries(jwtHeader)
95
132
  .filter(([key]) => !['alg', 'typ', 'kid'].includes(key))
96
- .map(([key, value]) => ((0, jsx_runtime_1.jsx)(InfoRow, { label: key, value: typeof value === 'object' ? JSON.stringify(value) : String(value), labelClass: labelClass, valueClass: valueClass }, key)))] })] })), jwtPayload && ((0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-green-900/30 border border-green-700' : 'bg-green-50 border border-green-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "JWT Payload Claims" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Subject (sub)", value: jwtPayload.sub, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Issuer (iss)", value: jwtPayload.iss, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Audience (aud)", value: Array.isArray(jwtPayload.aud) ? jwtPayload.aud.join(', ') : jwtPayload.aud, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Client ID", value: jwtPayload.client_id, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Expires (exp)", value: jwtPayload.exp ? new Date(jwtPayload.exp * 1000).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Issued At (iat)", value: jwtPayload.iat ? new Date(jwtPayload.iat * 1000).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "AMR (Auth Methods)", value: Array.isArray(jwtPayload.amr) ? jwtPayload.amr.join(', ') : jwtPayload.amr, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "ACR (Auth Context)", value: jwtPayload.acr, labelClass: labelClass, valueClass: valueClass })] })] })), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Session Metadata" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Session Expires", value: session?.expires ? new Date(session.expires).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Error", value: ext.error, labelClass: labelClass, valueClass: ext.error ? 'text-red-500' : valueClass })] })] }), (0, jsx_runtime_1.jsxs)("details", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("summary", { className: "font-semibold cursor-pointer", children: "Raw Session Data (Click to expand)" }), (0, jsx_runtime_1.jsx)("pre", { className: `mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: JSON.stringify(session, null, 2) })] })] }) }));
133
+ .map(([key, value]) => ((0, jsx_runtime_1.jsx)(InfoRow, { label: key, value: typeof value === 'object' ? JSON.stringify(value) : String(value), labelClass: labelClass, valueClass: valueClass }, key)))] })] })), jwtPayload && ((0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-green-900/30 border border-green-700' : 'bg-green-50 border border-green-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "JWT Payload Claims" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Subject (sub)", value: jwtPayload.sub, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Issuer (iss)", value: jwtPayload.iss, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Audience (aud)", value: Array.isArray(jwtPayload.aud) ? jwtPayload.aud.join(', ') : jwtPayload.aud, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Client ID", value: jwtPayload.client_id, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Expires (exp)", value: jwtPayload.exp ? new Date(jwtPayload.exp * 1000).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Issued At (iat)", value: jwtPayload.iat ? new Date(jwtPayload.iat * 1000).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "AMR (Auth Methods)", value: Array.isArray(jwtPayload.amr) ? jwtPayload.amr.join(', ') : jwtPayload.amr, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "ACR (Auth Context)", value: jwtPayload.acr, labelClass: labelClass, valueClass: valueClass })] })] })), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Session Metadata" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Session Expires", value: session?.expires ? new Date(session.expires).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Error", value: ext.error, labelClass: labelClass, valueClass: ext.error ? 'text-red-500' : valueClass })] })] }), (0, jsx_runtime_1.jsxs)("details", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("summary", { className: "font-semibold cursor-pointer", children: "Raw Session Data (Click to expand)" }), (0, jsx_runtime_1.jsx)("pre", { className: `mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: JSON.stringify({ clientSession: session, serverSession: serverSession }, null, 2) })] })] }) }));
97
134
  }
98
135
  // Helper Components
99
136
  function InfoRow({ label, value, labelClass, valueClass, }) {
@@ -99,16 +99,30 @@ async function tryBetterAuthSession(requestCookies) {
99
99
  if (!result?.session || !result?.user) {
100
100
  return null;
101
101
  }
102
- // Map Better Auth session to SessionData
102
+ // Read IDP tokens from BA Redis session (stored by post-login hook)
103
+ let idpTokens = null;
104
+ try {
105
+ const { getRedis } = await Promise.resolve().then(() => __importStar(require('../lib/redis')));
106
+ const { getAppSlug } = await Promise.resolve().then(() => __importStar(require('../lib/app-slug')));
107
+ const baKey = `ba:${getAppSlug()}:${result.session.token}`;
108
+ const baRaw = await getRedis().get(baKey);
109
+ if (baRaw) {
110
+ const baData = JSON.parse(baRaw);
111
+ idpTokens = baData.idpTokens;
112
+ }
113
+ }
114
+ catch { /* Redis unavailable */ }
115
+ // Map Better Auth session + IDP tokens to SessionData
103
116
  const sessionData = {
104
- userId: result.user.id || '',
105
- email: result.user.email || '',
106
- name: result.user.name || undefined,
107
- roles: [],
108
- idpAccessTokenExpires: result.session.expiresAt
109
- ? new Date(result.session.expiresAt).getTime()
110
- : Date.now() + 24 * 60 * 60 * 1000,
111
- mfaVerified: true, // Social login doesn't require MFA
117
+ userId: idpTokens?.userId || result.user.id || '',
118
+ email: idpTokens?.email || result.user.email || '',
119
+ name: idpTokens?.name || result.user.name || undefined,
120
+ roles: idpTokens?.roles || [],
121
+ idpAccessToken: idpTokens?.idpAccessToken,
122
+ idpRefreshToken: idpTokens?.idpRefreshToken,
123
+ idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
124
+ || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
125
+ mfaVerified: true,
112
126
  oauthProvider: 'google',
113
127
  };
114
128
  const jwtPayload = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.0.14",
3
+ "version": "4.0.16",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,6 +49,7 @@ export function JwtInspectPage() {
49
49
  const [isDarkMode, setIsDarkMode] = useState(false);
50
50
  const [jwtHeader, setJwtHeader] = useState<any>(null);
51
51
  const [jwtPayload, setJwtPayload] = useState<any>(null);
52
+ const [serverSession, setServerSession] = useState<any>(null);
52
53
 
53
54
  // Detect dark mode
54
55
  useEffect(() => {
@@ -63,15 +64,26 @@ export function JwtInspectPage() {
63
64
  return () => mediaQuery.removeEventListener('change', checkDarkMode);
64
65
  }, []);
65
66
 
66
- // Decode JWT header and payload when accessToken changes
67
+ // Fetch enriched session from server (has IDP tokens, roles, userId)
67
68
  useEffect(() => {
68
- const ext = session as any;
69
- if (ext?.accessToken) {
70
- setJwtHeader(decodeJwtHeader(ext.accessToken));
71
- setJwtPayload(decodeJwtPayload(ext.accessToken));
72
- }
69
+ if (!session) return;
70
+ fetch('/api/test-env/session-data')
71
+ .then(r => r.ok ? r.json() : null)
72
+ .then(d => {
73
+ if (d) setServerSession(d);
74
+ })
75
+ .catch(() => {});
73
76
  }, [session]);
74
77
 
78
+ // Decode JWT header and payload from server session's IDP access token
79
+ useEffect(() => {
80
+ const token = serverSession?.idpAccessToken || (session as any)?.accessToken;
81
+ if (token) {
82
+ setJwtHeader(decodeJwtHeader(token));
83
+ setJwtPayload(decodeJwtPayload(token));
84
+ }
85
+ }, [serverSession, session]);
86
+
75
87
  const copyToClipboard = (text: string, label: string) => {
76
88
  navigator.clipboard.writeText(text);
77
89
  setCopied(label);
@@ -97,9 +109,33 @@ export function JwtInspectPage() {
97
109
  );
98
110
  }
99
111
 
100
- // Extended session with all custom fields
112
+ // Merge server-side enriched session with client session
113
+ const ss = serverSession || {};
101
114
  const ext = session as any;
102
- const user = ext?.user || {};
115
+ const user = {
116
+ id: ss.userId || ext?.user?.id,
117
+ email: ss.email || ext?.user?.email,
118
+ name: ss.name || ext?.user?.name,
119
+ roles: ss.roles || [],
120
+ oauthProvider: ss.oauthProvider,
121
+ idpClientId: ss.idpClientId,
122
+ merchantId: ss.merchantId,
123
+ mfaVerified: ss.mfaVerified,
124
+ requiresTwoFactor: false,
125
+ authenticationMethods: ss.authenticationMethods,
126
+ authenticationLevel: ss.authenticationLevel,
127
+ mfaCompletedAt: ss.mfaCompletedAt,
128
+ mfaExpiresAt: ss.mfaExpiresAt,
129
+ twoFactorSessionVerified: ss.mfaVerified,
130
+ };
131
+ // Token fields for display
132
+ const displayExt = {
133
+ ...ext,
134
+ sessionToken: ss.sessionToken || ext?.sessionToken,
135
+ accessToken: ss.idpAccessToken || ext?.accessToken,
136
+ refreshToken: ss.idpRefreshToken || ext?.refreshToken,
137
+ accessTokenExpires: ss.idpAccessTokenExpires || ext?.accessTokenExpires,
138
+ };
103
139
 
104
140
  // Card styling helpers
105
141
  const cardClass = isDarkMode ? 'bg-slate-900 border border-slate-700' : 'bg-white border border-gray-200';
@@ -111,7 +147,7 @@ export function JwtInspectPage() {
111
147
  <div className="max-w-4xl mx-auto space-y-6">
112
148
  <h1 className="text-2xl font-bold">Session Inspector</h1>
113
149
  <p className={`text-sm ${labelClass}`}>
114
- Session data from Redis (via NextAuth session callback)
150
+ Better Auth session + IDP tokens from Redis
115
151
  </p>
116
152
 
117
153
  {/* User Identity */}
@@ -205,32 +241,32 @@ export function JwtInspectPage() {
205
241
  <div>
206
242
  <div className="flex items-center justify-between mb-2">
207
243
  <span className={labelClass}>Session Token (Redis Key):</span>
208
- {ext.sessionToken && (
244
+ {displayExt.sessionToken && (
209
245
  <CopyButton
210
- onClick={() => copyToClipboard(ext.sessionToken, 'session')}
246
+ onClick={() => copyToClipboard(displayExt.sessionToken, 'session')}
211
247
  copied={copied === 'session'}
212
248
  isDarkMode={isDarkMode}
213
249
  />
214
250
  )}
215
251
  </div>
216
252
  <code className={`block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
217
- {ext.sessionToken || 'N/A'}
253
+ {displayExt.sessionToken || 'N/A'}
218
254
  </code>
219
255
  </div>
220
256
 
221
257
  <div>
222
258
  <div className="flex items-center justify-between mb-2">
223
259
  <span className={labelClass}>Access Token (IDP):</span>
224
- {ext.accessToken && (
260
+ {displayExt.accessToken && (
225
261
  <CopyButton
226
- onClick={() => copyToClipboard(ext.accessToken, 'access')}
262
+ onClick={() => copyToClipboard(displayExt.accessToken, 'access')}
227
263
  copied={copied === 'access'}
228
264
  isDarkMode={isDarkMode}
229
265
  />
230
266
  )}
231
267
  </div>
232
268
  <code className={`block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
233
- {ext.accessToken || 'N/A'}
269
+ {displayExt.accessToken || 'N/A'}
234
270
  </code>
235
271
  </div>
236
272
 
@@ -238,7 +274,7 @@ export function JwtInspectPage() {
238
274
  <div>
239
275
  <span className={labelClass}>Has Refresh Token:</span>{' '}
240
276
  <StatusBadge
241
- value={!!ext.refreshToken}
277
+ value={!!displayExt.refreshToken}
242
278
  trueText="Yes"
243
279
  falseText="No"
244
280
  isDarkMode={isDarkMode}
@@ -246,7 +282,7 @@ export function JwtInspectPage() {
246
282
  </div>
247
283
  <InfoRow
248
284
  label="Access Token Expires"
249
- value={ext.accessTokenExpires ? new Date(ext.accessTokenExpires).toISOString() : undefined}
285
+ value={displayExt.accessTokenExpires ? new Date(displayExt.accessTokenExpires).toISOString() : undefined}
250
286
  labelClass={labelClass}
251
287
  valueClass={valueClass}
252
288
  />
@@ -338,7 +374,7 @@ export function JwtInspectPage() {
338
374
  <details className={`p-4 rounded-lg ${cardClass}`}>
339
375
  <summary className="font-semibold cursor-pointer">Raw Session Data (Click to expand)</summary>
340
376
  <pre className={`mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`}>
341
- {JSON.stringify(session, null, 2)}
377
+ {JSON.stringify({ clientSession: session, serverSession: serverSession }, null, 2)}
342
378
  </pre>
343
379
  </details>
344
380
  </div>
@@ -75,16 +75,30 @@ async function tryBetterAuthSession(
75
75
  return null;
76
76
  }
77
77
 
78
- // Map Better Auth session to SessionData
78
+ // Read IDP tokens from BA Redis session (stored by post-login hook)
79
+ let idpTokens: any = null;
80
+ try {
81
+ const { getRedis } = await import('../lib/redis');
82
+ const { getAppSlug } = await import('../lib/app-slug');
83
+ const baKey = `ba:${getAppSlug()}:${result.session.token}`;
84
+ const baRaw = await getRedis().get(baKey);
85
+ if (baRaw) {
86
+ const baData = JSON.parse(baRaw);
87
+ idpTokens = baData.idpTokens;
88
+ }
89
+ } catch { /* Redis unavailable */ }
90
+
91
+ // Map Better Auth session + IDP tokens to SessionData
79
92
  const sessionData: SessionData = {
80
- userId: result.user.id || '',
81
- email: result.user.email || '',
82
- name: result.user.name || undefined,
83
- roles: [],
84
- idpAccessTokenExpires: result.session.expiresAt
85
- ? new Date(result.session.expiresAt).getTime()
86
- : Date.now() + 24 * 60 * 60 * 1000,
87
- mfaVerified: true, // Social login doesn't require MFA
93
+ userId: idpTokens?.userId || result.user.id || '',
94
+ email: idpTokens?.email || result.user.email || '',
95
+ name: idpTokens?.name || result.user.name || undefined,
96
+ roles: idpTokens?.roles || [],
97
+ idpAccessToken: idpTokens?.idpAccessToken,
98
+ idpRefreshToken: idpTokens?.idpRefreshToken,
99
+ idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
100
+ || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
101
+ mfaVerified: true,
88
102
  oauthProvider: 'google',
89
103
  };
90
104