@faststore/core 4.10.1 → 4.10.2-dev.0

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 (79) hide show
  1. package/.turbo/turbo-generate.log +3 -3
  2. package/.turbo/turbo-test.log +304 -252
  3. package/@generated/graphql.ts +1 -0
  4. package/@generated/schema.graphql +1 -1
  5. package/CHANGELOG.md +82 -0
  6. package/package.json +6 -6
  7. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +3 -3
  8. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +5 -7
  9. package/src/components/account/Drawer/OrganizationSignInButton/OrganizationSignInButton.tsx +4 -1
  10. package/src/components/account/Layout/Layout.tsx +5 -7
  11. package/src/components/account/Menu/Menu.tsx +1 -1
  12. package/src/components/navigation/Navbar/Navbar.tsx +3 -3
  13. package/src/components/navigation/Navbar/NavbarAccountArea.tsx +5 -5
  14. package/src/components/navigation/NavbarSlider/NavbarSlider.tsx +3 -3
  15. package/src/experimental/myAccountCmsServerSideProps.ts +3 -3
  16. package/src/experimental/myAccountServerSideProps.ts +4 -4
  17. package/src/pages/[slug]/p.tsx +18 -4
  18. package/src/pages/api/fs/logout.ts +18 -10
  19. package/src/pages/api/fs/switch-contract.ts +170 -21
  20. package/src/pages/api/graphql.ts +101 -60
  21. package/src/pages/pvt/account/403.tsx +5 -4
  22. package/src/pages/pvt/account/cards/index.tsx +5 -5
  23. package/src/pages/pvt/account/orders/[id].tsx +5 -5
  24. package/src/pages/pvt/account/orders/index.tsx +5 -5
  25. package/src/pages/pvt/account/profile.tsx +5 -5
  26. package/src/pages/pvt/account/quotes/index.tsx +5 -5
  27. package/src/pages/pvt/account/security.tsx +5 -5
  28. package/src/pages/pvt/account/user-details.tsx +6 -6
  29. package/src/sdk/account/getB2BSessionClaims.ts +1 -1
  30. package/src/sdk/account/getIsB2BBuyer.ts +28 -0
  31. package/src/sdk/account/refreshToken.ts +89 -11
  32. package/src/sdk/account/refreshTokenPaths.ts +25 -0
  33. package/src/sdk/account/useRefreshToken.ts +15 -11
  34. package/src/sdk/product/isProductNotFound.ts +13 -0
  35. package/src/sdk/product/usePageProductsQuery.ts +80 -21
  36. package/src/sdk/product/useProductNotFoundRedirect.ts +43 -0
  37. package/src/sdk/product/useProductQuery.ts +29 -0
  38. package/src/sdk/session/index.ts +46 -34
  39. package/src/server/cms/index.ts +59 -7
  40. package/src/server/cms/pdp.ts +10 -5
  41. package/src/server/cms/plp.ts +10 -5
  42. package/src/server/content/promiseCache.ts +68 -0
  43. package/src/server/content/service.ts +71 -51
  44. package/src/utils/getCheckoutParentDomain.ts +53 -0
  45. package/src/utils/isLocalHost.ts +2 -16
  46. package/src/utils/shouldBypassRefreshToken.ts +63 -0
  47. package/src/utils/trustedForwardedHost.ts +18 -0
  48. package/test/components/account/Layout.test.tsx +1 -1
  49. package/test/components/account/Menu.browser.test.tsx +7 -5
  50. package/test/components/account/OrganizationDrawer.browser.test.tsx +13 -13
  51. package/test/components/account/OrganizationDrawerBody.test.tsx +3 -3
  52. package/test/components/account/OrganizationSignInButton.browser.test.tsx +19 -4
  53. package/test/components/navigation/Navbar.browser.test.tsx +3 -3
  54. package/test/components/navigation/NavbarAccountArea.browser.test.tsx +5 -5
  55. package/test/components/navigation/NavbarSlider.browser.test.tsx +3 -3
  56. package/test/experimental/myAccountCmsServerSideProps.test.ts +5 -5
  57. package/test/pages/api/graphql.test.ts +175 -0
  58. package/test/pages/api/logout.test.ts +106 -0
  59. package/test/pages/api/switch-contract.test.ts +443 -15
  60. package/test/pages/pvt/account/403.test.ts +67 -0
  61. package/test/pages/pvt/account/cards/index.test.ts +3 -3
  62. package/test/sdk/account/getIsB2BBuyer.test.ts +52 -0
  63. package/test/sdk/account/refreshToken.browser.test.ts +10 -1
  64. package/test/sdk/account/refreshToken.test.ts +150 -18
  65. package/test/sdk/account/useRefreshToken.browser.test.ts +72 -0
  66. package/test/sdk/product/usePageProductsQuery.browser.test.ts +260 -0
  67. package/test/sdk/product/usePageProductsQuery.deliveryPromise.browser.test.ts +130 -0
  68. package/test/sdk/product/useProductNotFoundRedirect.browser.test.ts +83 -0
  69. package/test/sdk/product/useProductQuery.browser.test.ts +154 -0
  70. package/test/sdk/session/refreshTokenRoute.browser.test.ts +106 -0
  71. package/test/sdk/session/validateSession.browser.test.ts +111 -5
  72. package/test/server/cms/index.test.ts +102 -9
  73. package/test/server/content/promiseCache.test.ts +150 -0
  74. package/test/server/content/service.test.ts +277 -11
  75. package/test/utils/shouldBypassRefreshToken.browser.test.ts +38 -0
  76. package/test/utils/shouldBypassRefreshToken.test.ts +98 -0
  77. package/test/utils/trustedForwardedHost.test.ts +21 -0
  78. package/src/sdk/account/getIsRepresentative.ts +0 -20
  79. package/test/utils/isLocalHost.browser.test.ts +0 -37
