@faststore/core 3.96.0-dev.18 → 3.96.0-dev.19

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 (45) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +36 -36
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/config.json +3 -3
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/server-production/0.pack +0 -0
  8. package/.next/cache/webpack/server-production/index.pack +0 -0
  9. package/.next/prerender-manifest.js +1 -1
  10. package/.next/prerender-manifest.json +1 -1
  11. package/.next/react-loadable-manifest.json +7 -7
  12. package/.next/routes-manifest.json +1 -1
  13. package/.next/server/chunks/948.js +2 -2
  14. package/.next/server/chunks/9563.js +1 -1
  15. package/.next/server/functions-config-manifest.json +1 -1
  16. package/.next/server/middleware-build-manifest.js +1 -1
  17. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  18. package/.next/server/pages/api/fs/logout.js +1 -0
  19. package/.next/server/pages/api/fs/logout.js.nft.json +1 -0
  20. package/.next/server/pages/api/graphql.js +1 -1
  21. package/.next/server/pages/en-US/404.html +1 -1
  22. package/.next/server/pages/en-US/500.html +1 -1
  23. package/.next/server/pages/en-US/checkout.html +1 -1
  24. package/.next/server/pages/en-US/login.html +1 -1
  25. package/.next/server/pages/en-US/s.html +1 -1
  26. package/.next/server/pages/en-US.html +1 -1
  27. package/.next/server/pages-manifest.json +1 -1
  28. package/.next/static/chunks/{2927.23bae2c79f0ac0f3.js → 2927.5a79877943a6bf7c.js} +1 -1
  29. package/.next/static/chunks/4803.9822f0b7e469b933.js +1 -0
  30. package/.next/static/chunks/9173-ab3c4d9d8f978c4b.js +1 -0
  31. package/.next/static/chunks/{UIToast.de15325248043ce5.js → UIToast.19a8664c01a00d3a.js} +1 -1
  32. package/.next/static/chunks/{webpack-237c97ab0eda7441.js → webpack-7a3373ab044e72a1.js} +1 -1
  33. package/.next/static/{kf421rvguK2vjVH6LCPtr → uv4I-g8aFXcaBykLttQUs}/_buildManifest.js +1 -1
  34. package/.next/trace +139 -138
  35. package/.turbo/turbo-build.log +22 -21
  36. package/.turbo/turbo-test.log +9 -7
  37. package/CHANGELOG.md +6 -0
  38. package/package.json +2 -2
  39. package/src/components/account/MyAccountDrawer/OrganizationDrawer/OrganizationDrawer.tsx +104 -4
  40. package/src/pages/api/fs/logout.ts +76 -0
  41. package/src/utils/clearCookies.ts +79 -0
  42. package/test/utils/clearCookies.test.ts +232 -0
  43. package/.next/static/chunks/4803.18fbf77cd924d443.js +0 -1
  44. package/.next/static/chunks/9173-2ed920b87ee6640e.js +0 -1
  45. /package/.next/static/{kf421rvguK2vjVH6LCPtr → uv4I-g8aFXcaBykLttQUs}/_ssgManifest.js +0 -0
@@ -1,23 +1,23 @@
1
1
 
2
- > @faststore/core@3.96.0-dev.17 prebuild /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@3.96.0-dev.18 prebuild /home/runner/work/faststore/faststore/packages/core
3
3
  > na run partytown && na run generate
4
4
 
5
5
 
6
- > @faststore/core@3.96.0-dev.17 partytown /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@3.96.0-dev.18 partytown /home/runner/work/faststore/faststore/packages/core
7
7
  > partytown copylib ./public/~partytown
8
8
 
9
9
  Partytown lib copied to: /home/runner/work/faststore/faststore/packages/core/public/~partytown
10
10
 
11
- > @faststore/core@3.96.0-dev.17 generate /home/runner/work/faststore/faststore/packages/core
11
+ > @faststore/core@3.96.0-dev.18 generate /home/runner/work/faststore/faststore/packages/core
12
12
  > na run generate:schema && na run generate:codegen && na run format:generated
13
13
 
