@ecomiq/storefront 2.0.12 → 2.0.14

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 (32) hide show
  1. package/bin/ecomiq.mjs +3 -1
  2. package/package.json +4 -2
  3. package/src/auth/create-storefront-auth.ts +65 -6
  4. package/src/auth/ecomiq-oauth-password-constants.ts +1 -3
  5. package/src/auth/ecomiq-oauth-password-plugin.ts +12 -17
  6. package/src/auth/ecomiq-storefront-sign-in.ts +0 -3
  7. package/src/auth/secure-cookies.ts +23 -0
  8. package/src/auth/storefront-auth-base-url.ts +117 -0
  9. package/src/codegen.mjs +27 -1
  10. package/src/components/account/customer-addresses-panel.tsx +678 -0
  11. package/src/components/account/customer-order-detail-panel.tsx +285 -0
  12. package/src/components/account/customer-orders-panel.tsx +192 -0
  13. package/src/components/account/customer-profile-panel.tsx +379 -0
  14. package/src/components/checkout/checkout-contact-section.tsx +17 -11
  15. package/src/components/checkout/checkout-session-view.tsx +83 -2
  16. package/src/components/checkout/checkout-shipping-address-section.tsx +107 -2
  17. package/src/components/checkout/checkout-steps.ts +15 -0
  18. package/src/components/checkout/map-customer-to-checkout.ts +188 -0
  19. package/src/components/checkout/use-checkout-customer-prefill.ts +409 -0
  20. package/src/components/checkout/use-checkout-session.ts +5 -4
  21. package/src/config.d.ts +4 -0
  22. package/src/config.mjs +14 -0
  23. package/src/index.ts +54 -0
  24. package/src/pages/account/account-page-skeleton.tsx +69 -0
  25. package/src/pages/account/account-page-view.tsx +270 -0
  26. package/src/pages/account/account-shell.tsx +101 -0
  27. package/src/pages/account/parse-account-route.ts +36 -0
  28. package/src/pages/account.tsx +16 -0
  29. package/src/pages/checkout.$checkoutId.tsx +1 -1
  30. package/src/providers/auth.tsx +11 -2
  31. package/src/proxy-api.ts +20 -0
  32. package/src/vite-inline.mjs +54 -1
