@payez/next-mvp 4.4.6 → 4.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/api-handlers/account/change-password.js +28 -6
  2. package/dist/auth/better-auth.d.ts +44 -14
  3. package/dist/auth/better-auth.js +92 -7
  4. package/dist/client/better-auth-client.d.ts +101 -956
  5. package/dist/config/env.d.ts +0 -2
  6. package/dist/config/env.js +0 -2
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +8 -1
  9. package/dist/lib/idp-client-config.js +8 -1
  10. package/dist/lib/never-in-response.d.ts +30 -0
  11. package/dist/lib/never-in-response.js +71 -0
  12. package/dist/lib/safe-error-response.d.ts +6 -6
  13. package/dist/lib/safe-error-response.js +6 -6
  14. package/dist/lib/two-factor-verify-failure.d.ts +36 -3
  15. package/dist/lib/two-factor-verify-failure.js +79 -2
  16. package/dist/middleware/create-middleware.d.ts +2 -0
  17. package/dist/middleware/create-middleware.js +36 -0
  18. package/dist/pages/login/page.js +5 -0
  19. package/dist/pages/verify-code/page.js +4 -2
  20. package/dist/routes/auth/settings.js +4 -1
  21. package/dist/server/auth.d.ts +36 -294
  22. package/dist/server/auth.js +22 -25
  23. package/dist/vibe/client.d.ts +132 -16
  24. package/dist/vibe/client.js +288 -82
  25. package/dist/vibe/document-unwrap.d.ts +50 -0
  26. package/dist/vibe/document-unwrap.js +78 -0
  27. package/dist/vibe/errors.d.ts +17 -0
  28. package/dist/vibe/errors.js +21 -1
  29. package/dist/vibe/generic.d.ts +2 -49
  30. package/dist/vibe/generic.js +17 -78
  31. package/dist/vibe/index.d.ts +1 -1
  32. package/dist/vibe/index.js +2 -1
  33. package/package.json +12 -2
  34. package/src/api-handlers/account/change-password.ts +31 -6
  35. package/src/auth/better-auth.ts +93 -8
  36. package/src/config/env.ts +0 -2
  37. package/src/index.ts +4 -0
  38. package/src/lib/idp-client-config.ts +8 -1
  39. package/src/lib/never-in-response.ts +64 -0
  40. package/src/lib/safe-error-response.ts +6 -6
  41. package/src/lib/two-factor-verify-failure.ts +86 -5
  42. package/src/middleware/create-middleware.ts +42 -0
  43. package/src/pages/login/page.tsx +4 -0
  44. package/src/pages/verify-code/page.tsx +4 -2
  45. package/src/routes/auth/settings.ts +5 -1
  46. package/src/server/auth.ts +23 -29
  47. package/src/vibe/client.ts +378 -91
  48. package/src/vibe/document-unwrap.ts +103 -0
  49. package/src/vibe/errors.ts +24 -0
  50. package/src/vibe/generic.ts +8 -100
  51. package/src/vibe/index.ts +1 -0
@@ -5,6 +5,25 @@ const server_1 = require("next/server");
5
5
  const auth_1 = require("../../server/auth");
6
6
  const session_store_1 = require("../../lib/session-store");
7
7
  const nanoid_1 = require("nanoid");
