@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.
- package/dist/api-handlers/account/change-password.js +28 -6
- package/dist/auth/better-auth.d.ts +44 -14
- package/dist/auth/better-auth.js +92 -7
- package/dist/client/better-auth-client.d.ts +101 -956
- package/dist/config/env.d.ts +0 -2
- package/dist/config/env.js +0 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -1
- package/dist/lib/idp-client-config.js +8 -1
- package/dist/lib/never-in-response.d.ts +30 -0
- package/dist/lib/never-in-response.js +71 -0
- package/dist/lib/safe-error-response.d.ts +6 -6
- package/dist/lib/safe-error-response.js +6 -6
- package/dist/lib/two-factor-verify-failure.d.ts +36 -3
- package/dist/lib/two-factor-verify-failure.js +79 -2
- package/dist/middleware/create-middleware.d.ts +2 -0
- package/dist/middleware/create-middleware.js +36 -0
- package/dist/pages/login/page.js +5 -0
- package/dist/pages/verify-code/page.js +4 -2
- package/dist/routes/auth/settings.js +4 -1
- package/dist/server/auth.d.ts +36 -294
- package/dist/server/auth.js +22 -25
- package/dist/vibe/client.d.ts +132 -16
- package/dist/vibe/client.js +288 -82
- package/dist/vibe/document-unwrap.d.ts +50 -0
- package/dist/vibe/document-unwrap.js +78 -0
- package/dist/vibe/errors.d.ts +17 -0
- package/dist/vibe/errors.js +21 -1
- package/dist/vibe/generic.d.ts +2 -49
- package/dist/vibe/generic.js +17 -78
- package/dist/vibe/index.d.ts +1 -1
- package/dist/vibe/index.js +2 -1
- package/package.json +12 -2
- package/src/api-handlers/account/change-password.ts +31 -6
- package/src/auth/better-auth.ts +93 -8
- package/src/config/env.ts +0 -2
- package/src/index.ts +4 -0
- package/src/lib/idp-client-config.ts +8 -1
- package/src/lib/never-in-response.ts +64 -0
- package/src/lib/safe-error-response.ts +6 -6
- package/src/lib/two-factor-verify-failure.ts +86 -5
- package/src/middleware/create-middleware.ts +42 -0
- package/src/pages/login/page.tsx +4 -0
- package/src/pages/verify-code/page.tsx +4 -2
- package/src/routes/auth/settings.ts +5 -1
- package/src/server/auth.ts +23 -29
- package/src/vibe/client.ts +378 -91
- package/src/vibe/document-unwrap.ts +103 -0
- package/src/vibe/errors.ts +24 -0
- package/src/vibe/generic.ts +8 -100
- 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 (
|
|
82
|
-
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 (
|
|
85
|
-
errorMessage =
|
|
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:
|
|
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)(
|
|
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
|
-
}, ...(
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
},
|
|
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("
|
|
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
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -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
|
|
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
|
});
|