14
14
 
15
- > @faststore/core@3.96.0-dev.17 generate:schema /home/runner/work/faststore/faststore/packages/core
15
+ > @faststore/core@3.96.0-dev.18 generate:schema /home/runner/work/faststore/faststore/packages/core
16
16
  > tsx src/server/generator/generateGraphQLSchemaFile.ts
17
17
 
18
18
  Schema GraphQL file generated successfully
19
19
 
20
- > @faststore/core@3.96.0-dev.17 generate:codegen /home/runner/work/faststore/faststore/packages/core
20
+ > @faststore/core@3.96.0-dev.18 generate:codegen /home/runner/work/faststore/faststore/packages/core
21
21
  > graphql-codegen
22
22
 
23
23
  [STARTED] Parse Configuration
@@ -37,11 +37,11 @@ Running lifecycle hook "afterStart" scripts...
37
37
  [CLI] Loading Documents
38
38
  [CLI] Generating output
39
39
 
40
- > @faststore/core@3.96.0-dev.17 format:generated /home/runner/work/faststore/faststore/packages/core
40
+ > @faststore/core@3.96.0-dev.18 format:generated /home/runner/work/faststore/faststore/packages/core
41
41
  > prettier --write "@generated/**/*.{ts,js,tsx,jsx,json}" --loglevel error
42
42
 
43
43
 
44
- > @faststore/core@3.96.0-dev.17 build /home/runner/work/faststore/faststore/packages/core
44
+ > @faststore/core@3.96.0-dev.18 build /home/runner/work/faststore/faststore/packages/core
45
45
  > next build
46
46
 
47
47
  ⚠ No build cache found. Please configure build caching for faster rebuilds. Read more: https://nextjs.org/docs/messages/no-cache
@@ -58,7 +58,7 @@ Browserslist: caniuse-lite is outdated. Please run:
58
58
  Creating an optimized production build ...
59
59
  Disabled SWC as replacement for Babel because of custom Babel configuration ".babelrc.js" https://nextjs.org/docs/messages/swc-disabled
60
60
  Using external babel configuration from /home/runner/work/faststore/faststore/packages/core/.babelrc.js
61
- Browserslist: browsers data (caniuse-lite) is 11 months old. Please run:
61
+ Browserslist: browsers data (caniuse-lite) is 12 months old. Please run:
62
62
  npx update-browserslist-db@latest
63
63
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
64
64
  <w> [webpack.cache.PackFileCacheStrategy] Skipped not serializable cache item 'Compilation/modules|/home/runner/work/faststore/faststore/node_modules/.pnpm/next@13.5.11_@babel+core@7.26.7_@opentelemetry+api@1.4.1_react-dom@18.3.1_react@18.3.1__react@18.3.1_sass@1.83.4/node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[7].oneOf[9].use[1]!/home/runner/work/faststore/faststore/node_modules/.pnpm/next@13.5.11_@babel+core@7.26.7_@opentelemetry+api@1.4.1_react-dom@18.3.1_react@18.3.1__react@18.3.1_sass@1.83.4/node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[7].oneOf[9].use[2]!/home/runner/work/faststore/faststore/node_modules/.pnpm/next@13.5.11_@babel+core@7.26.7_@opentelemetry+api@1.4.1_react-dom@18.3.1_react@18.3.1__react@18.3.1_sass@1.83.4/node_modules/next/dist/build/webpack/loaders/resolve-url-loader/index.js??ruleSet[1].rules[7].oneOf[9].use[3]!/home/runner/work/faststore/faststore/node_modules/.pnpm/next@13.5.11_@babel+core@7.26.7_@opentelemetry+api@1.4.1_react-dom@18.3.1_react@18.3.1__react@18.3.1_sass@1.83.4/node_modules/next/dist/compiled/sass-loader/cjs.js??ruleSet[1].rules[7].oneOf[9].use[4]!/home/runner/work/faststore/faststore/packages/core/src/components/common/Toast/section.module.scss': No serializer registered for Warning
@@ -91,39 +91,40 @@ Route (pages) Size First Load JS
91
91
  ┌ ● / 7.39 kB 147 kB
