@faststore/core 4.1.1 → 4.1.2-dev.1

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.
@@ -1,9 +1,9 @@
1
1
 
2
- > @faststore/core@4.1.1-dev.1 generate /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.1.2-dev.0 generate /home/runner/work/faststore/faststore/packages/core
3
3
  > pnpm run gen-types && pnpm run cache-graphql
4
4
 
5
5
 
6
- > @faststore/core@4.1.1-dev.1 gen-types /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@4.1.2-dev.0 gen-types /home/runner/work/faststore/faststore/packages/core
7
7
  > node ../cli/bin/run generate-types .
8
8
 
9
9
  [STARTED] Parse Configuration
@@ -19,7 +19,7 @@
19
19
  [COMPLETED] Generate to /home/runner/work/faststore/faststore/packages/core/@generated/
20
20
  [COMPLETED] Generate outputs
21
21
 
22
- > @faststore/core@4.1.1-dev.1 cache-graphql /home/runner/work/faststore/faststore/packages/core
22
+ > @faststore/core@4.1.2-dev.0 cache-graphql /home/runner/work/faststore/faststore/packages/core
23
23
  > node ../cli/bin/run cache-graphql --config=./discovery.config.default.js --queries=./@generated/persisted-documents.json
24
24
 
25
25
  [Info] - Config file location: /home/runner/work/faststore/faststore/packages/core/discovery.config.default.js
@@ -1,13 +1,14 @@
1
1
 
2
- > @faststore/core@4.1.1-dev.1 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.1.2-dev.0 test /home/runner/work/faststore/faststore/packages/core
3
3
  > vitest run
4
4
 
5
5
 
6
6
   RUN  v4.0.7 /home/runner/work/faststore/faststore/packages/core
7
7
 
8
- ✓  node  test/utils/match-url.test.ts (10 tests) 32ms
9
- ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 114ms
10
- ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 44ms
8
+ ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 107ms
9
+ ✓  node  test/utils/match-url.test.ts (10 tests) 20ms
10
+ ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 30ms
11
+ ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 18ms
11
12
  stderr | test/sdk/localization/store-url.browser.test.ts
12
13
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
13
14
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:119:27)
@@ -18,31 +19,32 @@
18
19
  stderr | test/sdk/localization/store-url.browser.test.ts
19
20
  Error in optimistic validation: ReferenceError: Cannot access '__vite_ssr_import_4__' before initialization
20
21
  at getSettings (/home/runner/work/faststore/faststore/packages/core/src/sdk/localization/useLocalizationConfig.tsx:126:45)
21
- at validateSession (/home/runner/work/faststore/faststore/packages/core/src/sdk/session/index.ts:144:22)
22
+ at validateSession (/home/runner/work/faststore/faststore/packages/core/src/sdk/session/index.ts:140:22)
22
23
  at onValidate (/home/runner/work/faststore/faststore/packages/core/src/sdk/useStore.ts:18:20)
23
24
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:353:23)
24
25
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
25
26
 
