@decocms/apps 0.21.4 → 0.23.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/commerce/components/Image.tsx +69 -83
- package/commerce/components/Picture.tsx +114 -0
- package/package.json +2 -1
- package/vtex/client.ts +23 -3
- package/vtex/invoke.ts +9 -1
- package/vtex/middleware.ts +10 -1
- package/vtex/utils/cookies.ts +62 -21
- package/vtex/utils/proxy.ts +18 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImgHTMLAttributes } from "react";
|
|
2
|
+
import { forwardRef } from "react";
|
|
2
3
|
|
|
3
4
|
// -------------------------------------------------------------------------
|
|
4
5
|
// Known asset prefixes that get stripped to produce a relative src path
|
|
@@ -34,7 +35,7 @@ export function getImageCdnDomain(): string {
|
|
|
34
35
|
// Fit options & optimization types
|
|
35
36
|
// -------------------------------------------------------------------------
|
|
36
37
|
|
|
37
|
-
export type FitOptions = "contain" | "cover";
|
|
38
|
+
export type FitOptions = "contain" | "cover" | "fill";
|
|
38
39
|
|
|
39
40
|
export const FACTORS = [1, 2];
|
|
40
41
|
|
|
@@ -163,92 +164,77 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "s
|
|
|
163
164
|
height?: number;
|
|
164
165
|
/** @description Object-fit */
|
|
165
166
|
fit?: FitOptions;
|
|
166
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* @description Web Vitals (LCP). Injects a `<link rel="preload">` tag
|
|
169
|
+
* alongside the `<img>`, sets `fetchPriority="high"` and `loading="eager"`.
|
|
170
|
+
* Use once per page for the LCP image.
|
|
171
|
+
*/
|
|
167
172
|
preload?: boolean;
|
|
173
|
+
/** @description Media query for responsive preloading (e.g. "(min-width: 768px)") */
|
|
174
|
+
media?: string;
|
|
168
175
|
}
|
|
169
176
|
|
|
170
|
-
export
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
src={src}
|
|
192
|
-
srcSet={srcSet}
|
|
193
|
-
sizes={srcSet ? (sizes ?? "(max-width: 768px) 100vw, 50vw") : undefined}
|
|
194
|
-
width={width}
|
|
195
|
-
height={height}
|
|
196
|
-
loading={loading ?? (preload ? "eager" : "lazy")}
|
|
197
|
-
decoding={decoding ?? "async"}
|
|
198
|
-
fetchPriority={preload ? "high" : undefined}
|
|
199
|
-
/>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// -------------------------------------------------------------------------
|
|
204
|
-
// Picture (responsive art direction)
|
|
205
|
-
// -------------------------------------------------------------------------
|
|
206
|
-
|
|
207
|
-
export interface PictureSourceProps {
|
|
208
|
-
src: string;
|
|
209
|
-
width: number;
|
|
210
|
-
height?: number;
|
|
211
|
-
media: string;
|
|
212
|
-
fit?: FitOptions;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export interface PictureProps extends Omit<ImageProps, "sizes"> {
|
|
216
|
-
sources: PictureSourceProps[];
|
|
217
|
-
}
|
|
177
|
+
export const Image = forwardRef<HTMLImageElement, ImageProps>(
|
|
178
|
+
function Image(
|
|
179
|
+
{
|
|
180
|
+
src,
|
|
181
|
+
width,
|
|
182
|
+
height,
|
|
183
|
+
fit = "cover",
|
|
184
|
+
preload,
|
|
185
|
+
media,
|
|
186
|
+
loading,
|
|
187
|
+
decoding,
|
|
188
|
+
srcSet: srcSetProp,
|
|
189
|
+
sizes,
|
|
190
|
+
fetchPriority,
|
|
191
|
+
...rest
|
|
192
|
+
},
|
|
193
|
+
ref,
|
|
194
|
+
) {
|
|
195
|
+
if (!height && typeof process !== "undefined") {
|
|
196
|
+
console.warn(`Missing height. This image will NOT be optimized: ${src}`);
|
|
197
|
+
}
|
|
218
198
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
199
|
+
const optimizedSrc = getOptimizedMediaUrl({
|
|
200
|
+
originalSrc: src,
|
|
201
|
+
width,
|
|
202
|
+
height,
|
|
203
|
+
fit,
|
|
204
|
+
});
|
|
205
|
+
const srcSet = srcSetProp ?? getSrcSet(src, width, height, fit);
|
|
206
|
+
const resolvedSizes = srcSet
|
|
207
|
+
? (sizes ?? "(max-width: 768px) 100vw, 50vw")
|
|
208
|
+
: undefined;
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<>
|
|
212
|
+
{preload && (
|
|
213
|
+
<link
|
|
214
|
+
as="image"
|
|
215
|
+
rel="preload"
|
|
216
|
+
href={optimizedSrc}
|
|
217
|
+
imageSrcSet={srcSet}
|
|
218
|
+
imageSizes={resolvedSizes}
|
|
219
|
+
fetchPriority={fetchPriority ?? "high"}
|
|
220
|
+
media={media}
|
|
239
221
|
/>
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
222
|
+
)}
|
|
223
|
+
<img
|
|
224
|
+
{...rest}
|
|
225
|
+
src={optimizedSrc}
|
|
226
|
+
srcSet={srcSet}
|
|
227
|
+
sizes={resolvedSizes}
|
|
228
|
+
width={width}
|
|
229
|
+
height={height}
|
|
230
|
+
loading={loading ?? (preload ? "eager" : "lazy")}
|
|
231
|
+
decoding={decoding ?? "async"}
|
|
232
|
+
fetchPriority={preload ? "high" : fetchPriority}
|
|
233
|
+
ref={ref}
|
|
234
|
+
/>
|
|
235
|
+
</>
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
);
|
|
253
239
|
|
|
254
240
|
export default Image;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
type ComponentPropsWithoutRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { getOptimizedMediaUrl, getSrcSet, type FitOptions } from "./Image";
|
|
10
|
+
|
|
11
|
+
// -------------------------------------------------------------------------
|
|
12
|
+
// Preload context — flows from <Picture preload> to child <Source> elements
|
|
13
|
+
// so each source can inject its own <link rel="preload"> with the correct
|
|
14
|
+
// media query for responsive art direction.
|
|
15
|
+
// -------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
interface PreloadContextValue {
|
|
18
|
+
preload: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PreloadContext = createContext<PreloadContextValue>({ preload: false });
|
|
22
|
+
|
|
23
|
+
// -------------------------------------------------------------------------
|
|
24
|
+
// Source — composable <source> with automatic srcSet optimization and
|
|
25
|
+
// preload link injection when inside a <Picture preload>.
|
|
26
|
+
// -------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export type SourceProps = Omit<
|
|
29
|
+
ComponentPropsWithoutRef<"source">,
|
|
30
|
+
"width" | "height"
|
|
31
|
+
> & {
|
|
32
|
+
src: string;
|
|
33
|
+
/** @description Improves Web Vitals (CLS|LCP) */
|
|
34
|
+
width: number;
|
|
35
|
+
/** @description Improves Web Vitals (CLS|LCP) */
|
|
36
|
+
height?: number;
|
|
37
|
+
/** @description Improves Web Vitals (LCP). Use high for LCP image. */
|
|
38
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
39
|
+
/** @description Object-fit */
|
|
40
|
+
fit?: FitOptions;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Source = forwardRef<HTMLSourceElement, SourceProps>(
|
|
44
|
+
function Source(
|
|
45
|
+
{ src, width, height, fetchPriority, fit = "cover", ...rest },
|
|
46
|
+
ref,
|
|
47
|
+
) {
|
|
48
|
+
const { preload } = useContext(PreloadContext);
|
|
49
|
+
|
|
50
|
+
const optimizedSrc = getOptimizedMediaUrl({
|
|
51
|
+
originalSrc: src,
|
|
52
|
+
width,
|
|
53
|
+
height,
|
|
54
|
+
fit,
|
|
55
|
+
});
|
|
56
|
+
const srcSet = rest.srcSet ?? getSrcSet(src, width, height, fit);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
{preload && (
|
|
61
|
+
<link
|
|
62
|
+
as="image"
|
|
63
|
+
rel="preload"
|
|
64
|
+
href={optimizedSrc}
|
|
65
|
+
imageSrcSet={srcSet}
|
|
66
|
+
fetchPriority={fetchPriority ?? "high"}
|
|
67
|
+
media={rest.media}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
<source
|
|
71
|
+
{...rest}
|
|
72
|
+
srcSet={srcSet ?? optimizedSrc}
|
|
73
|
+
width={width}
|
|
74
|
+
height={height}
|
|
75
|
+
ref={ref}
|
|
76
|
+
/>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
// Picture — composable wrapper that provides preload context to children.
|
|
84
|
+
//
|
|
85
|
+
// Usage:
|
|
86
|
+
// <Picture preload={isLcp}>
|
|
87
|
+
// <Source media="(max-width: 767px)" src={mobile} width={320} height={280} />
|
|
88
|
+
// <Source media="(min-width: 768px)" src={desktop} width={1280} height={280} />
|
|
89
|
+
// <Image src={desktop} width={1280} height={280} />
|
|
90
|
+
// </Picture>
|
|
91
|
+
// -------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export type PictureProps = ComponentPropsWithoutRef<"picture"> & {
|
|
94
|
+
children: ReactNode;
|
|
95
|
+
/**
|
|
96
|
+
* @description When true, child <Source> and <Image> elements inject
|
|
97
|
+
* `<link rel="preload">` tags for their respective media queries.
|
|
98
|
+
*/
|
|
99
|
+
preload?: boolean;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const Picture = forwardRef<HTMLPictureElement, PictureProps>(
|
|
103
|
+
function Picture({ children, preload = false, ...props }, ref) {
|
|
104
|
+
const value = useMemo(() => ({ preload }), [preload]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<PreloadContext.Provider value={value}>
|
|
108
|
+
<picture {...props} ref={ref}>
|
|
109
|
+
{children}
|
|
110
|
+
</picture>
|
|
111
|
+
</PreloadContext.Provider>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
);
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./commerce/types": "./commerce/types/commerce.ts",
|
|
8
8
|
"./commerce/components/JsonLd": "./commerce/components/JsonLd.tsx",
|
|
9
9
|
"./commerce/components/Image": "./commerce/components/Image.tsx",
|
|
10
|
+
"./commerce/components/Picture": "./commerce/components/Picture.tsx",
|
|
10
11
|
"./commerce/utils/*": "./commerce/utils/*.ts",
|
|
11
12
|
"./commerce/sdk/*": "./commerce/sdk/*.ts",
|
|
12
13
|
"./shopify": "./shopify/index.ts",
|
package/vtex/client.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { type FetchCacheOptions, fetchWithCache } from "./utils/fetchCache";
|
|
7
|
+
import { SEGMENT_COOKIE_NAME, parseSegment } from "./utils/segment";
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// URL sanitization (ported from deco-cx/apps vtex/utils/fetchVTEX.ts)
|
|
@@ -176,6 +177,22 @@ function authHeaders(): Record<string, string> {
|
|
|
176
177
|
return headers;
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Read regionId from the current request's vtex_segment cookie.
|
|
182
|
+
* Returns null when no cookie provider is registered or no regionId is set.
|
|
183
|
+
*/
|
|
184
|
+
function extractRegionIdFromCookies(): string | null {
|
|
185
|
+
if (!_getCookieHeader) return null;
|
|
186
|
+
const cookies = _getCookieHeader();
|
|
187
|
+
if (!cookies) return null;
|
|
188
|
+
const match = cookies.match(
|
|
189
|
+
new RegExp(`(?:^|;\\s*)${SEGMENT_COOKIE_NAME}=([^;]+)`),
|
|
190
|
+
);
|
|
191
|
+
if (!match?.[1]) return null;
|
|
192
|
+
const segment = parseSegment(match[1]);
|
|
193
|
+
return segment?.regionId ?? null;
|
|
194
|
+
}
|
|
195
|
+
|
|
179
196
|
export async function vtexFetchResponse(
|
|
180
197
|
path: string,
|
|
181
198
|
init?: RequestInit,
|
|
@@ -293,7 +310,7 @@ export async function vtexFetchWithCookies<T>(
|
|
|
293
310
|
export async function intelligentSearch<T>(
|
|
294
311
|
path: string,
|
|
295
312
|
params?: Record<string, string>,
|
|
296
|
-
opts?: { cookieHeader?: string; locale?: string },
|
|
313
|
+
opts?: { cookieHeader?: string; locale?: string; regionId?: string },
|
|
297
314
|
): Promise<T> {
|
|
298
315
|
const url = new URL(`${isUrl()}${path}`);
|
|
299
316
|
if (params) {
|
|
@@ -309,6 +326,11 @@ export async function intelligentSearch<T>(
|
|
|
309
326
|
url.searchParams.set("locale", locale);
|
|
310
327
|
}
|
|
311
328
|
|
|
329
|
+
const regionId = opts?.regionId ?? extractRegionIdFromCookies();
|
|
330
|
+
if (regionId) {
|
|
331
|
+
url.searchParams.set("regionId", regionId);
|
|
332
|
+
}
|
|
333
|
+
|
|
312
334
|
const headers: Record<string, string> = { ...authHeaders() };
|
|
313
335
|
if (opts?.cookieHeader) {
|
|
314
336
|
headers.cookie = opts.cookieHeader;
|
|
@@ -316,8 +338,6 @@ export async function intelligentSearch<T>(
|
|
|
316
338
|
|
|
317
339
|
const fullUrl = url.toString();
|
|
318
340
|
|
|
319
|
-
// IS GET requests go through SWR cache (3 min TTL via status-based defaults).
|
|
320
|
-
// The doFetch callback throws on non-ok responses, so null is never returned.
|
|
321
341
|
return fetchWithCache<T>(fullUrl, async () => {
|
|
322
342
|
const response = await _fetch(fullUrl, { headers });
|
|
323
343
|
if (!response.ok) {
|
package/vtex/invoke.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
type UploadAttachmentOpts,
|
|
43
43
|
type CreateDocumentResult,
|
|
44
44
|
} from "./actions/masterData";
|
|
45
|
-
import { createSession } from "./actions/session";
|
|
45
|
+
import { createSession, editSession, type SessionData } from "./actions/session";
|
|
46
46
|
import { subscribe, type SubscribeProps } from "./actions/newsletter";
|
|
47
47
|
import { notifyMe, type NotifyMeProps } from "./actions/misc";
|
|
48
48
|
import type { OrderForm } from "./types";
|
|
@@ -168,6 +168,14 @@ export const invoke = {
|
|
|
168
168
|
{ unwrap: true },
|
|
169
169
|
),
|
|
170
170
|
|
|
171
|
+
editSession: createInvokeFn(
|
|
172
|
+
(input: { public: Record<string, { value: string }> }) =>
|
|
173
|
+
editSession(input.public),
|
|
174
|
+
{ unwrap: true },
|
|
175
|
+
) as unknown as (ctx: {
|
|
176
|
+
data: { public: Record<string, { value: string }> };
|
|
177
|
+
}) => Promise<SessionData>,
|
|
178
|
+
|
|
171
179
|
// -- MasterData -------------------------------------------------------
|
|
172
180
|
|
|
173
181
|
createDocument: createInvokeFn(
|
package/vtex/middleware.ts
CHANGED
|
@@ -52,6 +52,12 @@ export interface VtexRequestContext {
|
|
|
52
52
|
email?: string;
|
|
53
53
|
/** Sales channel derived from segment. */
|
|
54
54
|
salesChannel: string;
|
|
55
|
+
/**
|
|
56
|
+
* VTEX region ID from the segment cookie.
|
|
57
|
+
* Present when the user has set a postal code (CEP) for regionalization.
|
|
58
|
+
* Null when no region is set (anonymous default segment).
|
|
59
|
+
*/
|
|
60
|
+
regionId: string | null;
|
|
55
61
|
/** Whether this request carries price tables (B2B). */
|
|
56
62
|
hasCustomPricing: boolean;
|
|
57
63
|
/** Intelligent Search session cookie. */
|
|
@@ -123,6 +129,7 @@ export function extractVtexContext(request: Request): VtexRequestContext {
|
|
|
123
129
|
isLoggedIn: authInfo?.isLoggedIn ?? false,
|
|
124
130
|
email: authInfo?.email,
|
|
125
131
|
salesChannel: segment.channel ?? "1",
|
|
132
|
+
regionId: segment.regionId ?? null,
|
|
126
133
|
hasCustomPricing: Boolean(
|
|
127
134
|
segment.priceTables && segment.priceTables.length > 0,
|
|
128
135
|
),
|
|
@@ -217,7 +224,9 @@ export function buildSegmentSetCookie(
|
|
|
217
224
|
*/
|
|
218
225
|
export function vtexCacheKeySuffix(ctx: VtexRequestContext): string {
|
|
219
226
|
if (ctx.isLoggedIn) return "__vtex_auth";
|
|
220
|
-
|
|
227
|
+
const parts = [`sc=${ctx.salesChannel}`];
|
|
228
|
+
if (ctx.regionId) parts.push(`r=${ctx.regionId}`);
|
|
229
|
+
return `__vtex_${parts.join("_")}`;
|
|
221
230
|
}
|
|
222
231
|
|
|
223
232
|
// -------------------------------------------------------------------------
|
package/vtex/utils/cookies.ts
CHANGED
|
@@ -10,32 +10,73 @@ interface Cookie {
|
|
|
10
10
|
sameSite?: "Strict" | "Lax" | "None";
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function parseSingleSetCookie(raw: string): Cookie | null {
|
|
14
|
+
const parts = raw.split(";").map((p) => p.trim());
|
|
15
|
+
const [nameValue, ...attrs] = parts;
|
|
16
|
+
const eqIdx = nameValue.indexOf("=");
|
|
17
|
+
if (eqIdx < 0) return null;
|
|
18
|
+
const cookie: Cookie = {
|
|
19
|
+
name: nameValue.slice(0, eqIdx),
|
|
20
|
+
value: nameValue.slice(eqIdx + 1),
|
|
21
|
+
};
|
|
22
|
+
for (const attr of attrs) {
|
|
23
|
+
const [k, v] = attr.split("=").map((s) => s.trim());
|
|
24
|
+
const lower = k.toLowerCase();
|
|
25
|
+
if (lower === "domain") cookie.domain = v;
|
|
26
|
+
else if (lower === "path") cookie.path = v;
|
|
27
|
+
else if (lower === "secure") cookie.secure = true;
|
|
28
|
+
else if (lower === "httponly") cookie.httpOnly = true;
|
|
29
|
+
else if (lower === "samesite") cookie.sameSite = v as Cookie["sameSite"];
|
|
30
|
+
}
|
|
31
|
+
return cookie;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract individual Set-Cookie values from a Headers object.
|
|
36
|
+
*
|
|
37
|
+
* Uses Headers.getSetCookie() (available in Cloudflare Workers and Node 18+)
|
|
38
|
+
* which returns each Set-Cookie as a separate string — unlike Headers.get()
|
|
39
|
+
* or Headers.forEach() which join multiple values with ", " and corrupt
|
|
40
|
+
* cookie strings that contain commas in Expires dates.
|
|
41
|
+
*/
|
|
13
42
|
function getSetCookies(headers: Headers): Cookie[] {
|
|
43
|
+
const rawCookies: string[] =
|
|
44
|
+
typeof headers.getSetCookie === "function"
|
|
45
|
+
? headers.getSetCookie()
|
|
46
|
+
: getRawSetCookiesFallback(headers);
|
|
47
|
+
|
|
14
48
|
const cookies: Cookie[] = [];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const eqIdx = nameValue.indexOf("=");
|
|
20
|
-
if (eqIdx < 0) return;
|
|
21
|
-
const cookie: Cookie = {
|
|
22
|
-
name: nameValue.slice(0, eqIdx),
|
|
23
|
-
value: nameValue.slice(eqIdx + 1),
|
|
24
|
-
};
|
|
25
|
-
for (const attr of attrs) {
|
|
26
|
-
const [k, v] = attr.split("=").map((s) => s.trim());
|
|
27
|
-
const lower = k.toLowerCase();
|
|
28
|
-
if (lower === "domain") cookie.domain = v;
|
|
29
|
-
else if (lower === "path") cookie.path = v;
|
|
30
|
-
else if (lower === "secure") cookie.secure = true;
|
|
31
|
-
else if (lower === "httponly") cookie.httpOnly = true;
|
|
32
|
-
else if (lower === "samesite") cookie.sameSite = v as Cookie["sameSite"];
|
|
33
|
-
}
|
|
34
|
-
cookies.push(cookie);
|
|
35
|
-
});
|
|
49
|
+
for (const raw of rawCookies) {
|
|
50
|
+
const cookie = parseSingleSetCookie(raw);
|
|
51
|
+
if (cookie) cookies.push(cookie);
|
|
52
|
+
}
|
|
36
53
|
return cookies;
|
|
37
54
|
}
|
|
38
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Fallback for runtimes without Headers.getSetCookie().
|
|
58
|
+
* Splits the comma-joined string heuristically — not perfect for cookies
|
|
59
|
+
* with Expires containing commas, but better than the old approach.
|
|
60
|
+
*/
|
|
61
|
+
function getRawSetCookiesFallback(headers: Headers): string[] {
|
|
62
|
+
const joined = headers.get("set-cookie");
|
|
63
|
+
if (!joined) return [];
|
|
64
|
+
const results: string[] = [];
|
|
65
|
+
let current = "";
|
|
66
|
+
for (const segment of joined.split(",")) {
|
|
67
|
+
const trimmed = segment.trimStart();
|
|
68
|
+
const looksLikeNewCookie = /^[^=;]+=[^;]/.test(trimmed) && current.length > 0;
|
|
69
|
+
if (looksLikeNewCookie) {
|
|
70
|
+
results.push(current.trim());
|
|
71
|
+
current = trimmed;
|
|
72
|
+
} else {
|
|
73
|
+
current += (current ? "," : "") + segment;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (current.trim()) results.push(current.trim());
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
39
80
|
function setCookie(headers: Headers, cookie: Cookie): void {
|
|
40
81
|
let str = `${cookie.name}=${cookie.value}`;
|
|
41
82
|
if (cookie.domain) str += `; Domain=${cookie.domain}`;
|
package/vtex/utils/proxy.ts
CHANGED
|
@@ -104,10 +104,20 @@ function buildOriginUrl(
|
|
|
104
104
|
return new URL(`https://${originHost}${url.pathname}${url.search}`);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Copy headers excluding hop-by-hop and Set-Cookie.
|
|
109
|
+
*
|
|
110
|
+
* Set-Cookie is excluded intentionally: Headers.forEach / .set() joins
|
|
111
|
+
* multiple Set-Cookie values with ", " which corrupts cookies containing
|
|
112
|
+
* commas (e.g. Expires dates). proxySetCookie handles Set-Cookie
|
|
113
|
+
* separately using Headers.getSetCookie() for correct multi-cookie support.
|
|
114
|
+
*/
|
|
107
115
|
function filterHeaders(headers: Headers): Headers {
|
|
108
116
|
const filtered = new Headers();
|
|
109
117
|
headers.forEach((value, key) => {
|
|
110
|
-
|
|
118
|
+
const lower = key.toLowerCase();
|
|
119
|
+
if (lower === "set-cookie") return;
|
|
120
|
+
if (!HOP_BY_HOP_HEADERS.has(lower)) {
|
|
111
121
|
filtered.set(key, value);
|
|
112
122
|
}
|
|
113
123
|
});
|
|
@@ -169,13 +179,13 @@ export async function proxyToVtex(
|
|
|
169
179
|
|
|
170
180
|
const responseHeaders = filterHeaders(new Headers(originResponse.headers));
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
new URL(request.url).origin
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
proxySetCookie(
|
|
183
|
+
originResponse.headers,
|
|
184
|
+
responseHeaders,
|
|
185
|
+
options?.rewriteCookieDomain !== false
|
|
186
|
+
? new URL(request.url).origin
|
|
187
|
+
: undefined,
|
|
188
|
+
);
|
|
179
189
|
|
|
180
190
|
if (originResponse.status >= 300 && originResponse.status < 400) {
|
|
181
191
|
const location = originResponse.headers.get("location");
|