@faststore/core 3.99.6 → 3.100.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.
Files changed (106) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +62 -55
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/config.json +3 -3
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  9. package/.next/cache/webpack/server-production/0.pack +0 -0
  10. package/.next/cache/webpack/server-production/index.pack +0 -0
  11. package/.next/prerender-manifest.js +1 -1
  12. package/.next/prerender-manifest.json +1 -1
  13. package/.next/react-loadable-manifest.json +6 -6
  14. package/.next/routes-manifest.json +1 -1
  15. package/.next/server/chunks/3779.js +1 -0
  16. package/.next/server/chunks/7098.js +2 -2
  17. package/.next/server/chunks/83.js +1 -1
  18. package/.next/server/chunks/948.js +3 -3
  19. package/.next/server/chunks/{9984.js → UINavbarSlider.js} +1 -1
  20. package/.next/server/edge-runtime-webpack.js +24 -0
  21. package/.next/server/edge-runtime-webpack.js.map +1 -1
  22. package/.next/server/instrumentation.js.nft.json +1 -1
  23. package/.next/server/middleware-build-manifest.js +1 -1
  24. package/.next/server/middleware-manifest.json +27 -2
  25. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  26. package/.next/server/pages/404.js +1 -1
  27. package/.next/server/pages/404.js.nft.json +1 -1
  28. package/.next/server/pages/500.js +1 -1
  29. package/.next/server/pages/500.js.nft.json +1 -1
  30. package/.next/server/pages/[...slug].js +1 -1
  31. package/.next/server/pages/[...slug].js.nft.json +1 -1
  32. package/.next/server/pages/[slug]/p.js +1 -1
  33. package/.next/server/pages/[slug]/p.js.nft.json +1 -1
  34. package/.next/server/pages/_app.js.nft.json +1 -1
  35. package/.next/server/pages/_document.js.nft.json +1 -1
  36. package/.next/server/pages/_error.js.nft.json +1 -1
  37. package/.next/server/pages/api/fs/auth/login.js +1 -0
  38. package/.next/server/pages/api/fs/auth/login.js.nft.json +1 -0
  39. package/.next/server/pages/api/fs/logout.js.nft.json +1 -1
  40. package/.next/server/pages/api/graphql.js.nft.json +1 -1
  41. package/.next/server/pages/api/health/live.js.nft.json +1 -1
  42. package/.next/server/pages/api/health/ready.js.nft.json +1 -1
  43. package/.next/server/pages/api/preview.js.nft.json +1 -1
  44. package/.next/server/pages/checkout.js +1 -1
  45. package/.next/server/pages/checkout.js.nft.json +1 -1
  46. package/.next/server/pages/en-US/404.html +2 -2
  47. package/.next/server/pages/en-US/500.html +2 -2
  48. package/.next/server/pages/en-US/checkout.html +2 -2
  49. package/.next/server/pages/en-US/fs-auth-login.html +42 -0
  50. package/.next/server/pages/en-US/login.html +2 -2
  51. package/.next/server/pages/en-US/s.html +2 -2
  52. package/.next/server/pages/en-US.html +2 -2
  53. package/.next/server/pages/fs-auth-login.js.nft.json +1 -0
  54. package/.next/server/pages/index.js +1 -1
  55. package/.next/server/pages/index.js.nft.json +1 -1
  56. package/.next/server/pages/login.js +1 -1
  57. package/.next/server/pages/login.js.nft.json +1 -1
  58. package/.next/server/pages/pvt/account/403.js +1 -1
  59. package/.next/server/pages/pvt/account/403.js.nft.json +1 -1
  60. package/.next/server/pages/pvt/account/404.js +1 -1
  61. package/.next/server/pages/pvt/account/404.js.nft.json +1 -1
  62. package/.next/server/pages/pvt/account/[...unknown].js.nft.json +1 -1
  63. package/.next/server/pages/pvt/account/orders/[id].js +1 -1
  64. package/.next/server/pages/pvt/account/orders/[id].js.nft.json +1 -1
  65. package/.next/server/pages/pvt/account/orders.js +1 -1
  66. package/.next/server/pages/pvt/account/orders.js.nft.json +1 -1
  67. package/.next/server/pages/pvt/account/profile.js +1 -1
  68. package/.next/server/pages/pvt/account/profile.js.nft.json +1 -1
  69. package/.next/server/pages/pvt/account/security.js +1 -1
  70. package/.next/server/pages/pvt/account/security.js.nft.json +1 -1
  71. package/.next/server/pages/pvt/account/user-details.js +1 -1
  72. package/.next/server/pages/pvt/account/user-details.js.nft.json +1 -1
  73. package/.next/server/pages/pvt/account.js.nft.json +1 -1
  74. package/.next/server/pages/s.js +1 -1
  75. package/.next/server/pages/s.js.nft.json +1 -1
  76. package/.next/server/pages-manifest.json +1 -1
  77. package/.next/server/src/middleware.js +5417 -0
  78. package/.next/server/src/middleware.js.map +1 -0
  79. package/.next/server/webpack-runtime.js +1 -1
  80. package/.next/static/Lk9fl1r-H5eeVzeqYkA-Y/_buildManifest.js +1 -0
  81. package/.next/static/chunks/{2927.5a79877943a6bf7c.js → 2927.23bae2c79f0ac0f3.js} +1 -1
  82. package/.next/static/chunks/{9173-515913f3f10caf5e.js → 9173-1670c2bce097f620.js} +1 -1
  83. package/.next/static/chunks/{UIToast.19a8664c01a00d3a.js → UIToast.de15325248043ce5.js} +1 -1
  84. package/.next/static/chunks/main-083a618181bb5fbb.js +1 -0
  85. package/.next/static/chunks/pages/fs-auth-login-ba2d1b3ef0ef0415.js +1 -0
  86. package/.next/static/chunks/webpack-ff85816fb58111fa.js +1 -0
  87. package/.next/static/css/3bd4edd425711792.css +1 -0
  88. package/.next/trace +156 -143
  89. package/.turbo/turbo-build.log +44 -19
  90. package/.turbo/turbo-test.log +11 -10
  91. package/CHANGELOG.md +12 -0
  92. package/package.json +3 -2
  93. package/src/components/ui/Image/loader.ts +2 -2
  94. package/src/middleware.ts +87 -0
  95. package/src/pages/api/fs/auth/login.ts +123 -0
  96. package/src/pages/fs-auth-login/fs-auth-login.module.scss +41 -0
  97. package/src/pages/fs-auth-login/index.tsx +90 -0
  98. package/src/server/authentication-service.ts +270 -0
  99. package/src/server/password-protection/webops-api.ts +38 -0
  100. package/src/utils/loginResponse.ts +20 -0
  101. package/test/server/authentication-service.test.ts +460 -0
  102. package/.next/static/UmIFhJcdMOfy3xrHNNZS2/_buildManifest.js +0 -1
  103. package/.next/static/chunks/main-ec03882c4375091d.js +0 -1
  104. package/.next/static/chunks/webpack-65b1de5f075b8367.js +0 -1
  105. package/src/middleware__DISABLED.ts +0 -65
  106. /package/.next/static/{UmIFhJcdMOfy3xrHNNZS2 → Lk9fl1r-H5eeVzeqYkA-Y}/_ssgManifest.js +0 -0
