@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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +36 -36
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/prerender-manifest.js +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/react-loadable-manifest.json +7 -7
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/948.js +2 -2
- package/.next/server/chunks/9563.js +1 -1
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/api/fs/logout.js +1 -0
- package/.next/server/pages/api/fs/logout.js.nft.json +1 -0
- package/.next/server/pages/api/graphql.js +1 -1
- package/.next/server/pages/en-US/404.html +1 -1
- package/.next/server/pages/en-US/500.html +1 -1
- package/.next/server/pages/en-US/checkout.html +1 -1
- package/.next/server/pages/en-US/login.html +1 -1
- package/.next/server/pages/en-US/s.html +1 -1
- package/.next/server/pages/en-US.html +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/static/chunks/{2927.23bae2c79f0ac0f3.js → 2927.5a79877943a6bf7c.js} +1 -1
- package/.next/static/chunks/4803.9822f0b7e469b933.js +1 -0
- package/.next/static/chunks/9173-ab3c4d9d8f978c4b.js +1 -0
- package/.next/static/chunks/{UIToast.de15325248043ce5.js → UIToast.19a8664c01a00d3a.js} +1 -1
- package/.next/static/chunks/{webpack-237c97ab0eda7441.js → webpack-7a3373ab044e72a1.js} +1 -1
- package/.next/static/{kf421rvguK2vjVH6LCPtr → uv4I-g8aFXcaBykLttQUs}/_buildManifest.js +1 -1
- package/.next/trace +139 -138
- package/.turbo/turbo-build.log +22 -21
- package/.turbo/turbo-test.log +9 -7
- package/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/src/components/account/MyAccountDrawer/OrganizationDrawer/OrganizationDrawer.tsx +104 -4
- package/src/pages/api/fs/logout.ts +76 -0
- package/src/utils/clearCookies.ts +79 -0
- package/test/utils/clearCookies.test.ts +232 -0
- package/.next/static/chunks/4803.18fbf77cd924d443.js +0 -1
- package/.next/static/chunks/9173-2ed920b87ee6640e.js +0 -1
- /package/.next/static/{kf421rvguK2vjVH6LCPtr → uv4I-g8aFXcaBykLttQUs}/_ssgManifest.js +0 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@3.96.0-dev.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
95
|
-
├ ● /[slug]/p 98.1 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
|
|
98
|
-
├ ● /500 1.57 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
|
|
104
|
-
├ ● /login 1.7 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
|
|
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
|
|
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
|
|
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
|
|
116
|
+
├ λ /pvt/account/profile 1.98 kB 142 kB
|
|
116
117
|
├ └ css/47f1b4e8de15d314.css 4.42 kB
|
|
117
|
-
├ λ /pvt/account/security 3.96 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
|
|
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-
|
|
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)
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@3.96.0-dev.
|
|
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 (
|
|
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
|
|
8
|
+
PASS test/utils/cookieCacheBusting.test.ts
|
|
8
9
|
PASS test/server/cms/index.test.ts
|
|
9
|
-
PASS test/server/index.test.ts (
|
|
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:
|
|
12
|
-
Tests:
|
|
13
|
+
Test Suites: 6 passed, 6 total
|
|
14
|
+
Tests: 47 passed, 47 total
|
|
13
15
|
Snapshots: 0 total
|
|
14
|
-
Time:
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
+
})
|