@@ -1,9 +1,17 @@
1
1
  import { parse } from 'cookie'
2
2
  import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
3
+ import { isIP } from 'node:net'
3
4
 
4
5
  import storeConfig from 'discovery.config'
5
6
  import { expireCookieServer, getCookieDomains } from 'src/utils/clearCookies'
7
+ import { getCheckoutParentDomain } from 'src/utils/getCheckoutParentDomain'
6
8
  import { getRequestHostname } from 'src/utils/getRequestHostname'
9
+ import {
10
+ isCookieSafeHostname,
11
+ isHostAllowed,
12
+ removeCookieDomain,
13
+ singleForwardedHost,
14
+ } from 'src/utils/trustedForwardedHost'
7
15
 
8
16
  /**
9
17
  * Cookies holding the checkout orderForm of the previous commercial context.
@@ -42,6 +50,23 @@ const isAuthCookie = (cookie: unknown): cookie is VtexAuthCookie => {
42
50
  return Boolean(candidate?.Name?.trim() && candidate?.Value)
43
51
  }
44
52
 
53
+ /**
54
+ * A non-positive `expiresIn` would turn into `Max-Age=0`, which expires the
55
+ * credential the response is supposed to install — logging the buyer out while
56
+ * the route reports success. Anything that is not a positive integer falls back
57
+ * to the default.
58
+ */
59
+ const resolveMaxAge = (expiresIn: unknown): number =>
60
+ typeof expiresIn === 'number' && Number.isInteger(expiresIn) && expiresIn > 0
61
+ ? expiresIn
62
+ : DEFAULT_AUTH_COOKIE_MAX_AGE
63
+
64
+ /**
65
+ * Uses the forwarded protocol when a proxy sets it, otherwise the connection
66
+ * itself. A non-local hostname alone does not imply https: a `Secure` cookie
67
+ * sent over plain http is dropped by the browser while the route reports
68
+ * success.
69
+ */
45
70
  const isSecureRequest = (request: NextApiRequest): boolean => {
46
71
  const forwarded = request.headers['x-forwarded-proto']
47
72
  const proto = Array.isArray(forwarded) ? forwarded[0] : forwarded
@@ -50,41 +75,162 @@ const isSecureRequest = (request: NextApiRequest): boolean => {
50
75
  return proto.split(',')[0].trim() === 'https'
51
76
  }
52
77
 
53
- const hostname = getRequestHostname(request.headers.host)
78
+ const socket = request.socket
79
+ return Boolean(socket && 'encrypted' in socket && socket.encrypted)
80
+ }
81
+
82
+ /**
83
+ * Every parent of `hostname` with at least two labels, closest first:
84
+ * `a.b.store.com` gives `b.store.com` and `store.com`. A parent that is a
85
+ * public suffix (e.g. `com.br`) is rejected by the browser, so expiring a
86
+ * cookie there is a no-op.
87
+ */
88
+ const getParentDomains = (hostname: string): string[] => {
89
+ const labels = hostname.split('.')
54
90
 
55
- return hostname !== 'localhost' && hostname !== '127.0.0.1'
91
+ return labels
92
+ .slice(1, -1)
93
+ .map((_, index) => labels.slice(index + 1).join('.'))
56
94
  }
57
95
 
