@akinon/next 2.0.102-beta.0 → 2.0.102
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/CHANGELOG.md +4 -2
- package/api/auth.ts +116 -90
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# @akinon/next
|
|
2
2
|
|
|
3
|
-
## 2.0.102
|
|
3
|
+
## 2.0.102
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- 52a5928: ZERO-5041: Keep the anonymous session on login so the guest basket merges
|
|
8
|
+
|
|
9
|
+
The login flow cleared the anonymous osessionid whenever getCurrentUser returned no pk, but the commerce currentUser endpoint returns 401 for a valid anonymous session too, so every guest's session (and its basket) was discarded before the login request reached the backend. Forward the anonymous session instead, and recover from a genuinely stale cookie by retrying the login once without it (ZERO-4247), scoped to password login and skipping OTP challenges (ZERO-4550) and throttled responses.
|
|
8
10
|
|
|
9
11
|
## 2.0.101
|
|
10
12
|
|
package/api/auth.ts
CHANGED
|
@@ -203,60 +203,73 @@ const getDefaultAuthConfig = () => {
|
|
|
203
203
|
if (sessionCookie) {
|
|
204
204
|
reqHeaders.set('cookie', sessionCookie);
|
|
205
205
|
}
|
|
206
|
-
} else if (credentials.formType === 'login') {
|
|
207
|
-
// Stale session cookie — only clear it before a fresh password
|
|
208
|
-
// login (ZERO-4247). Register and OTP flows are still anonymous at
|
|
209
|
-
// this point (no pk yet), but their session carries the pending OTP
|
|
210
|
-
// challenge; clearing it here makes the backend lose the challenge
|
|
211
|
-
// and re-issue the code on every verify (ZERO-4550).
|
|
212
|
-
// remove from headers and clear in browser
|
|
213
|
-
const currentCookies = reqHeaders.get('cookie') || '';
|
|
214
|
-
const cleanedCookies = currentCookies
|
|
215
|
-
.split(';')
|
|
216
|
-
.filter((c) => !c.trim().startsWith('osessionid='))
|
|
217
|
-
.join(';')
|
|
218
|
-
.trim();
|
|
219
|
-
reqHeaders.set('cookie', cleanedCookies);
|
|
220
|
-
|
|
221
|
-
const { localeUrlStrategy } = Settings.localization;
|
|
222
|
-
const fallbackHost =
|
|
223
|
-
headerStore.get('x-forwarded-host') ||
|
|
224
|
-
headerStore.get('host');
|
|
225
|
-
const hostname =
|
|
226
|
-
process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
|
|
227
|
-
const rootHostname =
|
|
228
|
-
localeUrlStrategy === LocaleUrlStrategy.Subdomain
|
|
229
|
-
? getRootHostname(hostname)
|
|
230
|
-
: null;
|
|
231
|
-
const expireOptions = {
|
|
232
|
-
path: '/',
|
|
233
|
-
maxAge: 0,
|
|
234
|
-
...(rootHostname ? { domain: rootHostname } : {})
|
|
235
|
-
};
|
|
236
|
-
cookieStore.set('osessionid', '', expireOptions);
|
|
237
|
-
cookieStore.set('sessionid', '', expireOptions);
|
|
238
206
|
}
|
|
207
|
+
// A guest's anonymous session (no pk yet) is deliberately NOT
|
|
208
|
+
// stripped here. It must be forwarded on the login request so the
|
|
209
|
+
// backend can merge the guest basket into the account on sign-in
|
|
210
|
+
// (ZERO-5041). getCurrentUser cannot tell a valid anonymous session
|
|
211
|
+
// apart from a truly stale one — the commerce currentUser endpoint
|
|
212
|
+
// returns 401 for both — so pre-emptively clearing on "no pk" also
|
|
213
|
+
// discarded every legitimate guest session. The ZERO-4247
|
|
214
|
+
// stale-cookie recovery is handled below instead, as a one-time
|
|
215
|
+
// retry without the session cookie after a login actually fails.
|
|
239
216
|
}
|
|
240
217
|
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
218
|
+
const performAuthRequest = async (headers: HeadersInit) => {
|
|
219
|
+
const request = await fetch(
|
|
220
|
+
`${Settings.commerceUrl}${user[credentials.formType]}`,
|
|
221
|
+
{
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers,
|
|
224
|
+
body: JSON.stringify(credentials)
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const body = (await request.json()) as {
|
|
229
|
+
key: string;
|
|
230
|
+
non_field_errors: string[];
|
|
231
|
+
redirect_url: string;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return { request, body };
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
let { request: apiRequest, body: response } =
|
|
238
|
+
await performAuthRequest(reqHeaders);
|
|
239
|
+
|
|
240
|
+
// ZERO-4247 recovery: the anonymous session was forwarded so the guest
|
|
241
|
+
// basket can merge (ZERO-5041). If a session cookie was present and the
|
|
242
|
+
// login still failed, the session may be genuinely stale, so retry once
|
|
243
|
+
// without it. Scoped to password login; OTP challenges (202,
|
|
244
|
+
// ZERO-4550) and throttled requests (429) are left untouched. On a
|
|
245
|
+
// successful retry the sessionId block below overwrites the browser
|
|
246
|
+
// cookies with the freshly issued session.
|
|
247
|
+
if (
|
|
248
|
+
!response.key &&
|
|
249
|
+
credentials.formType === 'login' &&
|
|
250
|
+
existingSessionId &&
|
|
251
|
+
apiRequest.status !== 202 &&
|
|
252
|
+
apiRequest.status !== 429
|
|
253
|
+
) {
|
|
254
|
+
const retryHeaders = new Headers(reqHeaders);
|
|
255
|
+
const cleanedCookies = (retryHeaders.get('cookie') || '')
|
|
256
|
+
.split(';')
|
|
257
|
+
.filter((c) => !c.trim().startsWith('osessionid='))
|
|
258
|
+
.join(';')
|
|
259
|
+
.trim();
|
|
260
|
+
retryHeaders.set('cookie', cleanedCookies);
|
|
261
|
+
|
|
262
|
+
const retry = await performAuthRequest(retryHeaders);
|
|
263
|
+
if (retry.body.key) {
|
|
264
|
+
apiRequest = retry.request;
|
|
265
|
+
response = retry.body;
|
|
247
266
|
}
|
|
248
|
-
|
|
267
|
+
}
|
|
249
268
|
|
|
250
269
|
logger.info(`Login/Register request result: ${apiRequest.status}`, {
|
|
251
270
|
userIp
|
|
252
271
|
});
|
|
253
272
|
|
|
254
|
-
const response = (await apiRequest.json()) as {
|
|
255
|
-
key: string;
|
|
256
|
-
non_field_errors: string[];
|
|
257
|
-
redirect_url: string;
|
|
258
|
-
};
|
|
259
|
-
|
|
260
273
|
logger.debug(`Login/Register response: ${JSON.stringify(response)}`);
|
|
261
274
|
|
|
262
275
|
let sessionId = '';
|
|
@@ -496,60 +509,73 @@ const defaultNextAuthOptionsV4 = (req: any, res: any) => {
|
|
|
496
509
|
if (sessionCookie) {
|
|
497
510
|
reqHeaders.set('cookie', sessionCookie);
|
|
498
511
|
}
|
|
499
|
-
} else if (credentials.formType === 'login') {
|
|
500
|
-
// Stale session cookie — only clear it before a fresh password
|
|
501
|
-
// login (ZERO-4247). Register and OTP flows are still anonymous at
|
|
502
|
-
// this point (no pk yet), but their session carries the pending OTP
|
|
503
|
-
// challenge; clearing it here makes the backend lose the challenge
|
|
504
|
-
// and re-issue the code on every verify (ZERO-4550).
|
|
505
|
-
// remove from headers and clear in browser
|
|
506
|
-
const currentCookies = reqHeaders.get('cookie') || '';
|
|
507
|
-
const cleanedCookies = currentCookies
|
|
508
|
-
.split(';')
|
|
509
|
-
.filter((c) => !c.trim().startsWith('osessionid='))
|
|
510
|
-
.join(';')
|
|
511
|
-
.trim();
|
|
512
|
-
reqHeaders.set('cookie', cleanedCookies);
|
|
513
|
-
|
|
514
|
-
const { localeUrlStrategy } = Settings.localization;
|
|
515
|
-
const fallbackHost =
|
|
516
|
-
req.headers['x-forwarded-host']?.toString() ||
|
|
517
|
-
req.headers.host?.toString();
|
|
518
|
-
const hostname =
|
|
519
|
-
process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
|
|
520
|
-
const rootHostname =
|
|
521
|
-
localeUrlStrategy === LocaleUrlStrategy.Subdomain
|
|
522
|
-
? getRootHostname(hostname)
|
|
523
|
-
: null;
|
|
524
|
-
const domainOption = rootHostname
|
|
525
|
-
? ` Domain=${rootHostname};`
|
|
526
|
-
: '';
|
|
527
|
-
res.setHeader('Set-Cookie', [
|
|
528
|
-
`osessionid=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;${domainOption}`,
|
|
529
|
-
`sessionid=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;${domainOption}`
|
|
530
|
-
]);
|
|
531
512
|
}
|
|
513
|
+
// A guest's anonymous session (no pk yet) is deliberately NOT
|
|
514
|
+
// stripped here. It must be forwarded on the login request so the
|
|
515
|
+
// backend can merge the guest basket into the account on sign-in
|
|
516
|
+
// (ZERO-5041). getCurrentUser cannot tell a valid anonymous session
|
|
517
|
+
// apart from a truly stale one — the commerce currentUser endpoint
|
|
518
|
+
// returns 401 for both — so pre-emptively clearing on "no pk" also
|
|
519
|
+
// discarded every legitimate guest session. The ZERO-4247
|
|
520
|
+
// stale-cookie recovery is handled below instead, as a one-time
|
|
521
|
+
// retry without the session cookie after a login actually fails.
|
|
532
522
|
}
|
|
533
523
|
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
524
|
+
const performAuthRequest = async (headers: HeadersInit) => {
|
|
525
|
+
const request = await fetch(
|
|
526
|
+
`${Settings.commerceUrl}${user[credentials.formType]}`,
|
|
527
|
+
{
|
|
528
|
+
method: 'POST',
|
|
529
|
+
headers,
|
|
530
|
+
body: JSON.stringify(credentials)
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const body = (await request.json()) as {
|
|
535
|
+
key: string;
|
|
536
|
+
non_field_errors: string[];
|
|
537
|
+
redirect_url: string;
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
return { request, body };
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
let { request: apiRequest, body: response } =
|
|
544
|
+
await performAuthRequest(reqHeaders);
|
|
545
|
+
|
|
546
|
+
// ZERO-4247 recovery: the anonymous session was forwarded so the guest
|
|
547
|
+
// basket can merge (ZERO-5041). If a session cookie was present and the
|
|
548
|
+
// login still failed, the session may be genuinely stale, so retry once
|
|
549
|
+
// without it. Scoped to password login; OTP challenges (202,
|
|
550
|
+
// ZERO-4550) and throttled requests (429) are left untouched. On a
|
|
551
|
+
// successful retry the sessionId block below overwrites the browser
|
|
552
|
+
// cookies with the freshly issued session.
|
|
553
|
+
if (
|
|
554
|
+
!response.key &&
|
|
555
|
+
credentials.formType === 'login' &&
|
|
556
|
+
req.cookies['osessionid'] &&
|
|
557
|
+
apiRequest.status !== 202 &&
|
|
558
|
+
apiRequest.status !== 429
|
|
559
|
+
) {
|
|
560
|
+
const retryHeaders = new Headers(reqHeaders);
|
|
561
|
+
const cleanedCookies = (retryHeaders.get('cookie') || '')
|
|
562
|
+
.split(';')
|
|
563
|
+
.filter((c) => !c.trim().startsWith('osessionid='))
|
|
564
|
+
.join(';')
|
|
565
|
+
.trim();
|
|
566
|
+
retryHeaders.set('cookie', cleanedCookies);
|
|
567
|
+
|
|
568
|
+
const retry = await performAuthRequest(retryHeaders);
|
|
569
|
+
if (retry.body.key) {
|
|
570
|
+
apiRequest = retry.request;
|
|
571
|
+
response = retry.body;
|
|
540
572
|
}
|
|
541
|
-
|
|
573
|
+
}
|
|
542
574
|
|
|
543
575
|
logger.info(`Login/Register request result: ${apiRequest.status}`, {
|
|
544
576
|
userIp
|
|
545
577
|
});
|
|
546
578
|
|
|
547
|
-
const response = (await apiRequest.json()) as {
|
|
548
|
-
key: string;
|
|
549
|
-
non_field_errors: string[];
|
|
550
|
-
redirect_url: string;
|
|
551
|
-
};
|
|
552
|
-
|
|
553
579
|
logger.debug(`Login/Register response: ${JSON.stringify(response)}`);
|
|
554
580
|
|
|
555
581
|
let sessionId = '';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/next",
|
|
3
3
|
"description": "Core package for Project Zero Next",
|
|
4
|
-
"version": "2.0.102
|
|
4
|
+
"version": "2.0.102",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"set-cookie-parser": "2.6.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@akinon/eslint-plugin-projectzero": "2.0.102
|
|
39
|
+
"@akinon/eslint-plugin-projectzero": "2.0.102",
|
|
40
40
|
"@babel/core": "7.26.10",
|
|
41
41
|
"@babel/preset-env": "7.26.9",
|
|
42
42
|
"@babel/preset-typescript": "7.27.0",
|