26
- ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 159ms
27
- ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 19ms
28
- ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 44ms
29
- ✓  node  test/sdk/localization/useBindingSelector.test.tsx (12 tests) 26ms
30
- ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 20ms
31
- ✓  node  test/utils/clearCookies.test.ts (20 tests) 103ms
32
- ✓  node  test/server/cms/global.test.ts (3 tests) 23ms
33
- ✓  node  test/server/content/service.test.ts (5 tests) 15ms
34
- ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 25ms
35
- ✓  node  test/sdk/localization/store-url.test.ts (1 test) 10ms
36
- ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 12ms
37
- ✓  node  test/pages/api/preview.test.ts (2 tests) 23ms
38
- ✓  node  test/server/cms/index.test.ts (2 tests) 12ms
39
- ✓  node  test/server/index.test.ts (7 tests) 1874ms
40
- ✓ should return a valid merged GraphQL schema  565ms
41
- ✓ should exist with its plugins  695ms
42
- ✓ should handle options and execute  582ms
43
-
44
-  Test Files  17 passed (17)
45
-  Tests  203 passed (203)
46
-  Start at  21:45:38
47
-  Duration  14.99s (transform 5.93s, setup 0ms, collect 17.31s, tests 2.56s, environment 9.15s, prepare 745ms)
27
+ ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 18ms
28
+ ✓  node  test/sdk/localization/useBindingSelector.test.tsx (12 tests) 25ms
29
+ ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 48ms
30
+ ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 104ms
31
+ ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 15ms
32
+ ✓  node  test/utils/clearCookies.test.ts (20 tests) 101ms
33
+ ✓  node  test/server/cms/global.test.ts (3 tests) 13ms
34
+ ✓  node  test/server/content/service.test.ts (5 tests) 17ms
35
+ ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 24ms
36
+ ✓  node  test/sdk/localization/store-url.test.ts (1 test) 6ms
37
+ ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 11ms
38
+ ✓  node  test/pages/api/preview.test.ts (2 tests) 16ms
39
+ ✓  node  test/server/cms/index.test.ts (2 tests) 17ms
40
+ ✓  node  test/utils/isLocalHost.test.ts (7 tests) 8ms
41
+ ✓  node  test/server/index.test.ts (7 tests) 1496ms
42
+ ✓ should return a valid merged GraphQL schema  567ms
43
+ ✓ should exist with its plugins  503ms
44
+ ✓ should handle options and execute  400ms
45
+
46
+  Test Files  19 passed (19)
47
+  Tests  213 passed (213)
48
+  Start at  17:39:14
49
+  Duration  14.78s (transform 5.74s, setup 0ms, collect 16.26s, tests 2.09s, environment 11.06s, prepare 673ms)
48
50
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
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
+ ## 4.1.2-dev.1 (2026-05-19)
7
+
8
+ ### Bug Fixes
9
+
10
+ - bypass refresh-token flow on localhost in 403 page and useRefreshToken hook ([#3325](https://github.com/vtex/faststore/issues/3325)) ([d48571a](https://github.com/vtex/faststore/commit/d48571a15176497b019bae1462617c3159459421)), closes [#3285](https://github.com/vtex/faststore/issues/3285) [#3285](https://github.com/vtex/faststore/issues/3285)
11
+
12
+ ## 4.1.2-dev.0 (2026-05-15)
13
+
14
+ ### Bug Fixes
15
+
16
+ - bump version ([4fbfa64](https://github.com/vtex/faststore/commit/4fbfa649afc57218dbf8020abc7333c05b0b034f))
17
+
6
18
  ## 4.1.1 (2026-05-15)
7
19
 
8
20
  ### Bug Fixes
package/index.ts CHANGED
@@ -27,7 +27,7 @@ export { default as ProductShelfSection } from './src/components/sections/Produc
27
27
  export { default as RegionBarSection } from './src/components/sections/RegionBar'
28
28
  export { default as Section } from './src/components/sections/Section'
29
29
 
30
- // Delivery Promise.
30
+ // Delivery Promise
31
31
  export {
32
32
  PICKUP_IN_POINT_FACET_VALUE,
33
33
  PICKUP_POINT_FACET_KEY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "4.1.1",
3
+ "version": "4.1.2-dev.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -70,11 +70,11 @@
70
70
  "style-loader": "^3.3.1",
71
71
  "swr": "^2.2.5",
72
72
  "use-sync-external-store": "^1.6.0",
73
- "@faststore/api": "4.1.1",
74
- "@faststore/lighthouse": "4.1.1",
75
- "@faststore/diagnostics": "4.1.1",
76
- "@faststore/ui": "4.1.1",
77
- "@faststore/sdk": "4.1.1"
73
+ "@faststore/api": "4.1.2-dev.1",
74
+ "@faststore/diagnostics": "4.1.2-dev.1",
75
+ "@faststore/lighthouse": "4.1.2-dev.1",
76
+ "@faststore/sdk": "4.1.2-dev.1",
77
+ "@faststore/ui": "4.1.2-dev.1"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@cypress/code-coverage": "^3.12.1",
@@ -10,6 +10,7 @@ import type { NextApiHandler, NextApiRequest } from 'next'
10
10
  import discoveryConfig from 'discovery.config'
11
11
  import { getJWTAutCookie } from 'src/utils/getCookie'
12
12
  import { getRequestHostname } from 'src/utils/getRequestHostname'
13
+ import { isLocalHost } from 'src/utils/isLocalHost'
13
14
  import { shouldForceRefreshTokenForValidateSession } from 'src/utils/validateSessionRefreshToken'
14
15
  import { execute } from '../../server'
15
16
 
@@ -142,8 +143,7 @@ const handler: NextApiHandler = async (request, response) => {
142
143
  // value is used to cache bust the request if there is a VtexIdclientAutCookie
143
144
  const { operation, variables, query, v: value } = parseRequest(request)
144
145
 
145
- const hostname = request.headers.host?.split(':')[0]
146
- const isLocal = hostname === 'localhost' || hostname === '127.0.0.1'
146
+ const isLocal = isLocalHost(getRequestHostname(request.headers.host))
147
147
 
148
148
  if (
149
149
  !isLocal &&
@@ -23,6 +23,8 @@ import { useRefreshToken } from 'src/sdk/account/useRefreshToken'
23
23
  import PageProvider from 'src/sdk/overrides/PageProvider'
24
24
  import { execute } from 'src/server'
25
25
  import { injectGlobalSections } from 'src/server/cms/global'
26
+ import { getRequestHostname } from 'src/utils/getRequestHostname'
27
+ import { isLocalHost } from 'src/utils/isLocalHost'
26
28
  import { withLocaleValidationSSR } from 'src/utils/localization/withLocaleValidation'
27
29
  import { getMyAccountRedirect } from 'src/utils/myAccountRedirect'
28
30
 
@@ -137,10 +139,18 @@ const getServerSidePropsBase: GetServerSideProps<
137
139
  const fromPage =
138
140
  typeof context.query.from === 'string' ? context.query.from : ''
139
141
 
142
+ // The refresh-token round-trip is unreachable from localhost (cross-origin
143
+ // POST that drops the `vid_rt` cookie), and forcing it would clear the
144
+ // manually injected `VtexIdclientAutCookie_<account>`. Render the static
145
+ // 403 view instead so developers can keep testing logged-in scenarios
146
+ // regardless of the `experimental.refreshToken` flag.
147
+ const isLocal = isLocalHost(getRequestHostname(context.req.headers.host))
148
+
140
149
  return {
141
150
  props: {
142
151
  globalSections: globalSectionsResult,
143
152
  needsRefreshToken:
153
+ !isLocal &&
144
154
  (statusCode === 401 || statusCode === 403) &&
145
155
  storeConfig.experimental?.refreshToken,
146
156
  fromPage,
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from 'react'
2
+ import { isLocalHostBrowser } from 'src/utils/isLocalHost'
2
3
  import { logoutAndClearSession, sessionStore } from '../session'
3
4
  import { isRefreshTokenSuccessful, refreshTokenRequest } from './refreshToken'
4
5
 
@@ -12,6 +13,17 @@ export const useRefreshToken = (
12
13
  const handleRefreshTokenAndUpdateSession = async () => {
13
14
  if (!needsRefreshToken) return
14
15
 
16
+ // The refresh-token endpoint lives on the production origin and relies on
17
+ // the `vid_rt` cookie scoped to that origin. From localhost the request is
18
+ // cross-origin, so the cookie is not sent and the refresh always fails —
19
+ // which would also wipe the manually injected `VtexIdclientAutCookie_<account>`
20
+ // via `logoutAndClearSession`. Bail out and fall back to the static 403
21
+ // view so developers can keep testing logged-in flows.
22
+ if (isLocalHostBrowser()) {
23
+ setShouldShow403(true)
24
+ return
25
+ }
26
+
15
27
  const currentSession = sessionStore.read() ?? sessionStore.readInitial()
16
28
 
17
29
  try {
@@ -8,6 +8,7 @@ import type {
8
8
  ValidateSessionMutationVariables,
9
9
  } from '@generated/graphql'
10
10
  import deepEqual from 'fast-deep-equal'
11
+ import { isLocalHostBrowser } from 'src/utils/isLocalHost'
11
12
  import { filterChannel } from 'src/utils/utilities'
12
13
  import storeConfig from '../../../discovery.config'
13
14
  import {
@@ -21,11 +22,6 @@ import { createValidationStore, useStore } from '../useStore'
21
22
  import { getPostalCode } from '../userLocation/index'
22
23
  import { RELOAD_AFTER_LOGOUT_KEY, SESSION_READY_KEY } from './storageKeys'
23
24
 
24
- const isLocalEnvironment = (): boolean =>
25
- typeof window !== 'undefined' &&
26
- (window.location.hostname === 'localhost' ||
27
- window.location.hostname === '127.0.0.1')
28
-
29
25
  const isReloadAfterLogoutPending = (): boolean => {
30
26
  try {
31
27
  return (
@@ -129,7 +125,7 @@ export const validateSession = async (session: Session) => {
129
125
  // On failure (logoutAndClearSession already triggered), bail out.
130
126
  // Skipped in local environments where the refresh token infrastructure is unavailable.
131
127
  if (
132
- !isLocalEnvironment() &&
128
+ !isLocalHostBrowser() &&
133
129
  storeConfig.experimental?.refreshToken &&
134
130
  isRefreshAfterExpired(session)
135
131
  ) {
@@ -200,7 +196,7 @@ export const validateSession = async (session: Session) => {
200
196
  return data.validateSession
201
197
  } catch (error) {
202
198
  const shouldRefreshToken =
203
- !isLocalEnvironment() &&
199
+ !isLocalHostBrowser() &&
204
200
  error?.status === 401 &&
205
201
  storeConfig.experimental?.refreshToken
206
202
 
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Hostnames that represent a local development environment.
3
+ *
4
+ * These are the only hosts where the FastStore app skips the refresh-token
5
+ * flow so developers can drive the app with a manually injected
6
+ * `VtexIdclientAutCookie_<account>` cookie regardless of feature flags.
7
+ */
8
+ const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1'])
9
+
10
+ /**
11
+ * Returns `true` when the given hostname corresponds to a local development
12
+ * environment (`localhost` / `127.0.0.1`).
13
+ *
14
+ * Refresh-token and other production-only flows MUST be bypassed on these
15
+ * hostnames so that developers can test logged-in scenarios with manually
16
+ * injected cookies, independent of any experimental flag.
17
+ *
18
+ * The input MUST be a bare hostname (no port). Use {@link getRequestHostname}
19
+ * on server-side request headers before calling this helper.
20
+ */
21
+ export function isLocalHost(hostname: string | null | undefined): boolean {
22
+ if (!hostname) return false
23
+ return LOCAL_HOSTNAMES.has(hostname.toLowerCase())
24
+ }
25
+
26
+ /**
27
+ * Browser-side wrapper that reads `window.location.hostname`. Returns `false`
28
+ * when `window` is not available (SSR/Node).
29
+ */
30
+ export function isLocalHostBrowser(): boolean {
31
+ if (typeof window === 'undefined') return false
32
+ return isLocalHost(window.location.hostname)
33
+ }
@@ -0,0 +1,37 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { isLocalHostBrowser } from '../../src/utils/isLocalHost'
3
+
4
+ const originalLocation = window.location
5
+
6
+ function stubHostname(hostname: string) {
7
+ // jsdom forbids assigning to `window.location` directly, so we redefine the
8
+ // descriptor with a minimal stub that exposes the hostname we want to test.
9
+ Object.defineProperty(window, 'location', {
10
+ configurable: true,
11
+ value: { ...originalLocation, hostname },
12
+ })
13
+ }
14
+
15
+ describe('isLocalHostBrowser', () => {
16
+ afterEach(() => {
17
+ Object.defineProperty(window, 'location', {
18
+ configurable: true,
19
+ value: originalLocation,
20
+ })
21
+ })
22
+
23
+ it('returns true when window.location.hostname is "localhost"', () => {
24
+ stubHostname('localhost')
25
+ expect(isLocalHostBrowser()).toBe(true)
26
+ })
27
+
28
+ it('returns true when window.location.hostname is "127.0.0.1"', () => {
29
+ stubHostname('127.0.0.1')
30
+ expect(isLocalHostBrowser()).toBe(true)
31
+ })
32
+
33
+ it('returns false for a production hostname', () => {
34
+ stubHostname('brandless.fast.store')
35
+ expect(isLocalHostBrowser()).toBe(false)
36
+ })
37
+ })
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isLocalHost } from '../../src/utils/isLocalHost'
3
+
4
+ describe('isLocalHost', () => {
5
+ it('returns true for "localhost"', () => {
6
+ expect(isLocalHost('localhost')).toBe(true)
7
+ })
8
+
9
+ it('returns true for "127.0.0.1"', () => {
10
+ expect(isLocalHost('127.0.0.1')).toBe(true)
11
+ })
12
+
13
+ it('is case-insensitive', () => {
14
+ expect(isLocalHost('LOCALHOST')).toBe(true)
15
+ expect(isLocalHost('Localhost')).toBe(true)
16
+ })
17
+
18
+ it('returns false for a production hostname', () => {
19
+ expect(isLocalHost('brandless.fast.store')).toBe(false)
20
+ })
21
+
22
+ it('returns false for IPv6 loopback (not in the bypass list)', () => {
23
+ // We intentionally bypass only the two canonical local hosts. IPv6
24
+ // loopback (`::1`) and other loopback variants are out of scope.
25
+ expect(isLocalHost('::1')).toBe(false)
26
+ })
27
+
28
+ it('returns false for inputs that still carry a port', () => {
29
+ // Callers MUST normalize the hostname via getRequestHostname first.
30
+ expect(isLocalHost('localhost:3000')).toBe(false)
31
+ })
32
+
33
+ it('returns false for null/undefined/empty input', () => {
34
+ expect(isLocalHost(null)).toBe(false)
35
+ expect(isLocalHost(undefined)).toBe(false)
36
+ expect(isLocalHost('')).toBe(false)
37
+ })
38
+ })