@payez/next-mvp 4.1.0 → 4.1.2

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.
@@ -41,12 +41,67 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.getAuthInstance = getAuthInstance;
43
43
  exports.getSession = getSession;
44
+ exports.getSessionData = getSessionData;
45
+ exports.getIdpToken = getIdpToken;
46
+ exports.getFreshIdpToken = getFreshIdpToken;
44
47
  exports.requireSession = requireSession;
45
48
  require("server-only");
46
49
  const better_auth_1 = require("../auth/better-auth");
47
50
  const idp_client_config_1 = require("../lib/idp-client-config");
51
+ const session_store_1 = require("../lib/session-store");
48
52
  let authInstance = null;
49
53
  let authInitPromise = null;
54
+ function buildSessionDataFromAuthSession(session) {
55
+ const user = session?.user;
56
+ if (!user?.id && !user?.email) {
57
+ return null;
58
+ }
59
+ const expiresAt = session?.session?.expiresAt
60
+ ? new Date(session.session.expiresAt).getTime()
61
+ : Date.now() + 24 * 60 * 60 * 1000;
62
+ return {
63
+ userId: user.userId || user.id || '',
64
+ email: user.email || '',
65
+ name: user.name || undefined,
66
+ image: user.image || undefined,
67
+ roles: Array.isArray(user.roles) ? user.roles : [],
68
+ idpAccessToken: user.idpAccessToken,
69
+ idpRefreshToken: user.idpRefreshToken,
70
+ idpAccessTokenExpires: user.idpAccessTokenExpires || expiresAt,
71
+ mfaVerified: user.mfaVerified ?? user.twoFactorSessionVerified ?? false,
72
+ oauthProvider: user.oauthProvider,
73
+ idpClientId: user.idpClientId,
74
+ merchantId: user.merchantId,
75
+ };
76
+ }
77
+ function attachSessionData(session, sessionData, sessionToken) {
78
+ if (!sessionData) {
79
+ return session;
80
+ }
81
+ const enrichedSessionData = {
82
+ ...sessionData,
83
+ ...(sessionToken ? { sessionToken } : {}),
84
+ };
85
+ session.sessionData = enrichedSessionData;
86
+ if (session?.user) {
87
+ const user = session.user;
88
+ user.userId = enrichedSessionData.userId || user.userId;
89
+ user.email = enrichedSessionData.email || user.email;
90
+ user.name = enrichedSessionData.name || user.name;
91
+ user.image = enrichedSessionData.image || user.image;
92
+ user.roles = enrichedSessionData.roles || user.roles || [];
93
+ user.idpAccessToken = enrichedSessionData.idpAccessToken;
94
+ user.idpRefreshToken = enrichedSessionData.idpRefreshToken;
95
+ user.idpAccessTokenExpires = enrichedSessionData.idpAccessTokenExpires;
96
+ user.mfaVerified = enrichedSessionData.mfaVerified;
97
+ user.twoFactorSessionVerified =
98
+ enrichedSessionData.mfaVerified ?? user.twoFactorSessionVerified;
99
+ user.oauthProvider = enrichedSessionData.oauthProvider || user.oauthProvider;
100
+ user.idpClientId = enrichedSessionData.idpClientId || user.idpClientId;
101
+ user.merchantId = enrichedSessionData.merchantId || user.merchantId;
102
+ }
103
+ return session;
104
+ }
50
105
  /**
51
106
  * Get the initialized Better Auth instance (singleton).
52
107
  */
@@ -75,31 +130,116 @@ async function getSession(request) {
75
130
  const session = await auth.api.getSession({ headers: request.headers });
76
131
  if (!session?.session?.token || !session?.user)
77
132
  return session;
78
- // Enrich with IDP tokens from Redis (stored by post-login hook)
133
+ const sessionToken = session.session.token;
134
+ let sessionData = null;
135
+ // Prefer the app's normalized Redis session. Fall back to Better Auth's
136
+ // secondary storage record, then finally to whatever Better Auth already
137
+ // put on the request session object.
79
138
  try {
80
- const { getRedis } = await Promise.resolve().then(() => __importStar(require('../lib/redis')));
81
- const { getAppSlug } = await Promise.resolve().then(() => __importStar(require('../lib/app-slug')));
82
- const baKey = `ba:${getAppSlug()}:${session.session.token}`;
83
- const baRaw = await getRedis().get(baKey);
84
- if (baRaw) {
85
- const baData = JSON.parse(baRaw);
86
- if (baData.idpTokens) {
87
- const u = session.user;
88
- u.roles = baData.idpTokens.roles || [];
89
- u.userId = baData.idpTokens.userId;
90
- u.idpAccessToken = baData.idpTokens.idpAccessToken;
91
- u.idpRefreshToken = baData.idpTokens.idpRefreshToken;
92
- u.idpAccessTokenExpires = baData.idpTokens.idpAccessTokenExpires;
93
- }
139
+ sessionData = await (0, session_store_1.getSession)(sessionToken);
140
+ if (!sessionData) {
141
+ sessionData = await (0, session_store_1.getBetterAuthSession)(sessionToken);
94
142
  }
95
143
  }
96
144
  catch { /* Redis unavailable */ }
97
- return session;
145
+ if (!sessionData) {
146
+ sessionData = buildSessionDataFromAuthSession(session);
147
+ }
148
+ return attachSessionData(session, sessionData, sessionToken);
98
149
  }
99
150
  catch {
100
151
  return null;
101
152
  }
102
153
  }