92
92
  ├ └ css/02259c549b2179f2.css 3.1 kB
93
93
  ├ /_app 0 B 108 kB
94
- ├ ● /[...slug] 2.55 kB 157 kB
95
- ├ ● /[slug]/p 98.1 kB 237 kB
94
+ ├ ● /[...slug] 2.55 kB 158 kB
95
+ ├ ● /[slug]/p 98.1 kB 238 kB
96
96
  ├ └ css/a6a4ebbe01adbbad.css 22.2 kB
97
- ├ ○ /404 1.57 kB 141 kB
98
- ├ ● /500 1.57 kB 141 kB
97
+ ├ ○ /404 1.57 kB 142 kB
98
+ ├ ● /500 1.57 kB 142 kB
99
+ ├ λ /api/fs/logout 0 B 108 kB
99
100
  ├ λ /api/graphql 0 B 108 kB
100
101
  ├ λ /api/health/live 0 B 108 kB
101
102
  ├ λ /api/health/ready 0 B 108 kB
102
103
  ├ λ /api/preview 0 B 108 kB
103
- ├ ● /checkout 749 B 140 kB
104
- ├ ● /login 1.7 kB 141 kB
104
+ ├ ● /checkout 749 B 141 kB
105
+ ├ ● /login 1.7 kB 142 kB
105
106
  ├ λ /pvt/account 247 B 108 kB
106
107
  ├ ● /pvt/account/[...unknown] 287 B 109 kB
107
- ├ λ /pvt/account/403 2.98 kB 142 kB
108
+ ├ λ /pvt/account/403 2.98 kB 143 kB
108
109
  ├ └ css/0fae3d432331aae9.css 4.68 kB
109
110
  ├ λ /pvt/account/404 2.18 kB 142 kB
110
111
  ├ └ css/0fc6b2ff69142c6a.css 4.74 kB
111
- ├ λ /pvt/account/orders 9.78 kB 149 kB
112
+ ├ λ /pvt/account/orders 9.78 kB 150 kB
112
113
  ├ └ css/40a294d0a24ad01d.css 14.1 kB
113
- ├ λ /pvt/account/orders/[id] 12.1 kB 151 kB
114
+ ├ λ /pvt/account/orders/[id] 12.1 kB 152 kB
114
115
  ├ └ css/5eecefd2c6deeee4.css 13.3 kB
115
- ├ λ /pvt/account/profile 1.98 kB 141 kB
116
+ ├ λ /pvt/account/profile 1.98 kB 142 kB
116
117
  ├ └ css/47f1b4e8de15d314.css 4.42 kB
117
- ├ λ /pvt/account/security 3.96 kB 143 kB
118
+ ├ λ /pvt/account/security 3.96 kB 144 kB
118
119
  ├ └ css/973dd40d4773e8cd.css 5.74 kB
119
- ├ λ /pvt/account/user-details 1.91 kB 141 kB
120
+ ├ λ /pvt/account/user-details 1.91 kB 142 kB
120
121
  ├ └ css/05c399956ff24b77.css 4.54 kB
121
122
  └ ● /s 3.36 kB 158 kB
122
123
  + First Load JS shared by all 112 kB
123
124
  ├ chunks/framework-d514426edf885c68.js 45.4 kB
124
125
  ├ chunks/main-ec03882c4375091d.js 33.2 kB
125
126
  ├ chunks/pages/_app-35b56c71ba4f9a54.js 25.8 kB
126
- ├ chunks/webpack-237c97ab0eda7441.js 3.85 kB
127
+ ├ chunks/webpack-7a3373ab044e72a1.js 3.85 kB
127
128
  └ css/0f070d03aacd9cc5.css 3.57 kB
128
129
 
129
130
  λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
@@ -1,15 +1,17 @@
1
1
 
2
- > @faststore/core@3.96.0-dev.17 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@3.96.0-dev.18 test /home/runner/work/faststore/faststore/packages/core
3
3
  > jest
4
4
 