58
96
  /**
59
- * A non-positive `expiresIn` would turn into `Max-Age=0`, which expires the
60
- * credential the response is supposed to install — logging the buyer out while
61
- * the route reports success. Anything that is not a positive integer falls back
62
- * to the default.
97
+ * Hosts where the auth cookies stay host-only: WebOps previews, whose ingress
98
+ * does not pass the browser host in `Host`, and local development.
99
+ * `{account}.vtex.app` is a production domain that receives the browser host,
100
+ * so it gets a Domain like any custom domain. `/api/graphql` keeps its wider
101
+ * `.vtex.app` list for the cookies it relays from upstream.
63
102
  */
64
- const resolveMaxAge = (expiresIn: unknown): number =>
65
- typeof expiresIn === 'number' && Number.isInteger(expiresIn) && expiresIn > 0
66
- ? expiresIn
67
- : DEFAULT_AUTH_COOKIE_MAX_AGE
103
+ const HOST_ONLY_AUTH_COOKIE_SUFFIXES = [
104
+ 'localhost',
105
+ '.localhost',
106
+ '.preview.vtex.app',
107
+ ]
108
+
109
+ const isHostOnlyCookieHost = (hostname: string | null): boolean =>
110
+ !!hostname && isHostAllowed(hostname, HOST_ONLY_AUTH_COOKIE_SUFFIXES)
68
111
 
69
112
  /**
70
- * Mirrors the attributes the storefront used to write via `document.cookie`.
71
- * `HttpOnly` is deliberately left out: the cookie lives on a domain shared
72
- * with VTEX's own checkout, so hardening it is a separate, explicitly tested
73
- * change.
113
+ * Domain for the auth cookies, or `undefined` to write them host-only.
114
+ *
115
+ * Previews and local hosts stay host-only: behind the preview ingress `Host`
116
+ * is not the host the browser requested, and a mismatched Domain makes the browser drop
117
+ * the cookie while the route reports success. `x-forwarded-host` only decides
118
+ * that the cookie is host-only; it never chooses the Domain. IP literals and
119
+ * hosts that cannot be written verbatim into the attribute (including a
120
+ * trailing-dot FQDN) are host-only too.
121
+ */
122
+ const resolveAuthCookieDomain = (
123
+ request: NextApiRequest
124
+ ): string | undefined => {
125
+ const hostname = getRequestHostname(request.headers.host)
126
+ const forwardedHost = getRequestHostname(
127
+ singleForwardedHost(request.headers['x-forwarded-host'])
128
+ )
129
+
130
+ if (
131
+ !hostname ||
132
+ isHostOnlyCookieHost(hostname) ||
133
+ isHostOnlyCookieHost(forwardedHost) ||
134
+ isIP(hostname) !== 0 ||
135
+ hostname.endsWith('.') ||
136
+ !isCookieSafeHostname(hostname)
137
+ ) {
138
+ return undefined
139
+ }
140
+
141
+ return hostname
142
+ }
143
+
144
+ /**
145
+ * Mirrors the attributes VTEX ID uses when it sets these cookies at login and
146
+ * refresh (`Domain=<host>; Path=/; Secure; SameSite=None; HttpOnly`).
147
+ *
148
+ * Browsers tell cookies apart by name, domain and path, and a cookie with a
149
+ * Domain attribute never matches a host-only one. Writing the contract token
150
+ * host-only would sit next to VTEX ID's cookie instead of replacing it, leaving
151
+ * two `VtexIdclientAutCookie_<account>` with different contracts. Each reader
152
+ * then picks a different one. Any host-only copy left by earlier versions is
153
+ * expired in the same batch.
154
+ *
155
+ * VTEX ID's refresh also writes the cookie on the parent domain (e.g.
156
+ * `Domain=store.com` for `www.store.com`). Parent copies are expired, so the
157
+ * only auth cookie left holds the switched contract. The exception is the
158
+ * parent shared with a checkout on a sibling host (`secure.store.com`): the
159
+ * token is written there too, with the same value as the host copy, so
160
+ * checkout keeps the session after the switch.
161
+ *
162
+ * The Domain-scoped cookie is always `Secure`, like VTEX ID's, because the
163
+ * forwarded protocol is not reliable behind the CDN. The host-only fallback
164
+ * follows the request protocol so plain-http local hosts keep working.
74
165
  */