8
+ /**
9
+ * PAY-1596: the failure code comes from the IdP's REAL envelope { success, error: { code, message, details } } - the handler
10
+ * used to read a top-level `error_code` the IdP never sends, so every refusal was CHANGE_PASSWORD_FAILED and "this account has
11
+ * no password" / "wrong current password" could not be told apart. Allowlisted: nothing but these codes ever leaves here.
12
+ * - error.code NO_PASSWORD_SET / INVALID_CURRENT_PASSWORD (IdP PR 412) / VALIDATION_ERROR
13
+ * - the pre-412 wrong-current shape: VALIDATION_ERROR + details.value[] { code: InvalidCurrentPassword, field_name: currentPassword }
14
+ * - a legacy top-level error_code, for older IdPs
15
+ */
16
+ const CHANGE_PASSWORD_CODES = new Set(['NO_PASSWORD_SET', 'INVALID_CURRENT_PASSWORD', 'VALIDATION_ERROR']);
17
+ function changePasswordFailureCode(body) {
18
+ const err = body && typeof body.error === 'object' && body.error ? body.error : null;
19
+ const code = typeof err?.code === 'string' ? err.code : typeof body?.error_code === 'string' ? body.error_code : '';
20
+ if (code === 'VALIDATION_ERROR' && Array.isArray(err?.details?.value)) {
21
+ const wrongCurrent = err.details.value.some((e) => e && typeof e === 'object' && (String(e.field_name ?? e.fieldName ?? '').toLowerCase() === 'currentpassword' || String(e.code ?? '').toLowerCase() === 'invalidcurrentpassword'));
22
+ if (wrongCurrent)
23
+ return 'INVALID_CURRENT_PASSWORD';
24
+ }
25
+ return CHANGE_PASSWORD_CODES.has(code) ? code : 'CHANGE_PASSWORD_FAILED';
26
+ }
8
27
  const safe_error_response_1 = require("../../lib/safe-error-response");
9
28
  // ...