5
- PASS test/utils/multipleTemplates.test.ts (25.029 s)
5
+ PASS test/utils/multipleTemplates.test.ts (28.172 s)
6
+ PASS test/utils/clearCookies.test.ts (26.854 s)
6
7
  PASS test/server/cms/global.test.ts
7
- PASS test/utils/cookieCacheBusting.test.ts (24.88 s)
8
+ PASS test/utils/cookieCacheBusting.test.ts
8
9
  PASS test/server/cms/index.test.ts
9
- PASS test/server/index.test.ts (28.994 s)
10
+ PASS test/server/index.test.ts (31.932 s)
11
+ A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
10
12
 
11
- Test Suites: 5 passed, 5 total
12
- Tests: 27 passed, 27 total
13
+ Test Suites: 6 passed, 6 total
14
+ Tests: 47 passed, 47 total
13
15
  Snapshots: 0 total
14
- Time: 30.044 s
16
+ Time: 33.242 s
15
17
  Ran all test suites.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.96.0-dev.19](https://github.com/vtex/faststore/compare/v3.96.0-dev.18...v3.96.0-dev.19) (2026-01-08)
7
+
8
+ ### Features
9
+
10
+ - logout clear storage ([#3163](https://github.com/vtex/faststore/issues/3163)) ([1fbacc3](https://github.com/vtex/faststore/commit/1fbacc3b3e32b4f83a2f56c1f97eb0f8ff843f61))
11
+
6
12
  # 3.96.0-dev.18 (2025-12-24)
7
13
 
8
14
  **Note:** Version bump only for package @faststore/core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "3.96.0-dev.18",
3
+ "version": "3.96.0-dev.19",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -123,5 +123,5 @@
123
123
  "ts-jest": "29.1.1",
124
124
  "typescript": "5.3.2"
125
125
  },
126
- "gitHead": "9542c229e2e447e1e3c2fdc38162bef78c7cc707"
126
+ "gitHead": "f0c386389af326e23b23cc9813f0821585df9723"
127
127
  }
@@ -1,6 +1,12 @@
1
1
  import { SlideOver, useFadeEffect } from '@faststore/ui'
2
2
 
3
3
  import { useSession } from 'src/sdk/session'
4
+ import {
5
+ expireCookieClient,
6
+ getCookieDomains,
7
+ getCookiePaths,
8
+ getVtexCookieNames,
9
+ } from 'src/utils/clearCookies'
4
10
  import storeConfig from '../../../../../discovery.config'
5
11
  import { ProfileSummary } from '../ProfileSummary/ProfileSummary'
6
12
  import { OrganizationDrawerBody } from './OrganizationDrawerBody'
@@ -13,11 +19,105 @@ type OrganizationDrawerProps = {
13
19
  isRepresentative: boolean
14
20
  }
15
21
 
16
- export const doLogout = () => {
22
+ const clearBrowserStorageForCurrentDomain = async () => {
23
+ if (typeof window === 'undefined' || !storeConfig) return
24
+
25
+ // Clear Faststore-specific sessionStorage keys
26
+ try {
27
+ const sessionStorageKeys = [
28
+ 'faststore_session_ready',
29
+ 'faststore_auth_cookie_value',
30
+ 'faststore_cache_bust_last_value',
31
+ ]
32
+
33
+ for (const key of sessionStorageKeys) {
34
+ try {
35
+ window.sessionStorage?.removeItem(key)
36
+ } catch {}
37
+ }
38
+
39
+ // Remove all keys starting with __fs_gallery_page_ (used for PLP pagination)
40
+ try {
41
+ const keysToRemove: string[] = []
42
+ for (let i = 0; i < window.sessionStorage.length; i++) {
43
+ const key = window.sessionStorage.key(i)
44
+ if (key && key.startsWith('__fs_gallery_page_')) {
45
+ keysToRemove.push(key)
46
+ }
47
+ }
48
+ for (const key of keysToRemove) {
49
+ try {
50
+ window.sessionStorage.removeItem(key)
51
+ } catch {}
52
+ }
53
+ } catch {}
54
+ } catch {}
55
+
56
+ // Clear all cookies containing 'vtex' in the name (case-insensitive)
57
+ try {
58
+ const hostname = window.location.hostname
59
+ const secure = window.location.protocol === 'https:'
60
+
61
+ // Extract all cookie names from document.cookie
62
+ const allCookieNames = document.cookie
63
+ .split(';')
64
+ .map((c) => c.trim())
65
+ .filter(Boolean)
66
+ .map((c) => c.split('=')[0])
67
+ .filter(Boolean)
68
+
69
+ const vtexCookieNames = getVtexCookieNames(allCookieNames)
70
+ const paths = getCookiePaths(window.location.pathname || '/')
71
+ const domains = getCookieDomains(hostname)
72
+
73
+ for (const name of vtexCookieNames) {
74
+ for (const path of paths) {
75
+ for (const domain of domains) {
76
+ try {
77
+ expireCookieClient({ name, path, domain, secure })
78
+ } catch {}
79
+ }
80
+ }
81
+ }
82
+ } catch {}
83
+
84
+ // Clear IndexedDB (keyval-store)
85
+ try {
86
+ if (!('indexedDB' in window)) return
87
+
88
+ const idb = window.indexedDB
89
+ if (!idb) return
90
+
91
+ await new Promise<void>((resolve) => {
92
+ const req = idb.deleteDatabase('keyval-store')
93
+ req.onsuccess = () => resolve()
94
+ req.onerror = () => resolve()
95
+ req.onblocked = () => resolve()
96
+ })
97
+ } catch {}
98
+ }
99
+
100
+ export const doLogout = async (_event?: unknown) => {
17
101
  if (!storeConfig) return
18
- window.location.assign(
19
- `${storeConfig.secureSubdomain}/api/vtexid/pub/logout?scope=${storeConfig.api.storeId}&returnUrl=${storeConfig.storeUrl}`
20
- )
102
+
103
+ try {
104
+ // Clear client-side storage (sessionStorage, localStorage, IndexedDB, non-HttpOnly cookies)
105
+ await clearBrowserStorageForCurrentDomain()
106
+
107
+ // Clear HttpOnly cookies via API endpoint (server-side)
108
+ try {
109
+ await fetch('/api/fs/logout', {
110
+ method: 'POST',
111
+ credentials: 'include',
112
+ })
113
+ } catch {
114
+ // Continue even if API call fails
115
+ }
116
+ } finally {
117
+ window.location.assign(
118
+ `${storeConfig.secureSubdomain}/api/vtexid/pub/logout?scope=${storeConfig.api.storeId}&returnUrl=${storeConfig.storeUrl}`
119
+ )
120
+ }
21
121
  }
22
122
 
23
123
  export const OrganizationDrawer = ({
@@ -0,0 +1,76 @@
1
+ import { parse } from 'cookie'
2
+ import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
3
+
4
+ import discoveryConfig from 'discovery.config'
5
+ import {
6
+ expireCookieServer,
7
+ getCookieDomains,
8
+ getVtexCookieNames,
9
+ } from 'src/utils/clearCookies'
10
+
11
+ const ADDITIONAL_COOKIES = ['CheckoutOrderFormOwnership'] as const
12
+
13
+ /**
14
+ * Clears all cookies containing 'vtex' in the name (case-insensitive) + ADDITIONAL_COOKIES
15
+ * This endpoint handles HttpOnly cookies that cannot be cleared via JavaScript
16
+ */
17
+ const handler: NextApiHandler = async (
18
+ request: NextApiRequest,
19
+ response: NextApiResponse
20
+ ) => {
21
+ if (request.method !== 'POST') {
22
+ response.status(405).end()
23
+ return
24
+ }
25
+
26
+ try {
27
+ const hostname = request.headers.host?.split(':')[0] ?? ''
28
+ const cookies = parse(request.headers.cookie ?? '')
29
+ const domains = getCookieDomains(hostname)
30
+ const clearedCookies: string[] = []
31
+
32
+ const vtexCookieNames = getVtexCookieNames(Object.keys(cookies))
33
+
34
+ // Clear vid_rt cookie with specific path (only if refreshToken is enabled)
35
+ if (discoveryConfig.experimental?.refreshToken && cookies.vid_rt) {
36
+ for (const domain of domains) {
37
+ const clearedCookie = expireCookieServer({
38
+ name: 'vid_rt',
39
+ path: '/api/vtexid/refreshtoken/webstore',
40
+ domain,
41
+ })
42
+ clearedCookies.push(clearedCookie)
43
+ }
44
+ }
45
+
46
+ // Clear other cookies with path /
47
+ const otherCookieNames = [
48
+ ...vtexCookieNames,
49
+ ...ADDITIONAL_COOKIES.filter((name) => cookies[name]),
50
+ ]
51
+
52
+ for (const cookieName of otherCookieNames) {
53
+ for (const domain of domains) {
54
+ const clearedCookie = expireCookieServer({
55
+ name: cookieName,
56
+ path: '/',
57
+ domain,
58
+ })
59
+ clearedCookies.push(clearedCookie)
60
+ }
61
+ }
62
+
63
+ if (clearedCookies.length > 0) {
64
+ response.setHeader('set-cookie', clearedCookies)
65
+ }
66
+
67
+ response.status(200).json({ success: true })
68
+ } catch (error) {
69
+ console.error('Error clearing cookies:', error)
70
+ response
71
+ .status(500)
72
+ .json({ success: false, error: 'Failed to clear cookies' })
73
+ }
74
+ }
75
+
76
+ export default handler
@@ -0,0 +1,79 @@
1
+ type ExpireCookieClientParams = {
2
+ name: string
3
+ path: string
4
+ domain?: string
5
+ secure?: boolean
6
+ }
7
+
8
+ type ExpireCookieServerParams = {
9
+ name: string
10
+ path: string
11
+ domain?: string
12
+ }
13
+
14
+ /**
15
+ * Client-side: Expires a cookie by setting it to expire in the past
16
+ */
17
+ export const expireCookieClient = ({
18
+ name,
19
+ path,
20
+ domain,
21
+ secure = false,
22
+ }: ExpireCookieClientParams): void => {
23
+ const domainAttr = domain ? `; domain=${domain}` : ''
24
+ const secureAttr = secure ? '; secure' : ''
25
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0; path=${path}${domainAttr}; samesite=lax${secureAttr}`
26
+ }
27
+
28
+ /**
29
+ * Server-side: Generates a Set-Cookie header string to expire a cookie
30
+ */
31
+ export const expireCookieServer = ({
32
+ name,
33
+ path,
34
+ domain,
35
+ }: ExpireCookieServerParams): string => {
36
+ const domainAttr = domain ? `; domain=${domain}` : ''
37
+ return `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; max-age=0; path=${path}${domainAttr}; samesite=lax; httponly`
38
+ }
39
+
40
+ /**
41
+ * Utility functions for clearing cookies
42
+ * Shared logic between client-side and server-side cookie clearing
43
+ */
44
+
45
+ /**
46
+ * Generates domain variations for cookie clearing
47
+ * Tries multiple domain combinations to ensure cookies are cleared regardless of how they were set
48
+ */
49
+ export const getCookieDomains = (
50
+ hostname: string
51
+ ): Array<string | undefined> => [
52
+ undefined, // host-only cookie
53
+ hostname,
54
+ hostname.startsWith('.') ? hostname : `.${hostname}`,
55
+ ]
56
+
57
+ /**
58
+ * Filters cookie names that contain 'vtex' (case-insensitive)
59
+ */
60
+ export const getVtexCookieNames = (cookieNames: string[]): string[] => {
61
+ return cookieNames.filter((name) => name.toLowerCase().includes('vtex'))
62
+ }
63
+
64
+ /**
65
+ * Generates paths to try when clearing cookies
66
+ * Includes root path and all parent paths from current pathname
67
+ */
68
+ export const getCookiePaths = (pathname: string): string[] => {
69
+ const paths: string[] = ['/']
70
+ const pathParts = pathname.split('/').filter(Boolean)
71
+
72
+ let current = ''
73
+ for (const part of pathParts) {
74
+ current += `/${part}`
75
+ if (!paths.includes(current)) paths.push(current)
76
+ }
77
+
78
+ return paths
79
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import {
6
+ expireCookieClient,
7
+ expireCookieServer,
8
+ getCookieDomains,
9
+ getCookiePaths,
10
+ getVtexCookieNames,
11
+ } from '../../src/utils/clearCookies'
12
+
13
+ describe('clearCookies', () => {
14
+ beforeEach(() => {
15
+ // Clear all cookies before each test
16
+ document.cookie.split(';').forEach((cookie) => {
17
+ const name = cookie.split('=')[0].trim()
18
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
19
+ })
20
+ })
21
+
22
+ describe('getCookieDomains', () => {
23
+ it('should return domain variations for a hostname', () => {
24
+ const hostname = 'example.com'
25
+ const domains = getCookieDomains(hostname)
26
+
27
+ expect(domains).toEqual([undefined, 'example.com', '.example.com'])
28
+ })
29
+
30
+ it('should handle hostname that already starts with dot', () => {
31
+ const hostname = '.example.com'
32
+ const domains = getCookieDomains(hostname)
33
+
34
+ expect(domains).toEqual([undefined, '.example.com', '.example.com'])
35
+ })
36
+
37
+ it('should handle localhost', () => {
38
+ const hostname = 'localhost'
39
+ const domains = getCookieDomains(hostname)
40
+
41
+ expect(domains).toEqual([undefined, 'localhost', '.localhost'])
42
+ })
43
+ })
44
+
45
+ describe('getVtexCookieNames', () => {
46
+ it('should filter cookies containing "vtex" (case-insensitive)', () => {
47
+ const cookieNames = [
48
+ 'vtex-search-anonymous',
49
+ 'VtexIdclientAutCookie_store',
50
+ 'other-cookie',
51
+ 'VTEX_SESSION',
52
+ 'vtex_segment',
53
+ ]
54
+
55
+ const result = getVtexCookieNames(cookieNames)
56
+
57
+ expect(result).toEqual([
58
+ 'vtex-search-anonymous',
59
+ 'VtexIdclientAutCookie_store',
60
+ 'VTEX_SESSION',
61
+ 'vtex_segment',
62
+ ])
63
+ })
64
+
65
+ it('should return empty array when no vtex cookies found', () => {
66
+ const cookieNames = ['other-cookie', 'another-cookie']
67
+
68
+ const result = getVtexCookieNames(cookieNames)
69
+
70
+ expect(result).toEqual([])
71
+ })
72
+
73
+ it('should handle empty array', () => {
74
+ const result = getVtexCookieNames([])
75
+
76
+ expect(result).toEqual([])
77
+ })
78
+ })
79
+
80
+ describe('getCookiePaths', () => {
81
+ it('should return root path for empty pathname', () => {
82
+ const paths = getCookiePaths('')
83
+
84
+ expect(paths).toEqual(['/'])
85
+ })
86
+
87
+ it('should return root path for root pathname', () => {
88
+ const paths = getCookiePaths('/')
89
+
90
+ expect(paths).toEqual(['/'])
91
+ })
92
+
93
+ it('should return paths for nested pathname', () => {
94
+ const paths = getCookiePaths('/api/vtexid/logout')
95
+
96
+ expect(paths).toEqual(['/', '/api', '/api/vtexid', '/api/vtexid/logout'])
97
+ })
98
+
99
+ it('should handle pathname without leading slash', () => {
100
+ const paths = getCookiePaths('api/vtexid')
101
+
102
+ expect(paths).toEqual(['/', '/api', '/api/vtexid'])
103
+ })
104
+
105
+ it('should handle single segment pathname', () => {
106
+ const paths = getCookiePaths('/api')
107
+
108
+ expect(paths).toEqual(['/', '/api'])
109
+ })
110
+ })
111
+
112
+ describe('expireCookieClient', () => {
113
+ it('should expire a cookie with default parameters', () => {
114
+ // Set a cookie first
115
+ document.cookie = 'test-cookie=value; path=/'
116
+
117
+ expireCookieClient({ name: 'test-cookie', path: '/' })
118
+
119
+ expect(document.cookie).not.toContain('test-cookie=value')
120
+ })
121
+
122
+ it('should expire a cookie with domain', () => {
123
+ document.cookie = 'test-cookie=value; path=/; domain=.example.com'
124
+
125
+ expireCookieClient({
126
+ name: 'test-cookie',
127
+ path: '/',
128
+ domain: '.example.com',
129
+ })
130
+
131
+ // Note: We can't easily verify domain-specific cookies in jsdom,
132
+ // but we can verify the function doesn't throw
133
+ expect(() => {
134
+ expireCookieClient({
135
+ name: 'test-cookie',
136
+ path: '/',
137
+ domain: '.example.com',
138
+ })
139
+ }).not.toThrow()
140
+ })
141
+
142
+ it('should expire a cookie with secure flag', () => {
143
+ document.cookie = 'secure-cookie=value; path=/; secure'
144
+
145
+ expireCookieClient({
146
+ name: 'secure-cookie',
147
+ path: '/',
148
+ secure: true,
149
+ })
150
+
151
+ expect(() => {
152
+ expireCookieClient({
153
+ name: 'secure-cookie',
154
+ path: '/',
155
+ secure: true,
156
+ })
157
+ }).not.toThrow()
158
+ })
159
+
160
+ it('should expire a cookie with all parameters', () => {
161
+ expireCookieClient({
162
+ name: 'full-cookie',
163
+ path: '/api',
164
+ domain: '.example.com',
165
+ secure: true,
166
+ })
167
+
168
+ expect(() => {
169
+ expireCookieClient({
170
+ name: 'full-cookie',
171
+ path: '/api',
172
+ domain: '.example.com',
173
+ secure: true,
174
+ })
175
+ }).not.toThrow()
176
+ })
177
+ })
178
+
179
+ describe('expireCookieServer', () => {
180
+ it('should generate Set-Cookie header string for basic cookie', () => {
181
+ const result = expireCookieServer({ name: 'test-cookie', path: '/' })
182
+
183
+ expect(result).toContain('test-cookie=')
184
+ expect(result).toContain('expires=Thu, 01 Jan 1970 00:00:00 GMT')
185
+ expect(result).toContain('max-age=0')
186
+ expect(result).toContain('path=/')
187
+ expect(result).toContain('samesite=lax')
188
+ expect(result).toContain('httponly')
189
+ })
190
+
191
+ it('should generate Set-Cookie header string with domain', () => {
192
+ const result = expireCookieServer({
193
+ name: 'test-cookie',
194
+ path: '/',
195
+ domain: '.example.com',
196
+ })
197
+
198
+ expect(result).toContain('test-cookie=')
199
+ expect(result).toContain('domain=.example.com')
200
+ expect(result).toContain('path=/')
201
+ expect(result).toContain('httponly')
202
+ })
203
+
204
+ it('should generate Set-Cookie header string without domain when undefined', () => {
205
+ const result = expireCookieServer({
206
+ name: 'test-cookie',
207
+ path: '/',
208
+ domain: undefined,
209
+ })
210
+
211
+ expect(result).toContain('test-cookie=')
212
+ expect(result).not.toContain('domain=')
213
+ expect(result).toContain('path=/')
214
+ })
215
+
216
+ it('should generate Set-Cookie header string for specific path', () => {
217
+ const result = expireCookieServer({
218
+ name: 'vid_rt',
219
+ path: '/api/vtexid/refreshtoken/webstore',
220
+ })
221
+
222
+ expect(result).toContain('vid_rt=')
223
+ expect(result).toContain('path=/api/vtexid/refreshtoken/webstore')
224
+ })
225
+
226
+ it('should always include httponly flag', () => {
227
+ const result = expireCookieServer({ name: 'test-cookie', path: '/' })
228
+
229
+ expect(result).toContain('httponly')
230
+ })
231
+ })
232
+ })