@@ -0,0 +1,270 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { NextRequest } from 'next/server'
3
+ import { jwtVerify, decodeJwt, importSPKI, errors } from 'jose'
4
+ import type { CryptoKey } from 'jose'
5
+
6
+ import storeConfig from '../../discovery.config'
7
+
8
+ import {
9
+ publicKeyUrl,
10
+ renewUrl,
11
+ protectionStatusUrl,
12
+ passwordProtectionTimeouts,
13
+ } from './password-protection/webops-api'
14
+
15
+ export const COOKIE_NAME = '__fs_auth_token'
16
+ export const TOKEN_TTL_SECONDS = 10 * 60
17
+ /** How long to reuse the RSA public key fetched from WebOps */
18
+ const PUBLIC_KEY_CACHE_MS = 60 * 60 * 1000
19
+
20
+ interface TokenPayload {
21
+ storeId: string
22
+ protected: boolean
23
+ passwordVersion?: string
24
+ scope?: string
25
+ exp?: number
26
+ }
27
+
28
+ interface AuthResult {
29
+ response: NextResponse
30
+ }
31
+
32
+ let publicKeyCache: {
33
+ pem: string
34
+ key: CryptoKey
35
+ fetchedAt: number
36
+ } | null = null
37
+
38
+ async function fetchPublicKeyPemFromWebOps(): Promise<string> {
39
+ const res = await fetch(publicKeyUrl(), {
40
+ signal: AbortSignal.timeout(passwordProtectionTimeouts.publicKeyMs),
41
+ })
42
+
43
+ if (!res.ok) {
44
+ throw new Error(`WebOps public-key returned ${res.status}`)
45
+ }
46
+
47
+ const body = (await res.json()) as { publicKey?: string }
48
+ const pem = body.publicKey?.trim()
49
+
50
+ if (!pem) {
51
+ throw new Error('WebOps public-key response missing publicKey')
52
+ }
53
+
54
+ return pem
55
+ }
56
+
57
+ async function getPublicVerificationKey(): Promise<CryptoKey> {
58
+ const now = Date.now()
59
+
60
+ if (publicKeyCache && now - publicKeyCache.fetchedAt < PUBLIC_KEY_CACHE_MS) {
61
+ return publicKeyCache.key
62
+ }
63
+
64
+ const pem = await fetchPublicKeyPemFromWebOps()
65
+ const key = await importSPKI(pem, 'RS256')
66
+ publicKeyCache = { pem, key, fetchedAt: now }
67
+
68
+ return key
69
+ }
70
+
71
+ /** Clears the cached WebOps public key (for unit tests only). */
72
+ export function resetPasswordProtectionPublicKeyCacheForTests(): void {
73
+ publicKeyCache = null
74
+ }
75
+
76
+ export class AuthenticationService {
77
+ private readonly storeId: string
78
+
79
+ constructor() {
80
+ this.storeId = storeConfig.api.storeId
81
+ }
82
+
83
+ private getNormalizedHost(request: NextRequest): string {
84
+ const host = request.headers.get('host') || ''
85
+ return host.split(':')[0].toLowerCase()
86
+ }
87
+
88
+ async authenticateRequest(request: NextRequest): Promise<AuthResult> {
89
+ if (!this.shouldCheckProtection(request)) {
90
+ return { response: NextResponse.next() }
91
+ }
92
+
93
+ const token = request.cookies.get(COOKIE_NAME)?.value
94
+
95
+ if (token) {
96
+ const verification = await this.verifyToken(token)
97
+
98
+ if (verification.valid && verification.payload) {
99
+ return { response: NextResponse.next() }
100
+ }
101
+
102
+ if (verification.expired && verification.payload) {
103
+ return await this.tryRenewSession(request, token, verification.payload)
104
+ }
105
+ }
106
+
107
+ return await this.handleNoAuth(request)
108
+ }
109
+
110
+ private shouldCheckProtection(request: NextRequest): boolean {
111
+ if (process.env.NODE_ENV === 'development') {
112
+ return false
113
+ }
114
+
115
+ const hostname = this.getNormalizedHost(request)
116
+ const isDefaultDomain = hostname.endsWith('.vtex.app')
117
+
118
+ if (isDefaultDomain) {
119
+ return true
120
+ }
121
+
122
+ return process.env.CUSTOM_DOMAINS_PROTECTION_ENABLED === 'true'
123
+ }
124
+
125
+ private shouldProtectDomain(request: NextRequest, scope?: string): boolean {
126
+ const hostname = this.getNormalizedHost(request)
127
+ const isDefaultDomain = hostname.endsWith('.vtex.app')
128
+
129
+ return (
130
+ scope === 'ALL_DOMAINS' ||
131
+ (scope === 'DEFAULT_DOMAINS' && isDefaultDomain) ||
132
+ (scope === 'CUSTOM_DOMAINS' && !isDefaultDomain)
133
+ )
134
+ }
135
+
136
+ private payloadMatchesStore(payload: TokenPayload): boolean {
137
+ return payload.storeId === this.storeId
138
+ }
139
+
140
+ private isJwtExpiredError(error: unknown): boolean {
141
+ return (
142
+ error instanceof errors.JWTExpired ||
143
+ (error instanceof Error && error.name === 'JWTExpired')
144
+ )
145
+ }
146
+
147
+ private async verifyToken(token: string): Promise<{
148
+ valid: boolean
149
+ expired: boolean
150
+ payload?: TokenPayload
151
+ }> {
152
+ try {
153
+ const key = await getPublicVerificationKey()
154
+ const { payload } = await jwtVerify(token, key)
155
+ const p = payload as unknown as TokenPayload
156
+
157
+ if (!this.payloadMatchesStore(p)) {
158
+ return { valid: false, expired: false }
159
+ }
160
+
161
+ return {
162
+ valid: true,
163
+ expired: false,
164
+ payload: p,
165
+ }
166
+ } catch (error) {
167
+ if (this.isJwtExpiredError(error)) {
168
+ const payload = decodeJwt(token) as unknown as TokenPayload
169
+
170
+ if (!this.payloadMatchesStore(payload)) {
171
+ return { valid: false, expired: false }
172
+ }
173
+
174
+ return { valid: false, expired: true, payload }
175
+ }
176
+
177
+ return { valid: false, expired: false }
178
+ }
179
+ }
180
+
181
+ private async tryRenewSession(
182
+ request: NextRequest,
183
+ expiredToken: string,
184
+ payload: TokenPayload
185
+ ): Promise<AuthResult> {
186
+ try {
187
+ const res = await fetch(renewUrl(), {
188
+ method: 'POST',
189
+ headers: { 'Content-Type': 'application/json' },
190
+ body: JSON.stringify({
191
+ storeId: this.storeId,
192
+ expiredToken,
193
+ }),
194
+ signal: AbortSignal.timeout(passwordProtectionTimeouts.defaultMs),
195
+ })
196
+
197
+ if (res.ok) {
198
+ const data = await res.json()
199
+
200
+ if (data.valid && data.token) {
201
+ const response = NextResponse.next()
202
+ this.setAuthCookie(response, data.token)
203
+
204
+ return { response }
205
+ }
206
+ }
207
+ } catch {
208
+ // Fall through to redirect or fail-open below
209
+ }
210
+
211
+ if (!this.shouldProtectDomain(request, payload.scope)) {
212
+ return { response: NextResponse.next() }
213
+ }
214
+
215
+ return { response: this.redirectToLogin(request) }
216
+ }
217
+
218
+ private async handleNoAuth(request: NextRequest): Promise<AuthResult> {
219
+ try {
220
+ const res = await fetch(protectionStatusUrl(), {
221
+ signal: AbortSignal.timeout(passwordProtectionTimeouts.defaultMs),
222
+ })
223
+
224
+ if (!res.ok) {
225
+ return { response: this.redirectToLogin(request) }
226
+ }
227
+
228
+ const status = await res.json()
229
+
230
+ if (!status.protected) {
231
+ const response = NextResponse.next()
232
+
233
+ if (status.token) {
234
+ this.setAuthCookie(response, status.token)
235
+ }
236
+
237
+ return { response }
238
+ }
239
+
240
+ if (!this.shouldProtectDomain(request, status.scope)) {
241
+ return { response: NextResponse.next() }
242
+ }
243
+
244
+ return { response: this.redirectToLogin(request) }
245
+ } catch {
246
+ return { response: this.redirectToLogin(request) }
247
+ }
248
+ }
249
+
250
+ private setAuthCookie(response: NextResponse, token: string): void {
251
+ response.cookies.set(COOKIE_NAME, token, {
252
+ httpOnly: true,
253
+ secure: true,
254
+ sameSite: 'lax',
255
+ path: '/',
256
+ maxAge: TOKEN_TTL_SECONDS,
257
+ })
258
+ }
259
+
260
+ private redirectToLogin(request: NextRequest): NextResponse {
261
+ const loginUrl = new URL('/fs-auth-login', request.url)
262
+ const returnTo = `${request.nextUrl.pathname}${request.nextUrl.search}`
263
+ loginUrl.searchParams.set('returnTo', returnTo)
264
+
265
+ const response = NextResponse.redirect(loginUrl)
266
+ response.headers.set('Cache-Control', 'no-store')
267
+
268
+ return response
269
+ }
270
+ }
@@ -0,0 +1,38 @@
1
+ import discoveryConfig from '../../../discovery.config'
2
+
3
+ const DEFAULT_WEBOPS_ORIGIN = 'https://faststore.vtex.com'
4
+
5
+ function getWebopsOrigin(): string {
6
+ const hostFromEnv = process.env.WEBOPS_API_URL?.trim() ?? ''
7
+ const hostWithoutTrailingSlash = hostFromEnv.replace(/\/+$/, '')
8
+ const origin =
9
+ hostWithoutTrailingSlash.length > 0
10
+ ? hostWithoutTrailingSlash
11
+ : DEFAULT_WEBOPS_ORIGIN
12
+
13
+ return /^https?:\/\//i.test(origin) ? origin : `https://${origin}`
14
+ }
15
+
16
+ export function publicKeyUrl(): URL {
17
+ return new URL('/api/v1/password-protection/public-key', getWebopsOrigin())
18
+ }
19
+
20
+ export function protectionStatusUrl(): URL {
21
+ const url = new URL('/api/v1/password-protection/status', getWebopsOrigin())
22
+ url.searchParams.set('storeId', discoveryConfig.api.storeId)
23
+ return url
24
+ }
25
+
26
+ export function sessionUrl(): URL {
27
+ return new URL('/api/v1/password-protection/session', getWebopsOrigin())
28
+ }
29
+
30
+ export function renewUrl(): URL {
31
+ return new URL('/api/v1/password-protection/renew', getWebopsOrigin())
32
+ }
33
+
34
+ /** Timeouts for WebOps password-protection calls (middleware / API routes). */
35
+ export const passwordProtectionTimeouts = {
36
+ publicKeyMs: 5_000,
37
+ defaultMs: 10_000,
38
+ } as const
@@ -0,0 +1,20 @@
1
+ export interface LoginResponse {
2
+ success?: boolean
3
+ redirectUrl?: string
4
+ error?: string
5
+ }
6
+
7
+ export const isLoginResponse = (data: unknown): data is LoginResponse => {
8
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
9
+ return false
10
+ }
11
+
12
+ const payload = data as Record<string, unknown>
13
+
14
+ return (
15
+ (payload.success === undefined || typeof payload.success === 'boolean') &&
16
+ (payload.redirectUrl === undefined ||
17
+ typeof payload.redirectUrl === 'string') &&
18
+ (payload.error === undefined || typeof payload.error === 'string')
19
+ )
20
+ }