@faststore/core 4.1.2-dev.0 → 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 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 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 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 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) 44ms
9
- ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 98ms
10
- ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 22ms
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)
@@ -15,34 +16,35 @@
15
16
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:353:23)
16
17
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
17
18
 
18
- ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 66ms
19
19
  stderr | test/sdk/localization/store-url.browser.test.ts
20
20
  Error in optimistic validation: ReferenceError: Cannot access '__vite_ssr_import_4__' before initialization
21
21
  at getSettings (/home/runner/work/faststore/faststore/packages/core/src/sdk/localization/useLocalizationConfig.tsx:126:45)
22
- 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)
23
23
  at onValidate (/home/runner/work/faststore/faststore/packages/core/src/sdk/useStore.ts:18:20)
24
24
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:353:23)
25
25
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
26
26
 
27
- ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 23ms
28
- ✓  node  test/sdk/localization/useBindingSelector.test.tsx (12 tests) 28ms
29
- ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 168ms
30
- ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 20ms
31
- ✓  node  test/utils/clearCookies.test.ts (20 tests) 98ms
32
- ✓  node  test/server/cms/global.test.ts (3 tests) 22ms
33
- ✓  node  test/server/content/service.test.ts (5 tests) 19ms
34
- ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 16ms
35
- ✓  node  test/sdk/localization/store-url.test.ts (1 test) 13ms
36
- ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 13ms
37
- ✓  node  test/pages/api/preview.test.ts (2 tests) 25ms
38
- ✓  node  test/server/cms/index.test.ts (2 tests) 15ms
39
- ✓  node  test/server/index.test.ts (7 tests) 1400ms
40
- ✓ should return a valid merged GraphQL schema  377ms
41
- ✓ should exist with its plugins  587ms
42
- ✓ should handle options and execute  382ms
43
-
44
-  Test Files  17 passed (17)
45
-  Tests  203 passed (203)
46
-  Start at  22:06:03
47
-  Duration  15.04s (transform 6.91s, setup 0ms, collect 17.26s, tests 2.09s, environment 10.70s, prepare 776ms)
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,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
+ ## 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
+
6
12
  ## 4.1.2-dev.0 (2026-05-15)
7
13
 
8
14
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "4.1.2-dev.0",
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/diagnostics": "4.1.2-dev.0",
74
- "@faststore/api": "4.1.2-dev.0",
75
- "@faststore/lighthouse": "4.1.2-dev.0",
76
- "@faststore/sdk": "4.1.2-dev.0",
77
- "@faststore/ui": "4.1.2-dev.0"
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
+ })