75
166
  const buildAuthCookies = (
76
167
  payload: SwitchPropertiesResponse,
168
+ domain: string | undefined,
77
169
  secure: boolean
78
170
  ): string[] => {
79
- const maxAge = resolveMaxAge(payload.expiresIn)
80
- const secureAttr = secure ? '; Secure' : ''
171
+ const checkoutParent = domain ? getCheckoutParentDomain(domain) : undefined
172
+ const staleScopes: Array<string | undefined> = domain
173
+ ? [
174
+ undefined,
175
+ ...getParentDomains(domain).filter(
176
+ (parent) => parent !== checkoutParent
177
+ ),
178
+ ]
179
+ : []
180
+ const writeScopes = checkoutParent ? [domain, checkoutParent] : [domain]
181
+ const attributes = (scope: string | undefined) =>
182
+ [
183
+ scope && `Domain=${scope}`,
184
+ 'Path=/',
185
+ `Max-Age=${resolveMaxAge(payload.expiresIn)}`,
186
+ 'HttpOnly',
187
+ (scope || secure) && 'Secure',
188
+ `SameSite=${scope ? 'None' : 'Lax'}`,
189
+ ]
190
+ .filter(Boolean)
191
+ .join('; ')
81
192
 
82
193
  return [payload.authCookie, payload.accountAuthCookie]
83
194
  .filter(isAuthCookie)
84
- .map(
85
- (cookie) =>
86
- `${cookie.Name}=${cookie.Value}; Path=/; Max-Age=${maxAge}; SameSite=Lax${secureAttr}`
87
- )
195
+ .flatMap((cookie) => [
196
+ ...staleScopes.map((staleDomain) =>
197
+ expireCookieServer({
198
+ name: cookie.Name,
199
+ path: '/',
200
+ domain: staleDomain,
201
+ })
202
+ ),
203
+ ...writeScopes.map(
204
+ (scope) => `${cookie.Name}=${cookie.Value}; ${attributes(scope)}`
205
+ ),
206
+ ])
207
+ }
208
+
209
+ /**
210
+ * `switch-properties` also issues `vid_rt` (the refresh token) on the
211
+ * Authenticator refresh paths. It is called server-side on
212
+ * `<account>.myvtex.com`, so its Domain points there and the browser would
213
+ * reject it. Re-scope it like the auth cookies so the next B2B refresh, which
214
+ * goes through the Authenticator route, finds it.
215
+ *
216
+ * Only relayed with `experimental.refreshToken` on: otherwise nothing uses it,
217
+ * and `/api/fs/logout` does not expire it either, so it would outlive logout.
218
+ */
219
+ const relayRefreshTokenCookies = (
220
+ headers: Headers | undefined,
221
+ domain: string | undefined
222
+ ): string[] => {
223
+ if (!storeConfig.experimental?.refreshToken) {
224
+ return []
225
+ }
226
+
227
+ return (headers?.getSetCookie?.() ?? [])
228
+ .filter((cookie) => cookie.trimStart().startsWith('vid_rt='))
229
+ .map((cookie) => {
230
+ const hostOnly = removeCookieDomain(cookie)
231
+
232
+ return domain ? `${hostOnly}; Domain=${domain}` : hostOnly
233
+ })
88
234
  }
89
235
 
