@faststore/core 3.99.4 → 3.100.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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +49 -42
- 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/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-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/routes-manifest.json +1 -1
- package/.next/server/chunks/3779.js +1 -0
- package/.next/server/chunks/3890.js +1 -1
- package/.next/server/chunks/7098.js +2 -2
- package/.next/server/chunks/83.js +1 -1
- package/.next/server/chunks/948.js +3 -3
- package/.next/server/chunks/9563.js +1 -1
- package/.next/server/chunks/{9984.js → UINavbarSlider.js} +1 -1
- package/.next/server/edge-runtime-webpack.js +24 -0
- package/.next/server/edge-runtime-webpack.js.map +1 -1
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +27 -2
- package/.next/server/pages/404.js +1 -1
- package/.next/server/pages/404.js.nft.json +1 -1
- package/.next/server/pages/500.js +1 -1
- package/.next/server/pages/500.js.nft.json +1 -1
- package/.next/server/pages/[...slug].js +1 -1
- package/.next/server/pages/[...slug].js.nft.json +1 -1
- package/.next/server/pages/[slug]/p.js +1 -1
- package/.next/server/pages/[slug]/p.js.nft.json +1 -1
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js.nft.json +1 -1
- package/.next/server/pages/_error.js.nft.json +1 -1
- package/.next/server/pages/api/fs/auth/login.js +1 -0
- package/.next/server/pages/api/fs/auth/login.js.nft.json +1 -0
- package/.next/server/pages/api/fs/logout.js.nft.json +1 -1
- package/.next/server/pages/api/graphql.js +1 -1
- package/.next/server/pages/api/graphql.js.nft.json +1 -1
- package/.next/server/pages/api/health/live.js.nft.json +1 -1
- package/.next/server/pages/api/health/ready.js.nft.json +1 -1
- package/.next/server/pages/api/preview.js.nft.json +1 -1
- package/.next/server/pages/checkout.js +1 -1
- package/.next/server/pages/checkout.js.nft.json +1 -1
- package/.next/server/pages/en-US/404.html +2 -2
- package/.next/server/pages/en-US/500.html +2 -2
- package/.next/server/pages/en-US/checkout.html +2 -2
- package/.next/server/pages/en-US/fs-auth-login.html +42 -0
- package/.next/server/pages/en-US/login.html +2 -2
- package/.next/server/pages/en-US/s.html +2 -2
- package/.next/server/pages/en-US.html +2 -2
- package/.next/server/pages/fs-auth-login.js.nft.json +1 -0
- package/.next/server/pages/index.js +1 -1
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/login.js +1 -1
- package/.next/server/pages/login.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/403.js +1 -1
- package/.next/server/pages/pvt/account/403.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/404.js +1 -1
- package/.next/server/pages/pvt/account/404.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/[...unknown].js.nft.json +1 -1
- package/.next/server/pages/pvt/account/orders/[id].js +1 -1
- package/.next/server/pages/pvt/account/orders/[id].js.nft.json +1 -1
- package/.next/server/pages/pvt/account/orders.js +1 -1
- package/.next/server/pages/pvt/account/orders.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/profile.js +1 -1
- package/.next/server/pages/pvt/account/profile.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/security.js +1 -1
- package/.next/server/pages/pvt/account/security.js.nft.json +1 -1
- package/.next/server/pages/pvt/account/user-details.js +1 -1
- package/.next/server/pages/pvt/account/user-details.js.nft.json +1 -1
- package/.next/server/pages/pvt/account.js.nft.json +1 -1
- package/.next/server/pages/s.js +1 -1
- package/.next/server/pages/s.js.nft.json +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/server/src/middleware.js +5417 -0
- package/.next/server/src/middleware.js.map +1 -0
- package/.next/server/webpack-runtime.js +1 -1
- package/.next/static/9kMY_8ze-x7sawSXOfQGe/_buildManifest.js +1 -0
- package/.next/static/chunks/main-083a618181bb5fbb.js +1 -0
- package/.next/static/chunks/pages/{_app-84aaff8a9d4de60e.js → _app-35e8dda638ee2672.js} +1 -1
- package/.next/static/chunks/pages/fs-auth-login-ba2d1b3ef0ef0415.js +1 -0
- package/.next/static/chunks/pages/pvt/account/403-ccddb4364ef27179.js +1 -0
- package/.next/static/chunks/webpack-ff85816fb58111fa.js +1 -0
- package/.next/static/css/3bd4edd425711792.css +1 -0
- package/.next/trace +156 -143
- package/.turbo/turbo-build.log +46 -21
- package/.turbo/turbo-test.log +11 -8
- package/CHANGELOG.md +12 -0
- package/package.json +3 -2
- package/src/middleware.ts +87 -0
- package/src/pages/api/fs/auth/login.ts +123 -0
- package/src/pages/api/graphql.ts +2 -2
- package/src/pages/fs-auth-login/fs-auth-login.module.scss +41 -0
- package/src/pages/fs-auth-login/index.tsx +90 -0
- package/src/pages/pvt/account/403.tsx +9 -0
- package/src/sdk/account/useRefreshToken.ts +12 -0
- package/src/sdk/session/index.ts +3 -7
- package/src/server/authentication-service.ts +270 -0
- package/src/server/password-protection/webops-api.ts +38 -0
- package/src/utils/isLocalHost.ts +33 -0
- package/src/utils/loginResponse.ts +20 -0
- package/test/server/authentication-service.test.ts +460 -0
- package/test/utils/isLocalHost.browser.test.ts +40 -0
- package/test/utils/isLocalHost.test.ts +38 -0
- package/.next/static/Xk7WgpVOdfEoXARPhQHxf/_buildManifest.js +0 -1
- package/.next/static/chunks/main-ec03882c4375091d.js +0 -1
- package/.next/static/chunks/pages/pvt/account/403-491e6b062487b30f.js +0 -1
- package/.next/static/chunks/webpack-61b283a96fbb5ee2.js +0 -1
- package/src/middleware__DISABLED.ts +0 -65
- /package/.next/static/{Xk7WgpVOdfEoXARPhQHxf → 9kMY_8ze-x7sawSXOfQGe}/_ssgManifest.js +0 -0
package/src/sdk/session/index.ts
CHANGED
|
@@ -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 storeConfig from '../../../discovery.config'
|
|
12
13
|
import {
|
|
13
14
|
isRefreshTokenSuccessful,
|
|
@@ -19,11 +20,6 @@ import { createValidationStore, useStore } from '../useStore'
|
|
|
19
20
|
import { getPostalCode } from '../userLocation/index'
|
|
20
21
|
import { RELOAD_AFTER_LOGOUT_KEY, SESSION_READY_KEY } from './storageKeys'
|
|
21
22
|
|
|
22
|
-
const isLocalEnvironment = (): boolean =>
|
|
23
|
-
typeof window !== 'undefined' &&
|
|
24
|
-
(window.location.hostname === 'localhost' ||
|
|
25
|
-
window.location.hostname === '127.0.0.1')
|
|
26
|
-
|
|
27
23
|
const isReloadAfterLogoutPending = (): boolean => {
|
|
28
24
|
try {
|
|
29
25
|
return (
|
|
@@ -127,7 +123,7 @@ export const validateSession = async (session: Session) => {
|
|
|
127
123
|
// On failure (logoutAndClearSession already triggered), bail out.
|
|
128
124
|
// Skipped in local environments where the refresh token infrastructure is unavailable.
|
|
129
125
|
if (
|
|
130
|
-
!
|
|
126
|
+
!isLocalHostBrowser() &&
|
|
131
127
|
storeConfig.experimental?.refreshToken &&
|
|
132
128
|
isRefreshAfterExpired(session)
|
|
133
129
|
) {
|
|
@@ -180,7 +176,7 @@ export const validateSession = async (session: Session) => {
|
|
|
180
176
|
return data.validateSession
|
|
181
177
|
} catch (error) {
|
|
182
178
|
const shouldRefreshToken =
|
|
183
|
-
!
|
|
179
|
+
!isLocalHostBrowser() &&
|
|
184
180
|
error?.status === 401 &&
|
|
185
181
|
storeConfig.experimental?.refreshToken
|
|
186
182
|
|
|
@@ -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,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). Callers should strip the port
|
|
19
|
+
* (e.g. `host.split(':')[0]`) before invoking 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,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
|
+
}
|