154
+ /**
155
+ * Get normalized session data for the current request.
156
+ *
157
+ * This prefers the app's Redis session because it carries the canonical
158
+ * IDP token, roles, and tenant-specific user identity used by app routes.
159
+ */
160
+ async function getSessionData(request) {
161
+ const session = await getSession(request);
162
+ const sessionData = session?.sessionData ||
163
+ buildSessionDataFromAuthSession(session);
164
+ if (!sessionData) {
165
+ return null;
166
+ }
167
+ const sessionToken = session?.session?.token;
168
+ return sessionToken
169
+ ? { ...sessionData, sessionToken }
170
+ : sessionData;
171
+ }
172
+ /**
173
+ * Get the current request's IDP access token without triggering a refresh.
174
+ *
175
+ * Use this for routes that only need the currently-issued bearer token and
176
+ * should fail closed instead of performing token lifecycle work. For backend
177
+ * proxy routes that forward the token to a downstream API, prefer
178
+ * `getFreshIdpToken` — it preflights expiry and refreshes single-flight, so
179
+ * the proxy never sends a credential it already knows is invalid.
180
+ */
181
+ async function getIdpToken(request) {
182
+ const sessionData = await getSessionData(request);
183
+ if (!sessionData) {
184
+ return { success: false, error: 'NO_SESSION', terminal: true };
185
+ }
186
+ const accessToken = sessionData.idpAccessToken || sessionData.accessToken;
187
+ if (!accessToken) {
188
+ return { success: false, error: 'NO_TOKEN', terminal: true };
189
+ }
190
+ return {
191
+ success: true,
192
+ accessToken,
193
+ sessionData,
194
+ };
195
+ }
196
+ /**
197
+ * Get the current request's IDP access token, preflight-refreshing if it is
198
+ * expired or within the safety window. Single-flight via Redis lock, so
199
+ * concurrent calls on the same session share one IDP round-trip and one
200
+ * single-use refresh-token consumption.
201
+ *
202
+ * Use this in proxy routes. The returned `accessToken` is safe to forward to
203
+ * a downstream API without expecting a 401. If `success` is false, surface a
204
+ * 401/redirect — there is no recoverable token for this session.
205
+ */
206
+ async function getFreshIdpToken(request, config) {
207
+ const session = await getSession(request);
208
+ const sessionToken = session?.session?.token;
209
+ if (!sessionToken) {
210
+ return { success: false, error: 'NO_SESSION', status: 401, terminal: true };
211
+ }
212
+ const { ensureFreshAccessToken } = await Promise.resolve().then(() => __importStar(require('../lib/ensure-fresh-access-token')));
213
+ const result = await ensureFreshAccessToken(sessionToken, {
214
+ idpBaseUrl: config.idpBaseUrl,
215
+ clientId: config.clientId,
216
+ refreshEndpoint: config.refreshEndpoint,
217
+ }, {
218
+ safetyWindowMs: config.safetyWindowMs,
219
+ requestId: request?.headers?.get('x-request-id') ?? undefined,
220
+ });
221
+ if (!result.ok) {
222
+ return {
223
+ success: false,
224
+ error: result.code,
225
+ status: result.status,
226
+ terminal: result.terminal,
227
+ discardToken: result.discardToken,
228
+ retryable: result.retryable,
229
+ resolution: result.resolution,
230
+ };
231
+ }
232
+ const sessionData = await (0, session_store_1.getSession)(sessionToken);
233
+ if (!sessionData) {
234
+ return { success: false, error: 'NO_SESSION', status: 401, terminal: true };
235
+ }
236
+ return {
237
+ success: true,
238
+ accessToken: result.accessToken,
239
+ sessionData,
240
+ refreshed: result.refreshed,
241
+ };
242
+ }
103
243
  /**
104
244
  * Get the current session, throwing if not authenticated.
105
245
  * Use in API handlers that require auth.
@@ -116,6 +116,8 @@ async function tryBetterAuthSession(requestCookies) {
116
116
  || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
117
117
  mfaVerified: idpTokens?.mfaVerified ?? false,
118
118
  oauthProvider: 'google',
119
+ idpClientId: idpTokens?.idpClientId ?? idpTokens?.clientId,
120
+ merchantId: idpTokens?.merchantId,
119
121
  };
120
122
  // Backwards compat: session.user.email works alongside session.email
121
123
  sessionData.user = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -112,6 +112,11 @@
112
112
  "require": "./dist/lib/session-store.js",
113
113
  "default": "./dist/lib/session-store.js"
114
114
  },
115
+ "./lib/ensure-fresh-access-token": {
116
+ "types": "./dist/lib/ensure-fresh-access-token.d.ts",
117
+ "require": "./dist/lib/ensure-fresh-access-token.js",
118
+ "default": "./dist/lib/ensure-fresh-access-token.js"
119
+ },
115
120
  "./lib/token-lifecycle": {
116
121
  "types": "./dist/lib/token-lifecycle.d.ts",
117
122
  "require": "./dist/lib/token-lifecycle.js",