@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/api/auth.ts +116 -90
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # @akinon/next
2
2
 
3
- ## 2.0.102-beta.0
3
+ ## 2.0.102
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - cbbbfd757: ZERO-4376: Bootstrap beta cycle (next-main pre-release motor)
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 apiRequest = await fetch(
242
- `${Settings.commerceUrl}${user[credentials.formType]}`,
243
- {
244
- method: 'POST',
245
- headers: reqHeaders,
246
- body: JSON.stringify(credentials)
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 apiRequest = await fetch(
535
- `${Settings.commerceUrl}${user[credentials.formType]}`,
536
- {
537
- method: 'POST',
538
- headers: reqHeaders,
539
- body: JSON.stringify(credentials)
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-beta.0",
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-beta.0",
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",