10
29
  async function POST(req) {
@@ -75,22 +94,25 @@ async function POST(req) {
75
94
  if (!idpResponse.ok) {
76
95
  // Extract error message from IDP response
77
96
  let errorMessage = 'Failed to change password';
97
+ const failureCode = changePasswordFailureCode(responseData);
98
+ // The real envelope nests details under `error`; older IdPs put them at the top level.
99
+ const idpDetails = (responseData.error && typeof responseData.error === 'object' ? responseData.error.details : undefined) ?? responseData.details;
78
100
  if (responseData.message) {
79
101
  errorMessage = responseData.message;
80
102
  }
81
- else if (responseData.details?.value && Array.isArray(responseData.details.value) && responseData.details.value.length > 0) {
82
- errorMessage = responseData.details.value[0].message || errorMessage;
103
+ else if (idpDetails?.value && Array.isArray(idpDetails.value) && idpDetails.value.length > 0) {
104
+ errorMessage = idpDetails.value[0].message || errorMessage;
83
105
  }
84
- else if (responseData.details?.message) {
85
- errorMessage = responseData.details.message;
106
+ else if (idpDetails?.message) {
107
+ errorMessage = idpDetails.message;
86
108
  }
87
109
  return server_1.NextResponse.json({
88
110
  success: false,
89
111
  message: errorMessage,
90
- error_code: responseData.error_code || 'CHANGE_PASSWORD_FAILED',
112
+ error_code: failureCode,
91
113
  request_id: requestId,
92
114
  // PAY-1438: only the password-policy messages the security pages display, never the raw envelope.
93
- details: (0, safe_error_response_1.projectPasswordPolicyDetails)(responseData.details),
115
+ details: (0, safe_error_response_1.projectPasswordPolicyDetails)(idpDetails),
94
116
  }, { status: idpResponse.status });
95
117
  }
96
118
  return server_1.NextResponse.json({
@@ -182,6 +182,9 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
182
182
  refreshCache: false;
183
183
  };
184
184
  };
185
+ account: {
186
+ storeStateStrategy: "cookie";
187
+ };
185
188
  advanced: {
186
189
  cookiePrefix: string;
187
190
  cookies: {
@@ -192,22 +195,25 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
192
195
  };
193
196
  plugins: [{
194
197
  id: "next-cookies";
198
+ version: string;
195
199
  hooks: {
196
200
  before: {
197
201
  matcher(ctx: import("better-auth").HookEndpointContext): boolean;
198
- handler: (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>;
202
+ handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
199
203
  }[];
200
204
  after: {
201
205
  matcher(ctx: import("better-auth").HookEndpointContext): true;
202
- handler: (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>;
206
+ handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
203
207
  }[];
204
208
  };
205
- }, ...(import("better-auth").BetterAuthPlugin | {
209
+ }, ...({
206
210
  id: "magic-link";
211
+ version: string;
207
212
  endpoints: {
208
213
  signInMagicLink: import("better-auth").StrictEndpoint<"/sign-in/magic-link", {
209
214
  method: "POST";
210
215
  requireHeaders: true;
216
+ use: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>[];
211
217
  body: import("better-auth").ZodObject<{
212
218
  email: import("better-auth").ZodEmail;
213
219
  name: import("better-auth").ZodOptional<import("better-auth").ZodString>;
@@ -215,7 +221,7 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
215
221
  newUserCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
216
222
  errorCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
217
223
  metadata: import("better-auth").ZodOptional<import("better-auth").ZodRecord<import("better-auth").ZodString, import("better-auth").ZodAny>>;
218
- }, import("better-auth").$strip>;
224
+ }, import("zod/v4/core").$strip>;
219
225
  metadata: {
220
226
  openapi: {
221
227
  operationId: string;
@@ -249,8 +255,8 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
249
255
  callbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
250
256
  errorCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
251
257
  newUserCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
252
- }, import("better-auth").$strip>;
253
- use: ((inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>)[];
258
+ }, import("zod/v4/core").$strip>;
259
+ use: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>[];
254
260
  requireHeaders: true;
255
261
  metadata: {
256
262
  openapi: {
@@ -289,6 +295,16 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
289
295
  name: string;
290
296
  image?: string | null | undefined;
291
297
  };
298
+ session: {
299
+ id: string;
300
+ createdAt: Date;
301
+ updatedAt: Date;
302
+ userId: string;
303
+ expiresAt: Date;
304
+ token: string;
305
+ ipAddress?: string | null | undefined;
306
+ userAgent?: string | null | undefined;
307
+ };
292
308
  }>;
293
309
  };
294
310
  rateLimit: {
@@ -297,8 +313,9 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
297
313
  max: number;
298
314
  }[];
299
315
  options: MagicLinkOptions;
300
- } | {
316
+ } | import("better-auth").BetterAuthPlugin | {
301
317
  id: "generic-oauth";
318
+ version: string;
302
319
  init: (ctx: import("better-auth").AuthContext) => {
303
320
  context: {
304
321
  socialProviders: import("better-auth").OAuthProvider<Record<string, any>, Partial<import("better-auth").ProviderOptions<any>>>[];
@@ -316,7 +333,7 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
316
333
  scopes: import("better-auth").ZodOptional<import("better-auth").ZodArray<import("better-auth").ZodString>>;
317
334
  requestSignUp: import("better-auth").ZodOptional<import("better-auth").ZodBoolean>;
318
335
  additionalData: import("better-auth").ZodOptional<import("better-auth").ZodRecord<import("better-auth").ZodString, import("better-auth").ZodAny>>;
319
- }, import("better-auth").$strip>;
336
+ }, import("zod/v4/core").$strip>;
320
337
  metadata: {
321
338
  openapi: {
322
339
  description: string;
@@ -354,7 +371,7 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
354
371
  error_description: import("better-auth").ZodOptional<import("better-auth").ZodString>;
355
372
  state: import("better-auth").ZodOptional<import("better-auth").ZodString>;
356
373
  iss: import("better-auth").ZodOptional<import("better-auth").ZodString>;
357
- }, import("better-auth").$strip>;
374
+ }, import("zod/v4/core").$strip>;
358
375
  metadata: {
359
376
  allowedMediaTypes: string[];
360
377
  openapi: {
@@ -379,7 +396,7 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
379
396
  };
380
397
  scope: "server";
381
398
  };
382
- }, void>;
399
+ }, never>;
383
400
  oAuth2LinkAccount: import("better-auth").StrictEndpoint<"/oauth2/link", {
384
401
  method: "POST";
385
402
  body: import("better-auth").ZodObject<{
@@ -387,8 +404,8 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
387
404
  callbackURL: import("better-auth").ZodString;
388
405
  scopes: import("better-auth").ZodOptional<import("better-auth").ZodArray<import("better-auth").ZodString>>;
389
406
  errorCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
390
- }, import("better-auth").$strip>;
391
- use: ((inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
407
+ }, import("zod/v4/core").$strip>;
408
+ use: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
392
409
  session: {
393
410
  session: Record<string, any> & {
394
411
  id: string;
@@ -410,7 +427,7 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opt
410
427
  image?: string | null | undefined;
411
428
  };
412
429
  };
413
- }>)[];
430
+ }>>[];
414
431
  metadata: {
415
432
  openapi: {
416
433
  description: string;
@@ -565,12 +582,25 @@ export declare function exchangeOAuthForIdpTokens(sessionToken: string, provider
565
582
  * state what actually authenticated ('passkey', 'google').
566
583
  */
567
584
  export declare function storeIdpTokensInSession(sessionToken: string, idpTokenData: Record<string, unknown>, authProvider: string): Promise<boolean>;
585
+ /**
586
+ * The `?error=` code the login page receives when an OAuth sign-in is ABORTED because the IdP exchange did not
587
+ * store tokens (PAY-1733). Hosts map it to ONE neutral line; the card's words are "That sign-in didn't work.
588
+ * Try again or use another method." The cause stays in the server log, never on the page.
589
+ */
590
+ export declare const OAUTH_SIGN_IN_FAILED_ERROR = "OAUTH_SIGN_IN_FAILED";
591
+ /**
592
+ * Re-emit each cookie the callback response SET as an immediate expiry, keeping the attributes that decide
593
+ * WHICH cookie the browser overwrites (Path, Domain, Secure, SameSite, HttpOnly - a `__Secure-` name is only
594
+ * accepted with Secure) and dropping Expires/Max-Age in favour of Max-Age=0.
595
+ */
596
+ export declare function expireSetCookies(setCookies: readonly string[]): string[];
568
597
  /**
569
598
  * Create a production-ready GET handler for the auth catch-all route.
570
599
  *
571
600
  * Wraps better-auth's GET handler with:
572
601
  * - OAuth state error recovery (redirects to login instead of error page)
573
- * - IDP token exchange after successful OAuth callback
602
+ * - IDP token exchange after successful OAuth callback - and, when it stores no tokens, the sign-in is ABORTED
603
+ * (session destroyed, cookies expired, login page with OAUTH_SIGN_IN_FAILED; PAY-1733)
574
604
  *
575
605
  * Usage in host app:
576
606
  * ```ts
@@ -43,6 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  };
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.OAUTH_SIGN_IN_FAILED_ERROR = void 0;
46
47
  exports.secretFingerprint = secretFingerprint;
47
48
  exports.buildIdpOAuthProviderConfig = buildIdpOAuthProviderConfig;
48
49
  exports.buildBetterAuthProviders = buildBetterAuthProviders;
@@ -54,6 +55,7 @@ exports.getBetterAuthInstance = getBetterAuthInstance;
54
55
  exports.getBetterAuthHandler = getBetterAuthHandler;
55
56
  exports.exchangeOAuthForIdpTokens = exchangeOAuthForIdpTokens;
56
57
  exports.storeIdpTokensInSession = storeIdpTokensInSession;
58
+ exports.expireSetCookies = expireSetCookies;
57
59
  exports.createAuthGetHandler = createAuthGetHandler;
58
60
  require("server-only");
59
61
  const node_async_hooks_1 = require("node:async_hooks");
@@ -748,6 +750,18 @@ function createBetterAuthInstance(idpConfig, opts = {}) {
748
750
  refreshCache: false,
749
751
  },
750
752
  },
753
+ // PAY-1781 B: KEEP OAUTH STATE IN THE ENCRYPTED COOKIE, as it was on better-auth 1.5.6.
754
+ // 1.5.6 resolved the default as `options.database ? 'database' : 'cookie'` - we have no
755
+ // database, so: cookie. 1.6.13+ resolves it as `(database || secondaryStorage) ? 'database'
756
+ // : 'cookie'`, and we DO have secondaryStorage, so the upgrade alone would silently move every
757
+ // OAuth state record into Redis through the adapter above (its enrichment read-modify-write
758
+ // and key handling were built for session records, not state). Pinned so the upgrade changes
759
+ // the version, not the design. The cookie branch is where GHSA-wxw3-q3m9-c3jr lived; 1.6.2+
760
+ // compares the returned `state` with the nonce sealed in the cookie, and our provider is also
761
+ // pkce:true, which the advisory required to be false. Test: oauth-state-strategy-cookie.
762
+ account: {
763
+ storeStateStrategy: 'cookie',
764
+ },
751
765
  // Cookie prefix must match slim-middleware expectations ({slug}.session-token)
752
766
  advanced: {
753
767
  cookiePrefix: appSlug,
@@ -1202,12 +1216,76 @@ async function storeIdpTokensInSession(sessionToken, idpTokenData, authProvider)
1202
1216
  return false;
1203
1217
  }
1204
1218
  }
1219
+ /**
1220
+ * The `?error=` code the login page receives when an OAuth sign-in is ABORTED because the IdP exchange did not
1221
+ * store tokens (PAY-1733). Hosts map it to ONE neutral line; the card's words are "That sign-in didn't work.
1222
+ * Try again or use another method." The cause stays in the server log, never on the page.
1223
+ */
1224
+ exports.OAUTH_SIGN_IN_FAILED_ERROR = 'OAUTH_SIGN_IN_FAILED';
1225
+ /**
1226
+ * Re-emit each cookie the callback response SET as an immediate expiry, keeping the attributes that decide
1227
+ * WHICH cookie the browser overwrites (Path, Domain, Secure, SameSite, HttpOnly - a `__Secure-` name is only
1228
+ * accepted with Secure) and dropping Expires/Max-Age in favour of Max-Age=0.
1229
+ */
1230
+ function expireSetCookies(setCookies) {
1231
+ const out = [];
1232
+ for (const sc of setCookies) {
1233
+ const [pair, ...attrs] = sc.split(';').map((s) => s.trim());
1234
+ const eq = pair.indexOf('=');
1235
+ const name = eq > 0 ? pair.slice(0, eq) : '';
1236
+ if (!name)
1237
+ continue;
1238
+ const kept = attrs.filter((a) => /^(path|domain|secure|samesite|httponly)\b/i.test(a));
1239
+ out.push([`${name}=`, ...kept, 'Max-Age=0'].join('; '));
1240
+ }
1241
+ return out;
1242
+ }
1243
+ /**
1244
+ * PAY-1733 (Jon, live on :4797): ABORT an OAuth sign-in whose IdP exchange did not store tokens.
1245
+ *
1246
+ * Before this, a failed exchange was a console.error and the user kept the Better Auth session the callback had
1247
+ * just created: signed in, holding NO IdP tokens. The host then routed that session to 2FA, which painted the
1248
+ * user's email and failed every call - a red "sign in again" screen about an account the app could no longer
1249
+ * authenticate. The IdP's 401 was CORRECT (it fail-closes unverified assertions); keeping the session was the bug.
1250
+ *
1251
+ * So: no session without IdP tokens. The Better Auth session is destroyed (its own signOut, best effort, then the
1252
+ * `ba:<slug>:<token>` record directly - that record is what makes it resolvable), every cookie the callback set is
1253
+ * expired, and the user goes to the login page with OAUTH_SIGN_IN_FAILED. Relative Location, as for the state-error
1254
+ * branch above (328439: an absolute URL built from request.url is a dead tab in a container).
1255
+ */
1256
+ async function abortOAuthSignIn(callbackResponse, sessionToken, cookieHeader, loginPath) {
1257
+ const headers = new Headers();
1258
+ if (sessionToken) {
1259
+ try {
1260
+ const auth = await getBetterAuthInstance();
1261
+ const out = await auth?.api?.signOut?.({ headers: new Headers({ cookie: cookieHeader }), asResponse: true });
1262
+ for (const sc of out?.headers?.getSetCookie?.() ?? [])
1263
+ headers.append('set-cookie', sc);
1264
+ }
1265
+ catch (err) {
1266
+ console.warn('[BETTER_AUTH] abort: Better Auth signOut failed, deleting the record directly:', err instanceof Error ? err.message : String(err));
1267
+ }
1268
+ try {
1269
+ const config = await (0, idp_client_config_1.getIDPClientConfig)();
1270
+ await (0, redis_1.getRedis)().del(`ba:${config.clientSlug || (0, app_slug_1.getAppSlug)()}:${sessionToken}`);
1271
+ }
1272
+ catch (err) {
1273
+ console.error('[BETTER_AUTH] abort: could not delete the Better Auth session record', (0, token_log_id_1.tokenLogId)(sessionToken), err instanceof Error ? err.message : String(err));
1274
+ }
1275
+ }
1276
+ for (const sc of expireSetCookies(callbackResponse.headers.getSetCookie?.() ?? []))
1277
+ headers.append('set-cookie', sc);
1278
+ headers.set('location', `${loginPath}${loginPath.includes('?') ? '&' : '?'}error=${exports.OAUTH_SIGN_IN_FAILED_ERROR}`);
1279
+ console.warn('[BETTER_AUTH] OAuth sign-in ABORTED: the IdP exchange stored no tokens, so no session is kept (PAY-1733)', sessionToken ? (0, token_log_id_1.tokenLogId)(sessionToken) : '(no session)');
1280
+ return new Response(null, { status: 302, headers });
1281
+ }
1205
1282
  /**
1206
1283
  * Create a production-ready GET handler for the auth catch-all route.
1207
1284
  *
1208
1285
  * Wraps better-auth's GET handler with:
1209
1286
  * - OAuth state error recovery (redirects to login instead of error page)
1210
- * - IDP token exchange after successful OAuth callback
1287
+ * - IDP token exchange after successful OAuth callback - and, when it stores no tokens, the sign-in is ABORTED
1288
+ * (session destroyed, cookies expired, login page with OAUTH_SIGN_IN_FAILED; PAY-1733)
1211
1289
  *
1212
1290
  * Usage in host app:
1213
1291
  * ```ts
@@ -1276,22 +1354,27 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
1276
1354
  return new Response(null, { status: 302, headers: { location: loginPath } });
1277
1355
  }
1278
1356
  }
1279
- // After successful OAuth callback: exchange Google identity for IDP tokens
1357
+ // After the OAuth callback: exchange the provider identity for IDP tokens. If that does NOT store tokens - the
1358
+ // IdP refused (401), no session was found, or anything threw - the sign-in is ABORTED (PAY-1733). A session is
1359
+ // kept only when it holds IdP tokens.
1280
1360
  const url = new URL(request.url);
1281
1361
  if (url.pathname.includes('/callback/') && response.status === 302) {
1362
+ const setCookies = response.headers.getSetCookie?.() || [];
1363
+ const cookieHeader = setCookies
1364
+ .map((c) => c.split(';')[0])
1365
+ .join('; ');
1366
+ let sessionToken = null;
1367
+ let exchanged = false;
1282
1368
  try {
1283
1369
  const auth = await getBetterAuthInstance();
1284
1370
  if (auth?.api?.getSession) {
1285
- const setCookies = response.headers.getSetCookie?.() || [];
1286
- const cookieHeader = setCookies
1287
- .map((c) => c.split(';')[0])
1288
- .join('; ');
1289
1371
  const headers = new Headers();
1290
1372
  headers.set('cookie', cookieHeader);
1291
1373
  const session = await auth.api.getSession({ headers });
1292
1374
  if (session?.session?.token) {
1375
+ sessionToken = session.session.token;
1293
1376
  console.log('[BETTER_AUTH] Got session token from callback:', (0, token_log_id_1.tokenLogId)(session.session.token), '| email:', session.user?.email);
1294
- await exchangeOAuthForIdpTokens(session.session.token);
1377
+ exchanged = await exchangeOAuthForIdpTokens(session.session.token);
1295
1378
  }
1296
1379
  else {
1297
1380
  console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
@@ -1301,6 +1384,8 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
1301
1384
  catch (err) {
1302
1385
  console.error('[BETTER_AUTH] IDP token exchange failed:', err.message);
1303
1386
  }
1387
+ if (!exchanged)
1388
+ return abortOAuthSignIn(response, sessionToken, cookieHeader, loginPath);
1304
1389
  }
1305
1390
  return response;
1306
1391
  });