90
236
  const buildOrderFormExpiryCookies = (request: NextApiRequest): string[] => {
@@ -167,8 +313,11 @@ const handler: NextApiHandler<SwitchContractResponse> = async (
167
313
  return
168
314
  }
169
315
 
316
+ const domain = resolveAuthCookieDomain(request)
317
+
170
318
  response.setHeader('Set-Cookie', [
171
- ...buildAuthCookies(payload, isSecureRequest(request)),
319
+ ...buildAuthCookies(payload, domain, isSecureRequest(request)),
320
+ ...relayRefreshTokenCookies(switchResponse.headers, domain),
172
321
  ...buildOrderFormExpiryCookies(request),
173
322
  ])
174
323
 
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  BadRequestError,
3
+ type CacheControl,
3
4
  UnauthorizedError,
4
5
  isFastStoreError,
5
6
  stringifyCacheControl,
@@ -10,8 +11,9 @@ import type { NextApiHandler, NextApiRequest } from 'next'
10
11
  import discoveryConfig from 'discovery.config'
11
12
  import { getJWTAutCookie } from 'src/utils/getCookie'
12
13
  import { getRequestHostname } from 'src/utils/getRequestHostname'
13
- import { isLocalHost } from 'src/utils/isLocalHost'
14
+ import { shouldBypassRefreshTokenForRequest } from 'src/utils/shouldBypassRefreshToken'
14
15
  import {
16
+ HOST_ONLY_COOKIE_HOST_SUFFIXES,
15
17
  isHostAllowed,
16
18
  removeCookieDomain,
17
19
  singleForwardedHost,
@@ -24,7 +26,6 @@ const OTELLogger = logger('@faststore/core')
24
26
 
25
27
  const DEFAULT_MAX_AGE = 5 * 60 // 5 minutes
26
28
  const DEFAULT_STALE_WHILE_REVALIDATE = 60 * 60 // 1 hour
27
- const ALLOWED_HOST_SUFFIXES = ['localhost', '.vtex.app', '.localhost']
28
29
 
29
30
  /**
30
31
  * Recovers a FastStoreError from an execution error, whether it is the error
@@ -87,7 +88,8 @@ const normalizeSetCookieDomain = ({
87
88
  OTELLogger('warn', 'Ignoring multi-value or malformed x-forwarded-host')
88
89
  }
89
90
  const host =
90
- forwardedHost && isHostAllowed(forwardedHost, ALLOWED_HOST_SUFFIXES)
91
+ forwardedHost &&
92
+ isHostAllowed(forwardedHost, HOST_ONLY_COOKIE_HOST_SUFFIXES)
91
93
  ? forwardedHost
92
94
  : getRequestHostname(request.headers.host)
93
95
  if (!host) {
@@ -96,7 +98,7 @@ const normalizeSetCookieDomain = ({
96
98
  const cookieDomain = domainMatch[1]
97
99
 
98
100
  if (
99
- !isHostAllowed(host, ALLOWED_HOST_SUFFIXES) ||
101
+ !isHostAllowed(host, HOST_ONLY_COOKIE_HOST_SUFFIXES) ||
100
102
  !shouldReplaceCookieDomain({ cookieDomain, host })
101
103
  ) {
102
104
  return setCookie
@@ -260,6 +262,87 @@ const respondToUnexpectedError = (
260
262
  response.status(500).end()
261
263
  }
262
264
 
265
+ /**
266
+ * With `experimental.refreshToken` on, rejects a ValidateSession with 401 when
267
+ * the token needs a refresh, so the client runs the refresh-token flow first.
268
+ *
269
+ * Skipped on localhost and WebOps previews: neither proxies VTEX ID, so the
270
+ * refresh would fail and log out a manually injected auth cookie.
271
+ */
272
+ const assertValidateSessionTokenIsFresh = ({
273
+ request,
274
+ operationName,
275
+ sessionRefreshAfter,
276
+ }: {
277
+ request: NextApiRequest
278
+ operationName: string | undefined
279
+ sessionRefreshAfter: string | null | undefined
280
+ }) => {
281
+ if (
282
+ operationName !== 'ValidateSession' ||
283
+ !discoveryConfig.experimental?.refreshToken ||
284
+ shouldBypassRefreshTokenForRequest(request.headers)
285
+ ) {
286
+ return
287
+ }
288
+
289
+ const jwt = getJWTAutCookie({
290
+ headers: request.headers,
291
+ account: discoveryConfig.api.storeId,
292
+ })
293
+
294
+ if (shouldForceRefreshTokenForValidateSession({ jwt, sessionRefreshAfter })) {
295
+ throw new UnauthorizedError(
296
+ 'Unauthorized: Token expired. Please login again or refresh the page.'
297
+ )
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Cache-Control for a successful response: the directives collected while
303
+ * executing the operation when there are any, otherwise a default
304
+ * `s-maxage`/`stale-while-revalidate` for GET queries and `no-store` for
305
+ * everything else.
306
+ */
307
+ const getSuccessCacheControl = ({
308
+ request,
309
+ operationName,
310
+ cacheControl,
311
+ hasAuthCookie,
312
+ }: {
313
+ request: NextApiRequest
314
+ operationName: string | undefined
315
+ cacheControl: CacheControl | undefined
316
+ hasAuthCookie: boolean
317
+ }) => {
318
+ if (cacheControl) {
319
+ return stringifyCacheControl(cacheControl, hasAuthCookie)
320
+ }
321
+
322
+ if (
323
+ request.method !== 'GET' ||
324
+ !operationName?.toLowerCase()?.endsWith('query')
325
+ ) {
326
+ return 'no-cache, no-store'
327
+ }
328
+
329
+ const graphqlCacheControl = discoveryConfig?.experimental?.graphqlCacheControl
330
+
331
+ const maxAge =
332
+ graphqlCacheControl?.maxAge && graphqlCacheControl.maxAge > 0
333
+ ? graphqlCacheControl.maxAge
334
+ : DEFAULT_MAX_AGE // 5 minutes
335
+
336
+ const staleWhileRevalidate =
337
+ graphqlCacheControl?.staleWhileRevalidate &&
338
+ graphqlCacheControl.staleWhileRevalidate > 0
339
+ ? graphqlCacheControl.staleWhileRevalidate
340
+ : DEFAULT_STALE_WHILE_REVALIDATE // 1 hour
341
+
342
+ const scope = hasAuthCookie ? 'private' : 'public'
343
+ return `${scope}, s-maxage=${maxAge}, stale-while-revalidate=${staleWhileRevalidate}`
344
+ }
345
+
263
346
  const handler: NextApiHandler = async (request, response) => {
264
347
  if (request.method !== 'POST' && request.method !== 'GET') {
265
348
  response.status(405).end()
@@ -286,29 +369,11 @@ const handler: NextApiHandler = async (request, response) => {
286
369
  operation?.__meta__?.operationHash
287
370
  )
288
371
 
289
- const isLocal = isLocalHost(getRequestHostname(request.headers.host))
290
-
291
- if (
292
- !isLocal &&
293
- operation.__meta__.operationName === 'ValidateSession' &&
294
- discoveryConfig.experimental?.refreshToken
295
- ) {
296
- const jwt = getJWTAutCookie({
297
- headers: request.headers,
298
- account: discoveryConfig.api.storeId,
299
- })
300
-
301
- const shouldRefreshToken = shouldForceRefreshTokenForValidateSession({
302
- jwt,
303
- sessionRefreshAfter: variables?.session?.refreshAfter,
304
- })
305
-
306
- if (shouldRefreshToken) {
307
- throw new UnauthorizedError(
308
- 'Unauthorized: Token expired. Please login again or refresh the page.'
309
- )
310
- }
311
- }
372
+ assertValidateSessionTokenIsFresh({
373
+ request,
374
+ operationName: operation.__meta__.operationName,
375
+ sessionRefreshAfter: variables?.session?.refreshAfter,
376
+ })
312
377
 
313
378
  // Prevents to call ValidateSession or ValidateCartMutation without session (required) and get GraphQLError
314
379
  const doNotRun =
@@ -338,39 +403,15 @@ const handler: NextApiHandler = async (request, response) => {
338
403
 
339
404
  const hasAuthCookie = hasVtexIdclientAutCookie(request)
340
405
 
341
- if (extensions.cacheControl) {
342
- const cacheControl = stringifyCacheControl(
343
- extensions.cacheControl,
344
- hasAuthCookie
345
- )
346
- response.setHeader('cache-control', cacheControl)
347
- } else if (
348
- request.method === 'GET' &&
349
- operation.__meta__.operationName?.toLowerCase()?.endsWith('query')
350
- ) {
351
- const maxAge =
352
- discoveryConfig?.experimental?.graphqlCacheControl?.maxAge &&
353
- discoveryConfig?.experimental?.graphqlCacheControl?.maxAge > 0
354
- ? discoveryConfig.experimental.graphqlCacheControl.maxAge
355
- : DEFAULT_MAX_AGE // 5 minutes
356
-
357
- const staleWhileRevalidate =
358
- discoveryConfig?.experimental?.graphqlCacheControl
359
- ?.staleWhileRevalidate &&
360
- discoveryConfig?.experimental?.graphqlCacheControl
361
- ?.staleWhileRevalidate > 0
362
- ? discoveryConfig.experimental.graphqlCacheControl
363
- .staleWhileRevalidate
364
- : DEFAULT_STALE_WHILE_REVALIDATE // 1 hour
365
-
366
- const scope = hasAuthCookie ? 'private' : 'public'
367
- response.setHeader(
368
- 'cache-control',
369
- `${scope}, s-maxage=${maxAge}, stale-while-revalidate=${staleWhileRevalidate}`
370
- )
371
- } else {
372
- response.setHeader('cache-control', 'no-cache, no-store')
373
- }
406
+ response.setHeader(
407
+ 'cache-control',
408
+ getSuccessCacheControl({
409
+ request,
410
+ operationName: operation.__meta__.operationName,
411
+ cacheControl: extensions.cacheControl,
412
+ hasAuthCookie,
413
+ })
414
+ )
374
415
 
375
416
  const setCookieValues = Array.from(extensions.cookies.values())
376
417
  if (setCookieValues.length > 0 && !hasErrors) {
@@ -24,8 +24,7 @@ import PageProvider from 'src/sdk/overrides/PageProvider'
24
24
  import { execute } from 'src/server'
25
25
  import { injectGlobalSections } from 'src/server/cms/global'
26
26
  import { fetchMyAccountPageContent } from 'src/server/cms/fetchMyAccountPageContent'
27
- import { getRequestHostname } from 'src/utils/getRequestHostname'
28
- import { isLocalHost } from 'src/utils/isLocalHost'
27
+ import { shouldBypassRefreshTokenForRequest } from 'src/utils/shouldBypassRefreshToken'
29
28
  import { withLocaleValidationSSR } from 'src/utils/localization/withLocaleValidation'
30
29
  import { getMyAccountRedirect } from 'src/utils/myAccountRedirect'
31
30
 
@@ -137,14 +136,16 @@ const getServerSidePropsBase: GetServerSideProps<
137
136
 
138
137
  const statusCode: number = (account.errors[0] as any)?.extensions?.status
139
138
 
140
- const isLocal = isLocalHost(getRequestHostname(context.req.headers.host))
139
+ const bypassRefreshToken = shouldBypassRefreshTokenForRequest(
140
+ context.req.headers
141
+ )
141
142
 
142
143
  return {
143
144
  props: {
144
145
  globalSections: globalSectionsResult,
145
146
  pageSections: pageContent.sections,
146
147
  needsRefreshToken:
147
- !isLocal &&
148
+ !bypassRefreshToken &&
148
149
  (statusCode === 401 || statusCode === 403) &&
149
150
  storeConfig.experimental?.refreshToken,
150
151
  fromPage,
@@ -21,7 +21,7 @@ import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
21
21
  import type { AccountCardsPageData } from 'src/sdk/account/accountPageContext'
22
22
  import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
23
23
  import { getB2BSessionClaims } from 'src/sdk/account/getB2BSessionClaims'
24
- import { getIsRepresentative } from 'src/sdk/account/getIsRepresentative'
24
+ import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
25
25
  import { execute } from 'src/server'
26
26
  import { injectGlobalSections } from 'src/server/cms/global'
27
27
  import { extractAccountNavigationData } from 'src/server/cms/myAccountDefaultSections'
@@ -72,7 +72,7 @@ export default function ListCardsPage({
72
72
  navigationLabels,
73
73
  accountPageData,
74
74
  accountName,
75
- isRepresentative,
75
+ isB2BBuyer,
76
76
  }: ListCardsPageProps) {
77
77
  const { sections: globalSections, settings: globalSettings } =
78
78
  globalSectionsProp ?? {}
@@ -89,7 +89,7 @@ export default function ListCardsPage({
89
89
  <NextSeo noindex nofollow />
90
90
 
91
91
  <Layout
92
- isRepresentative={isRepresentative}
92
+ isB2BBuyer={isB2BBuyer}
93
93
  accountName={accountName}
94
94
  navigationLabels={navigationLabels}
95
95
  >
@@ -109,7 +109,7 @@ const getServerSidePropsBase: GetServerSideProps<
109
109
  Record<string, string>,
110
110
  Locator
111
111
  > = async (context) => {
112
- const isRepresentative = getIsRepresentative({
112
+ const isB2BBuyer = getIsB2BBuyer({
113
113
  headers: context.req.headers as Record<string, string>,
114
114
  account: storeConfig.api.storeId,
115
115
  })
@@ -249,7 +249,7 @@ const getServerSidePropsBase: GetServerSideProps<
249
249
  hasError,
250
250
  },
251
251
  pageSections,
252
- isRepresentative,
252
+ isB2BBuyer,
253
253
  },
254
254
  }
255
255
  }
@@ -23,7 +23,7 @@ import storeConfig from 'discovery.config'
23
23
  import { getGlobalSectionsData } from 'src/components/cms/GlobalSections'
24
24
  import { default as AfterSection } from 'src/customizations/src/myAccount/extensions/orders/[id]/after'
25
25
  import { default as BeforeSection } from 'src/customizations/src/myAccount/extensions/orders/[id]/before'
26
- import { getIsRepresentative } from 'src/sdk/account/getIsRepresentative'
26
+ import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
27
27
  import PageProvider from 'src/sdk/overrides/PageProvider'
28
28
  import { execute } from 'src/server'
29
29
  import { injectGlobalSections } from 'src/server/cms/global'
@@ -51,7 +51,7 @@ export default function OrderDetailsPage({
51
51
  navigationLabels,
52
52
  accountPageData,
53
53
  accountName,
54
- isRepresentative,
54
+ isB2BBuyer,
55
55
  }: OrderDetailsPageProps) {
56
56
  const { sections: globalSections, settings: globalSettings } =
57
57
  globalSectionsProp ?? {}
@@ -68,7 +68,7 @@ export default function OrderDetailsPage({
68
68
  <NextSeo noindex nofollow />
69
69
 
70
70
  <Layout
71
- isRepresentative={isRepresentative}
71
+ isB2BBuyer={isB2BBuyer}
72
72
  accountName={accountName}
73
73
  navigationLabels={navigationLabels}
74
74
  >
@@ -313,7 +313,7 @@ const getServerSidePropsBase: GetServerSideProps<
313
313
  Record<string, string>,
314
314
  Locator
315
315
  > = async (context) => {
316
- const isRepresentative = getIsRepresentative({
316
+ const isB2BBuyer = getIsB2BBuyer({
317
317
  headers: context.req.headers as Record<string, string>,
318
318
  account: storeConfig.api.storeId,
319
319
  })
@@ -415,7 +415,7 @@ const getServerSidePropsBase: GetServerSideProps<
415
415
  ...(orderStatusLabels ? { orderStatusLabels } : {}),
416
416
  },
417
417
  pageSections,
418
- isRepresentative,
418
+ isB2BBuyer,
419
419
  },
420
420
  }
421
421
  }
@@ -22,7 +22,7 @@ import { default as BeforeSection } from 'src/customizations/src/myAccount/exten
22
22
  import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
23
23
  import type { AccountOrdersListPageData } from 'src/sdk/account/accountPageContext'
24
24
  import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
25
- import { getIsRepresentative } from 'src/sdk/account/getIsRepresentative'
25
+ import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
26
26
  import { execute } from 'src/server'
27
27
  import { injectGlobalSections } from 'src/server/cms/global'
28
28
  import { extractAccountNavigationData } from 'src/server/cms/myAccountDefaultSections'
@@ -52,7 +52,7 @@ export default function ListOrdersPage({
52
52
  navigationLabels,
53
53
  accountPageData,
54
54
  accountName,
55
- isRepresentative,
55
+ isB2BBuyer,
56
56
  }: ListOrdersPageProps) {
57
57
  const { sections: globalSections, settings: globalSettings } =
58
58
  globalSectionsProp ?? {}
@@ -69,7 +69,7 @@ export default function ListOrdersPage({
69
69
  <NextSeo noindex nofollow />
70
70
 
71
71
  <Layout
72
- isRepresentative={isRepresentative}
72
+ isB2BBuyer={isB2BBuyer}
73
73
  accountName={accountName}
74
74
  navigationLabels={navigationLabels}
75
75
  >
@@ -132,7 +132,7 @@ const getServerSidePropsBase: GetServerSideProps<
132
132
  Record<string, string>,
133
133
  Locator
134
134
  > = async (context) => {
135
- const isRepresentative = getIsRepresentative({
135
+ const isB2BBuyer = getIsB2BBuyer({
136
136
  headers: context.req.headers as Record<string, string>,
137
137
  account: storeConfig.api.storeId,
138
138
  })
@@ -265,7 +265,7 @@ const getServerSidePropsBase: GetServerSideProps<
265
265
  },
266
266
  },
267
267
  pageSections,
268
- isRepresentative,
268
+ isB2BBuyer,
269
269
  },
270
270
  }
271
271
  }
@@ -22,7 +22,7 @@ import { default as BeforeSection } from 'src/customizations/src/myAccount/exten
22
22
  import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
23
23
  import type { AccountProfilePageData } from 'src/sdk/account/accountPageContext'
24
24
  import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
25
- import { getIsRepresentative } from 'src/sdk/account/getIsRepresentative'
25
+ import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
26
26
  import { injectGlobalSections } from 'src/server/cms/global'
27
27
  import { extractAccountNavigationData } from 'src/server/cms/myAccountDefaultSections'
28
28
  import { fetchMyAccountPageContent } from 'src/server/cms/fetchMyAccountPageContent'
@@ -51,7 +51,7 @@ export default function Profile({
51
51
  navigationLabels,
52
52
  accountPageData,
53
53
  accountName,
54
- isRepresentative,
54
+ isB2BBuyer,
55
55
  }: ProfilePageProps) {
56
56
  const { sections: globalSections, settings: globalSettings } =
57
57
  globalSectionsProp ?? {}
@@ -68,7 +68,7 @@ export default function Profile({
68
68
  <NextSeo noindex nofollow />
69
69
 
70
70
  <Layout
71
- isRepresentative={isRepresentative}
71
+ isB2BBuyer={isB2BBuyer}
72
72
  accountName={accountName}
73
73
  navigationLabels={navigationLabels}
74
74
  >
@@ -108,7 +108,7 @@ const getServerSidePropsBase: GetServerSideProps<
108
108
  return { redirect }
109
109
  }
110
110
 
111
- const isRepresentative = getIsRepresentative({
111
+ const isB2BBuyer = getIsB2BBuyer({
112
112
  headers: context.req.headers as Record<string, string>,
113
113
  account: storeConfig.api.storeId,
114
114
  })
@@ -184,7 +184,7 @@ const getServerSidePropsBase: GetServerSideProps<
184
184
  },
185
185
  pageSections,
186
186
  navigationLabels: navigationData as AccountNavigationLabels,
187
- isRepresentative,
187
+ isB2BBuyer,
188
188
  },
189
189
  }
190
190
  }