@@ -0,0 +1,270 @@
1
+ import { useEffect, useRef, type ReactNode } from 'react'
2
+ import { useQueryClient } from '@tanstack/react-query'
3
+ import { useRouterState } from '@tanstack/react-router'
4
+ import {
5
+ createCustomerAddress,
6
+ customerMeToProfile,
7
+ deleteCustomerAddress,
8
+ getCustomerAddresses,
9
+ getCustomerMe,
10
+ getCustomerOrder,
11
+ getCustomerOrders,
12
+ patchCustomer,
13
+ toCustomerSavedAddresses,
14
+ updateCustomerAddress,
15
+ type CustomerAddressUpsertInput,
16
+ type CustomerProfilePatch,
17
+ } from '@ecomiq/core'
18
+ import { MapPinHouse, MonitorSmartphone, Package, UserRound } from 'lucide-react'
19
+ import { CustomerAddressesPanel } from '../../components/account/customer-addresses-panel'
20
+ import { CustomerOrderDetailPanel } from '../../components/account/customer-order-detail-panel'
21
+ import { CustomerOrdersPanel } from '../../components/account/customer-orders-panel'
22
+ import { CustomerProfilePanel } from '../../components/account/customer-profile-panel'
23
+ import {
24
+ useApiQuery,
25
+ useAuth,
26
+ useSearchNavigate,
27
+ useSettings,
28
+ } from '../../providers'
29
+ import { createStorefrontProxyApi } from '../../proxy-api'
30
+ import { AccountShell } from './account-shell'
31
+ import {
32
+ AccountPageSkeleton,
33
+ AccountSectionSkeleton,
34
+ } from './account-page-skeleton'
35
+ import { parseAccountRoute } from './parse-account-route'
36
+
37
+ const ACCOUNT_BASE = '/account'
38
+ const ORDERS_BASE = `${ACCOUNT_BASE}/orders`
39
+ const LOGIN_HREF = '/login'
40
+ const HOME_HREF = '/'
41
+
42
+ function proxyApi() {
43
+ return createStorefrontProxyApi()
44
+ }
45
+
46
+ export function AccountPageView() {
47
+ const pathname = useRouterState({ select: (s) => s.location.pathname })
48
+ const { section, orderId } = parseAccountRoute(pathname || ACCOUNT_BASE)
49
+ const navigate = useSearchNavigate()
50
+ const queryClient = useQueryClient()
51
+ const settings = useSettings()
52
+ const { client, session, isPending, signOut } = useAuth()
53
+ const isAuthenticated = Boolean(session?.user)
54
+ const redirectTargetRef = useRef<string | null>(null)
55
+ const language = settings.store.language || 'es-PE'
56
+
57
+ const profileQuery = useApiQuery(
58
+ ['account-customer-me'],
59
+ async () => customerMeToProfile(await getCustomerMe(proxyApi())),
60
+ {
61
+ enabled: section === 'personal' && isAuthenticated && !isPending,
62
+ freshForMs: 60_000,
63
+ },
64
+ )
65
+
66
+ const addressesQuery = useApiQuery(
67
+ ['customer-addresses'],
68
+ async () =>
69
+ toCustomerSavedAddresses(await getCustomerAddresses(proxyApi())),
70
+ {
71
+ enabled: section === 'addresses' && isAuthenticated && !isPending,
72
+ freshForMs: 30_000,
73
+ },
74
+ )
75
+
76
+ const ordersQuery = useApiQuery(
77
+ ['customer-orders'],
78
+ async () => getCustomerOrders(proxyApi()),
79
+ {
80
+ enabled:
81
+ section === 'orders' && !orderId && isAuthenticated && !isPending,
82
+ freshForMs: 30_000,
83
+ },
84
+ )
85
+
86
+ const orderDetailQuery = useApiQuery(
87
+ ['customer-order', orderId],
88
+ async () => getCustomerOrder(proxyApi(), orderId!),
89
+ {
90
+ enabled:
91
+ section === 'orders' && Boolean(orderId) && isAuthenticated && !isPending,
92
+ freshForMs: 30_000,
93
+ },
94
+ )
95
+
96
+ useEffect(() => {
97
+ if (isPending || isAuthenticated) return
98
+
99
+ const redirectTarget = redirectTargetRef.current ?? (pathname || ACCOUNT_BASE)
100
+ redirectTargetRef.current = redirectTarget
101
+
102
+ void navigate({
103
+ to: LOGIN_HREF,
104
+ search: { redirect: redirectTarget },
105
+ replace: true,
106
+ })
107
+ }, [isAuthenticated, isPending, navigate, pathname])
108
+
109
+ const handleLogout = async () => {
110
+ await signOut()
111
+ void navigate({ to: HOME_HREF, replace: true })
112
+ }
113
+
114
+ const invalidateAddresses = () =>
115
+ queryClient.invalidateQueries({ queryKey: ['customer-addresses'] })
116
+
117
+ const handleUpdateProfile = async (payload: CustomerProfilePatch) => {
118
+ await patchCustomer(proxyApi(), payload)
119
+ const displayName = `${payload.firstName} ${payload.lastName}`.trim()
120
+ try {
121
+ await client.$fetch('/update-user', {
122
+ method: 'POST',
123
+ body: {
124
+ name: displayName || payload.firstName,
125
+ firstName: payload.firstName,
126
+ lastName: payload.lastName,
127
+ phone: payload.phone,
128
+ },
129
+ })
130
+ } catch {
131
+ // La sesión Better Auth es secundaria; el PATCH upstream ya se aplicó.
132
+ }
133
+ await queryClient.invalidateQueries({ queryKey: ['account-customer-me'] })
134
+ }
135
+
136
+ const handleCreateAddress = async (payload: CustomerAddressUpsertInput) => {
137
+ await createCustomerAddress(proxyApi(), payload)
138
+ await invalidateAddresses()
139
+ }
140
+
141
+ const handleUpdateAddress = async (
142
+ addressId: string,
143
+ payload: CustomerAddressUpsertInput,
144
+ ) => {
145
+ await updateCustomerAddress(proxyApi(), addressId, payload)
146
+ await invalidateAddresses()
147
+ }
148
+
149
+ const handleDeleteAddress = async (addressId: string) => {
150
+ await deleteCustomerAddress(proxyApi(), addressId)
151
+ await invalidateAddresses()
152
+ }
153
+
154
+ const menuItems = [
155
+ {
156
+ key: 'personal',
157
+ href: ACCOUNT_BASE,
158
+ label: 'Datos personales',
159
+ icon: <UserRound className="size-4" />,
160
+ },
161
+ {
162
+ key: 'addresses',
163
+ href: `${ACCOUNT_BASE}/addresses`,
164
+ label: 'Direcciones',
165
+ icon: <MapPinHouse className="size-4" />,
166
+ },
167
+ {
168
+ key: 'orders',
169
+ href: ORDERS_BASE,
170
+ label: 'Mis pedidos',
171
+ icon: <Package className="size-4" />,
172
+ },
173
+ {
174
+ key: 'devices',
175
+ href: `${ACCOUNT_BASE}/devices`,
176
+ label: 'Dispositivos vinculados',
177
+ icon: <MonitorSmartphone className="size-4" />,
178
+ },
179
+ ]
180
+
181
+ if (isPending || !isAuthenticated) {
182
+ return <AccountPageSkeleton />
183
+ }
184
+
185
+ let sectionContent: ReactNode
186
+
187
+ switch (section) {
188
+ case 'personal':
189
+ sectionContent = (
190
+ <SectionPanel>
191
+ <CustomerProfilePanel
192
+ profile={profileQuery.data}
193
+ isLoading={profileQuery.isLoading}
194
+ errorMessage={profileQuery.error?.message}
195
+ onUpdateProfile={handleUpdateProfile}
196
+ />
197
+ </SectionPanel>
198
+ )
199
+ break
200
+ case 'addresses':
201
+ sectionContent = (
202
+ <SectionPanel>
203
+ <CustomerAddressesPanel
204
+ addresses={addressesQuery.data}
205
+ isLoading={addressesQuery.isLoading}
206
+ errorMessage={addressesQuery.error?.message}
207
+ onCreateAddress={handleCreateAddress}
208
+ onUpdateAddress={handleUpdateAddress}
209
+ onDeleteAddress={handleDeleteAddress}
210
+ />
211
+ </SectionPanel>
212
+ )
213
+ break
214
+ case 'orders':
215
+ sectionContent = (
216
+ <SectionPanel>
217
+ {orderId ? (
218
+ <CustomerOrderDetailPanel
219
+ order={orderDetailQuery.data}
220
+ isLoading={orderDetailQuery.isLoading}
221
+ errorMessage={orderDetailQuery.error?.message}
222
+ backHref={ORDERS_BASE}
223
+ language={language}
224
+ productHref={(handle) => `/products/${encodeURIComponent(handle)}`}
225
+ />
226
+ ) : (
227
+ <CustomerOrdersPanel
228
+ orders={ordersQuery.data}
229
+ isLoading={ordersQuery.isLoading}
230
+ errorMessage={ordersQuery.error?.message}
231
+ orderDetailBasePath={ORDERS_BASE}
232
+ shopHref={HOME_HREF}
233
+ language={language}
234
+ />
235
+ )}
236
+ </SectionPanel>
237
+ )
238
+ break
239
+ case 'devices':
240
+ sectionContent = (
241
+ <SectionPanel>
242
+ <h2 className="text-lg font-medium text-foreground">Dispositivos vinculados</h2>
243
+ <p className="mt-2 text-muted-foreground">
244
+ Aún no hay endpoint para listar sesiones/dispositivos.
245
+ </p>
246
+ <p className="mt-1 text-sm text-muted-foreground">
247
+ Cuando esté disponible el backend, podrás cerrar sesiones remotas desde aquí.
248
+ </p>
249
+ </SectionPanel>
250
+ )
251
+ break
252
+ default:
253
+ sectionContent = <AccountSectionSkeleton />
254
+ }
255
+
256
+ return (
257
+ <AccountShell
258
+ menuItems={menuItems}
259
+ onLogout={() => void handleLogout()}
260
+ pathname={pathname}
261
+ homeHref={HOME_HREF}
262
+ >
263
+ {sectionContent}
264
+ </AccountShell>
265
+ )
266
+ }
267
+
268
+ function SectionPanel({ children }: { children: ReactNode }) {
269
+ return <div className="px-6">{children}</div>
270
+ }
@@ -0,0 +1,101 @@
1
+ import type { ReactNode } from 'react'
2
+ import { ArrowLeft, ChevronRight, LogOut } from 'lucide-react'
3
+ import { Link } from '../../link'
4
+ import { cn } from '../../ui/cn'
5
+ import {
6
+ accountSectionLabel,
7
+ parseAccountRoute,
8
+ } from './parse-account-route'
9
+
10
+ export type AccountMenuItem = {
11
+ key: string
12
+ href: string
13
+ label: string
14
+ icon: ReactNode
15
+ }
16
+
17
+ type AccountShellProps = {
18
+ menuItems: AccountMenuItem[]
19
+ onLogout: () => void
20
+ profileSyncError?: string | null
21
+ children: ReactNode
22
+ pathname: string
23
+ homeHref?: string
24
+ }
25
+
26
+ export function AccountShell({
27
+ menuItems,
28
+ onLogout,
29
+ profileSyncError,
30
+ children,
31
+ pathname,
32
+ homeHref = '/',
33
+ }: AccountShellProps) {
34
+ const { section, orderId } = parseAccountRoute(pathname || '/account')
35
+ const sectionTitle = accountSectionLabel(section, orderId)
36
+
37
+ return (
38
+ <div className="container max-w-6xl mx-auto px-4 py-8">
39
+ <Link
40
+ to={homeHref}
41
+ className="inline-flex items-center gap-2 text-sm text-muted-foreground transition-colors [@media(hover:hover)]:hover:text-foreground"
42
+ >
43
+ <ArrowLeft className="size-4" />
44
+ <span className="underline">Volver a la tienda</span>
45
+ </Link>
46
+
47
+ <h1 className="mt-2 text-3xl font-semibold tracking-tight">Mi perfil</h1>
48
+
49
+ {profileSyncError ? (
50
+ <p className="mt-3 text-sm text-amber-800">{profileSyncError}</p>
51
+ ) : null}
52
+
53
+ <div className="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]">
54
+ <aside className="overflow-hidden rounded-xl border border-border bg-background lg:sticky lg:top-15 lg:self-start">
55
+ <nav className="divide-y divide-border">
56
+ {menuItems.map((item) => {
57
+ const isOrders = item.key === 'orders'
58
+ const active =
59
+ (section === item.key && !orderId) ||
60
+ (isOrders && section === 'orders' && Boolean(orderId))
61
+
62
+ return (
63
+ <Link
64
+ key={item.key}
65
+ to={item.href}
66
+ className={cn(
67
+ 'flex items-center justify-between border-b border-border px-4 py-4 text-[15px] transition-colors',
68
+ active
69
+ ? 'bg-muted font-semibold'
70
+ : '[@media(hover:hover)]:hover:bg-muted',
71
+ )}
72
+ >
73
+ <span className="flex items-center gap-3">
74
+ <span className="text-muted-foreground">{item.icon}</span>
75
+ <span>{item.label}</span>
76
+ </span>
77
+ <ChevronRight className="size-4 text-muted-foreground" />
78
+ </Link>
79
+ )
80
+ })}
81
+ <button
82
+ type="button"
83
+ onClick={onLogout}
84
+ className="flex w-full items-center justify-between px-4 py-4 text-left text-[15px] transition-colors [@media(hover:hover)]:hover:bg-muted"
85
+ >
86
+ <span className="flex items-center gap-3">
87
+ <LogOut className="size-4 text-muted-foreground" />
88
+ <span>Cerrar sesión</span>
89
+ </span>
90
+ <ChevronRight className="size-4 text-muted-foreground" />
91
+ </button>
92
+ </nav>
93
+ </aside>
94
+
95
+ <section className="min-w-0" aria-label={sectionTitle}>
96
+ {children}
97
+ </section>
98
+ </div>
99
+ </div>
100
+ )
101
+ }
@@ -0,0 +1,36 @@
1
+ export type AccountSection = 'personal' | 'addresses' | 'orders' | 'devices'
2
+
3
+ export type ParsedAccountRoute = {
4
+ section: AccountSection
5
+ orderId?: string
6
+ }
7
+
8
+ const SECTION_LABELS: Record<AccountSection, string> = {
9
+ personal: 'Datos personales',
10
+ addresses: 'Direcciones',
11
+ orders: 'Mis pedidos',
12
+ devices: 'Dispositivos vinculados',
13
+ }
14
+
15
+ export function accountSectionLabel(section: AccountSection, orderId?: string): string {
16
+ if (section === 'orders' && orderId) return 'Detalle del pedido'
17
+ return SECTION_LABELS[section]
18
+ }
19
+
20
+ /** Deriva sección y orderId desde `/account/...`. */
21
+ export function parseAccountRoute(pathname: string): ParsedAccountRoute {
22
+ const normalized = pathname.replace(/\/$/, '') || '/'
23
+ const parts = normalized.split('/').filter(Boolean)
24
+ const accountIdx = parts.indexOf('account')
25
+ if (accountIdx === -1) return { section: 'personal' }
26
+
27
+ const sub = parts.slice(accountIdx + 1)
28
+ if (sub.length === 0) return { section: 'personal' }
29
+ if (sub[0] === 'addresses') return { section: 'addresses' }
30
+ if (sub[0] === 'devices') return { section: 'devices' }
31
+ if (sub[0] === 'orders') {
32
+ if (sub[1]) return { section: 'orders', orderId: decodeURIComponent(sub[1]) }
33
+ return { section: 'orders' }
34
+ }
35
+ return { section: 'personal' }
36
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Área de cuenta cerrada de Ecomiq (`/account`, `/account/addresses`, …).
3
+ * Los templates no pueden overridear estas rutas desde `pages/account`.
4
+ * Login/register viven en la raíz del template (`/login`, `/register`).
5
+ */
6
+ import { definePage } from '../index'
7
+ import { AccountPageView } from './account/account-page-view'
8
+
9
+ export default definePage({
10
+ meta: () => ({
11
+ title: 'Mi cuenta',
12
+ description: 'Gestiona tu cuenta, direcciones y pedidos',
13
+ noIndex: true,
14
+ }),
15
+ component: AccountPageView,
16
+ })
@@ -34,7 +34,7 @@ function CheckoutPage({ params }: { params: { checkoutId: string } }) {
34
34
  useEffect(() => {
35
35
  if (!needsAuth || !checkoutId) return
36
36
  void navigate({
37
- to: '/account/login',
37
+ to: '/login',
38
38
  search: { redirect: `/checkout/${checkoutId}` },
39
39
  replace: true,
40
40
  })
@@ -62,8 +62,17 @@ export function useAuth() {
62
62
  session: sessionQuery.data,
63
63
  isPending: sessionQuery.isPending,
64
64
  error: sessionQuery.error,
65
- signIn: (credentials: EcomiqSignInCredentials) =>
66
- signInWithEcomiqCredentials(client, settings, credentials),
65
+ signIn: async (credentials: EcomiqSignInCredentials) => {
66
+ await signInWithEcomiqCredentials(client, settings, credentials)
67
+ const result = await client.getSession()
68
+ const session = (result as { data?: StorefrontSession | null })?.data ?? null
69
+ if (!session?.user) {
70
+ throw new Error(
71
+ 'No se pudo establecer la sesión. Revisa cookies / certificado HTTPS e inténtalo de nuevo.',
72
+ )
73
+ }
74
+ return session
75
+ },
67
76
  signOut: () => client.signOut(),
68
77
  }
69
78
  }
@@ -0,0 +1,20 @@
1
+ import { createApi, type Api } from '@ecomiq/core'
2
+
3
+ /**
4
+ * Cliente HTTP hacia el BFF same-origin (`/api/*`).
5
+ * El proxy inyecta `x-Store-Id` y el Bearer desde la cookie OAuth
6
+ * (`ecomiq_sf_oauth_at`, path `/api`).
7
+ *
8
+ * Usar con los services de `@ecomiq/core` (p. ej. `getCustomerAddresses(api)`).
9
+ * `version` vacío: el path del proxy ya es `/api/...` (el upstream añade `/v1`).
10
+ */
11
+ export function createStorefrontProxyApi(): Api {
12
+ const origin =
13
+ typeof window !== 'undefined' && window.location?.origin
14
+ ? window.location.origin
15
+ : 'http://localhost'
16
+ return createApi({
17
+ baseUrl: `${origin}/api`,
18
+ version: '',
19
+ })
20
+ }
@@ -7,12 +7,14 @@
7
7
  * Alineado con el ejemplo oficial TanStack Start + Cloudflare:
8
8
  * https://github.com/TanStack/router/tree/main/examples/react/start-basic-cloudflare
9
9
  */
10
- import { dirname, resolve } from 'node:path'
10
+ import { readFileSync } from 'node:fs'
11
+ import { dirname, isAbsolute, resolve } from 'node:path'
11
12
  import { fileURLToPath } from 'node:url'
12
13
  import { tanstackStart } from '@tanstack/react-start/plugin/vite'
13
14
  import viteReact from '@vitejs/plugin-react'
14
15
  import tailwindcss from '@tailwindcss/vite'
15
16
  import { cloudflare } from '@cloudflare/vite-plugin'
17
+ import basicSsl from '@vitejs/plugin-basic-ssl'
16
18
 
17
19
  const postcssBrowserStub = fileURLToPath(
18
20
  new URL('./runtime/postcss-browser-stub.mjs', import.meta.url),
@@ -97,6 +99,51 @@ export function resolveDevRuntime({ config, runtime } = {}) {
97
99
  return runtime ?? config?.dev?.runtime ?? 'node'
98
100
  }
99
101
 
102
+ /**
103
+ * HTTPS en `ecomiq dev` (localhost).
104
+ * - `dev.https: true` → certificado auto (plugin-basic-ssl)
105
+ * - `dev.https: { cert, key }` → PEM propios (paths relativos al project root)
106
+ *
107
+ * @param {{ config?: object, projectRoot?: string, appDir?: string }} opts
108
+ * @returns {{ enabled: boolean, plugin?: object, https?: import('node:https').ServerOptions }}
109
+ */
110
+ export function resolveDevHttps({ config, projectRoot, appDir } = {}) {
111
+ const https = config?.dev?.https
112
+ if (!https) return { enabled: false }
113
+
114
+ if (https === true) {
115
+ return {
116
+ enabled: true,
117
+ plugin: basicSsl({
118
+ name: 'ecomiq',
119
+ domains: ['localhost', '127.0.0.1', '::1'],
120
+ }),
121
+ }
122
+ }
123
+
124
+ if (typeof https === 'object' && https !== null) {
125
+ const root = projectRoot ?? (appDir ? resolve(appDir, '..') : process.cwd())
126
+ const certPath = resolvePathFromRoot(root, https.cert)
127
+ const keyPath = resolvePathFromRoot(root, https.key)
128
+ return {
129
+ enabled: true,
130
+ https: {
131
+ cert: readFileSync(certPath),
132
+ key: readFileSync(keyPath),
133
+ },
134
+ }
135
+ }
136
+
137
+ return { enabled: false }
138
+ }
139
+
140
+ function resolvePathFromRoot(root, value) {
141
+ if (typeof value !== 'string' || !value.trim()) {
142
+ throw new Error('dev.https.cert y dev.https.key deben ser paths a archivos PEM')
143
+ }
144
+ return isAbsolute(value) ? value : resolve(root, value)
145
+ }
146
+
100
147
  /**
101
148
  * @param {{ appDir: string, config?: object, projectRoot?: string, runtime?: 'workerd' | 'node' }} opts
102
149
  */
@@ -111,9 +158,14 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
111
158
  ]
112
159
 
113
160
  const resolvedRuntime = resolveDevRuntime({ config, runtime })
161
+ const resolvedHttps = resolveDevHttps({ config, projectRoot: root, appDir })
114
162
 
115
163
  const plugins = [tailwindcss()]
116
164
 
165
+ if (resolvedHttps.plugin) {
166
+ plugins.push(resolvedHttps.plugin)
167
+ }
168
+
117
169
  if (resolvedRuntime === 'workerd') {
118
170
  plugins.unshift(bundleSsrDependenciesForWorkerd())
119
171
  }
@@ -172,6 +224,7 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
172
224
  },
173
225
  server: {
174
226
  fs: { allow },
227
+ ...(resolvedHttps.https ? { https: resolvedHttps.https } : {}),
175
228
  },
176
229
  }
177
230
  }