@decocms/apps-vtex 8.0.0 → 8.1.0-next.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.
- package/package.json +7 -52
- package/src/index.ts +12 -15
- package/src/utils/constants.ts +2 -56
- package/src/utils/cookieSanitizer.ts +59 -59
- package/src/utils/operationRouter.ts +76 -72
- package/src/utils/types.ts +709 -1557
- package/src/vtexClient.ts +444 -0
- package/src/README.md +0 -6
- package/src/__tests__/client-segment-cookie.test.ts +0 -255
- package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
- package/src/__tests__/schemas.test.ts +0 -71
- package/src/actions/address.ts +0 -250
- package/src/actions/analytics/sendEvent.ts +0 -86
- package/src/actions/auth.ts +0 -333
- package/src/actions/checkout.ts +0 -659
- package/src/actions/index.ts +0 -11
- package/src/actions/masterData.ts +0 -168
- package/src/actions/misc.ts +0 -188
- package/src/actions/newsletter.ts +0 -105
- package/src/actions/orders.ts +0 -34
- package/src/actions/profile.ts +0 -201
- package/src/actions/session.ts +0 -85
- package/src/actions/trigger.ts +0 -43
- package/src/actions/wishlist.ts +0 -114
- package/src/client.ts +0 -667
- package/src/commerceLoaders.ts +0 -267
- package/src/hooks/__tests__/createCart.render.test.ts +0 -114
- package/src/hooks/__tests__/createCart.test.ts +0 -92
- package/src/hooks/__tests__/createUseCart.test.ts +0 -184
- package/src/hooks/__tests__/createUseUser.test.ts +0 -48
- package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
- package/src/hooks/cartQuery.ts +0 -204
- package/src/hooks/createCart.ts +0 -427
- package/src/hooks/createUseCart.ts +0 -360
- package/src/hooks/createUseUser.ts +0 -153
- package/src/hooks/createUseWishlist.ts +0 -242
- package/src/hooks/index.ts +0 -25
- package/src/hooks/useAutocomplete.ts +0 -83
- package/src/hooks/useCart.ts +0 -243
- package/src/hooks/useUser.ts +0 -78
- package/src/hooks/useWishlist.ts +0 -119
- package/src/invoke.ts +0 -235
- package/src/loaders/ProductDetailsPage.ts +0 -1
- package/src/loaders/ProductList.ts +0 -1
- package/src/loaders/ProductListingPage.ts +0 -1
- package/src/loaders/address.ts +0 -115
- package/src/loaders/autocomplete.ts +0 -58
- package/src/loaders/brands.ts +0 -44
- package/src/loaders/cart/attachments.ts +0 -53
- package/src/loaders/cart/full.ts +0 -67
- package/src/loaders/cart/gifts.ts +0 -43
- package/src/loaders/cart/orderFormId.ts +0 -31
- package/src/loaders/cart/shipping.test.ts +0 -125
- package/src/loaders/cart/shipping.ts +0 -136
- package/src/loaders/cart/summary.ts +0 -36
- package/src/loaders/cart.ts +0 -52
- package/src/loaders/catalog.ts +0 -163
- package/src/loaders/collections.ts +0 -55
- package/src/loaders/index.ts +0 -20
- package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
- package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -113
- package/src/loaders/intelligentSearch/productList.ts +0 -154
- package/src/loaders/intelligentSearch/productListingPage.ts +0 -526
- package/src/loaders/intelligentSearch/suggestions.ts +0 -46
- package/src/loaders/legacy/productDetailsPage.ts +0 -1
- package/src/loaders/legacy/productList.ts +0 -1
- package/src/loaders/legacy/relatedProductsLoader.ts +0 -81
- package/src/loaders/legacy.ts +0 -635
- package/src/loaders/logistics.ts +0 -111
- package/src/loaders/minicart.ts +0 -78
- package/src/loaders/navbar.ts +0 -26
- package/src/loaders/orders.ts +0 -92
- package/src/loaders/pageType.ts +0 -68
- package/src/loaders/paths/PDPDefaultPath.ts +0 -51
- package/src/loaders/payment.ts +0 -97
- package/src/loaders/productListFull.ts +0 -159
- package/src/loaders/profile.ts +0 -138
- package/src/loaders/promotion.ts +0 -30
- package/src/loaders/search.ts +0 -124
- package/src/loaders/session.ts +0 -83
- package/src/loaders/user.ts +0 -87
- package/src/loaders/wishlist.ts +0 -89
- package/src/loaders/wishlistProducts.ts +0 -69
- package/src/loaders/workflow/products.ts +0 -64
- package/src/loaders/workflow.ts +0 -316
- package/src/logo.png +0 -0
- package/src/manifest.gen.ts +0 -89
- package/src/middleware.ts +0 -240
- package/src/mod.ts +0 -157
- package/src/registry.ts +0 -9
- package/src/schemas.gen.ts +0 -3191
- package/src/schemas.ts +0 -30
- package/src/types.ts +0 -248
- package/src/utils/__tests__/cartProjection.test.ts +0 -118
- package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
- package/src/utils/__tests__/fetch.test.ts +0 -80
- package/src/utils/__tests__/fetchCache.test.ts +0 -155
- package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
- package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
- package/src/utils/__tests__/minicart.test.ts +0 -184
- package/src/utils/__tests__/operationRouter.test.ts +0 -227
- package/src/utils/__tests__/resilience.test.ts +0 -205
- package/src/utils/__tests__/resourceRange.test.ts +0 -32
- package/src/utils/__tests__/simulationCache.test.ts +0 -40
- package/src/utils/__tests__/sitemap.test.ts +0 -185
- package/src/utils/__tests__/slugify.test.ts +0 -31
- package/src/utils/__tests__/transform.test.ts +0 -769
- package/src/utils/accountLoaders.ts +0 -203
- package/src/utils/authHelpers.ts +0 -85
- package/src/utils/batch.ts +0 -18
- package/src/utils/cartProjection.ts +0 -96
- package/src/utils/cookies.ts +0 -167
- package/src/utils/enrichment.ts +0 -560
- package/src/utils/fetch.ts +0 -107
- package/src/utils/fetchCache.ts +0 -199
- package/src/utils/index.ts +0 -19
- package/src/utils/instrumentedFetch.ts +0 -97
- package/src/utils/intelligentSearch.ts +0 -96
- package/src/utils/legacy.ts +0 -139
- package/src/utils/minicart.ts +0 -139
- package/src/utils/pickAndOmit.ts +0 -25
- package/src/utils/proxy.ts +0 -435
- package/src/utils/resilience.ts +0 -352
- package/src/utils/resourceRange.ts +0 -10
- package/src/utils/segment.ts +0 -152
- package/src/utils/similars.ts +0 -37
- package/src/utils/simulationCache.ts +0 -73
- package/src/utils/sitemap.ts +0 -282
- package/src/utils/slugCache.ts +0 -28
- package/src/utils/slugify.ts +0 -13
- package/src/utils/transform.ts +0 -1522
- package/src/utils/vtexId.ts +0 -138
- package/tsconfig.json +0 -7
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-built section loaders for VTEX account pages.
|
|
3
|
-
*
|
|
4
|
-
* Every VTEX site with account pages repeats the same pattern:
|
|
5
|
-
* 1. Extract VTEX cookies from request
|
|
6
|
-
* 2. Call the VTEX user/profile/address/payment API
|
|
7
|
-
* 3. Return enriched props with { device, logged, ...data }
|
|
8
|
-
* 4. Catch errors gracefully (return logged: false)
|
|
9
|
-
*
|
|
10
|
-
* These factories encapsulate that boilerplate.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* import { vtexAccountLoaders } from "@decocms/apps/vtex/utils/accountLoaders";
|
|
15
|
-
*
|
|
16
|
-
* registerSectionLoaders({
|
|
17
|
-
* "site/sections/Account/PersonalData.tsx": vtexAccountLoaders.personalData(),
|
|
18
|
-
* "site/sections/Account/MyOrders.tsx": vtexAccountLoaders.orders(),
|
|
19
|
-
* "site/sections/Account/Cards.tsx": vtexAccountLoaders.cards(),
|
|
20
|
-
* "site/sections/Account/Addresses.tsx": vtexAccountLoaders.addresses(),
|
|
21
|
-
* "site/sections/Account/Auth.tsx": vtexAccountLoaders.authentication(),
|
|
22
|
-
* "site/sections/Account/Other.tsx": vtexAccountLoaders.loggedIn(),
|
|
23
|
-
* });
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { detectDevice } from "@decocms/blocks/sdk/useDevice";
|
|
28
|
-
import { getUserAddresses, type VtexAddress } from "../loaders/address";
|
|
29
|
-
import { getUserPayments, type Payment } from "../loaders/payment";
|
|
30
|
-
import { getCurrentProfile, type Profile } from "../loaders/profile";
|
|
31
|
-
import { getUser } from "../loaders/user";
|
|
32
|
-
import { getVtexCookies } from "./cookies";
|
|
33
|
-
|
|
34
|
-
type Device = "mobile" | "tablet" | "desktop";
|
|
35
|
-
|
|
36
|
-
type SectionLoaderFn = (
|
|
37
|
-
props: Record<string, unknown>,
|
|
38
|
-
req: Request,
|
|
39
|
-
) => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
40
|
-
|
|
41
|
-
function getDevice(req: Request): Device {
|
|
42
|
-
return detectDevice(req.headers.get("user-agent") ?? "");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// personalData — fetches full VTEX profile for personal data sections
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
export interface PersonalDataOptions {
|
|
50
|
-
/** Extra custom profile fields to request beyond the standard set. */
|
|
51
|
-
extraProfileFields?: string[];
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Transform the raw VTEX Profile into the shape your component expects.
|
|
55
|
-
* When omitted, the raw Profile object is returned as `profile`.
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```ts
|
|
59
|
-
* vtexAccountLoaders.personalData({
|
|
60
|
-
* mapProfile: (p) => ({
|
|
61
|
-
* "@id": p.userId ?? p.id,
|
|
62
|
-
* email: p.email,
|
|
63
|
-
* givenName: p.firstName ?? null,
|
|
64
|
-
* familyName: p.lastName ?? null,
|
|
65
|
-
* taxID: p.document,
|
|
66
|
-
* }),
|
|
67
|
-
* })
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
mapProfile?: (profile: Profile) => Record<string, unknown>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function personalData(options?: PersonalDataOptions): SectionLoaderFn {
|
|
74
|
-
const { extraProfileFields, mapProfile } = options ?? {};
|
|
75
|
-
return async (props, req) => {
|
|
76
|
-
const cookie = getVtexCookies(req);
|
|
77
|
-
try {
|
|
78
|
-
const profile = await getCurrentProfile(cookie, extraProfileFields);
|
|
79
|
-
const data = mapProfile ? mapProfile(profile) : profile;
|
|
80
|
-
return {
|
|
81
|
-
...props,
|
|
82
|
-
device: getDevice(req),
|
|
83
|
-
logged: !!profile,
|
|
84
|
-
loading: false,
|
|
85
|
-
userData: data,
|
|
86
|
-
};
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error("[accountLoaders.personalData]", error);
|
|
89
|
-
return { ...props, device: getDevice(req), logged: false, loading: false, userData: null };
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// orders — checks login status for order listing sections
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
function orders(): SectionLoaderFn {
|
|
99
|
-
return async (props, req) => {
|
|
100
|
-
const cookie = getVtexCookies(req);
|
|
101
|
-
try {
|
|
102
|
-
const userData = await getUser(cookie);
|
|
103
|
-
return { ...props, device: getDevice(req), logged: !!userData };
|
|
104
|
-
} catch {
|
|
105
|
-
return { ...props, device: getDevice(req), logged: false };
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
// cards — fetches saved payment tokens for card management sections
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
function cards(): SectionLoaderFn {
|
|
115
|
-
return async (props, req) => {
|
|
116
|
-
const cookie = getVtexCookies(req);
|
|
117
|
-
try {
|
|
118
|
-
const user = await getUser(cookie);
|
|
119
|
-
const logged = !!user;
|
|
120
|
-
let payments: Payment[] = [];
|
|
121
|
-
if (logged) {
|
|
122
|
-
try {
|
|
123
|
-
payments = (await getUserPayments(cookie)) ?? [];
|
|
124
|
-
} catch {
|
|
125
|
-
payments = [];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return { ...props, logged, payments };
|
|
129
|
-
} catch {
|
|
130
|
-
return { ...props, logged: false, payments: [] };
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// addresses — fetches address list for address management sections
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
function addresses(): SectionLoaderFn {
|
|
140
|
-
return async (props, req) => {
|
|
141
|
-
const cookie = getVtexCookies(req);
|
|
142
|
-
try {
|
|
143
|
-
const userData = await getUser(cookie);
|
|
144
|
-
const logged = !!userData;
|
|
145
|
-
let userAddressData: VtexAddress[] | null = null;
|
|
146
|
-
if (logged) {
|
|
147
|
-
try {
|
|
148
|
-
userAddressData = await getUserAddresses(cookie);
|
|
149
|
-
} catch {
|
|
150
|
-
userAddressData = null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return { ...props, device: getDevice(req), logged, userAddressData };
|
|
154
|
-
} catch {
|
|
155
|
-
return { ...props, device: getDevice(req), logged: false, userAddressData: null };
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
// authentication — checks login + returns user data for auth pages
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
function authentication(): SectionLoaderFn {
|
|
165
|
-
return async (props, req) => {
|
|
166
|
-
const cookie = getVtexCookies(req);
|
|
167
|
-
try {
|
|
168
|
-
const userData = await getUser(cookie);
|
|
169
|
-
return { ...props, device: getDevice(req), logged: !!userData, userData };
|
|
170
|
-
} catch {
|
|
171
|
-
return { ...props, device: getDevice(req), logged: false, userData: null };
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
// loggedIn — generic "is the user logged in?" loader
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
function loggedIn(): SectionLoaderFn {
|
|
181
|
-
return async (props, req) => {
|
|
182
|
-
const cookie = getVtexCookies(req);
|
|
183
|
-
try {
|
|
184
|
-
const userData = await getUser(cookie);
|
|
185
|
-
return { ...props, device: getDevice(req), logged: !!userData };
|
|
186
|
-
} catch {
|
|
187
|
-
return { ...props, device: getDevice(req), logged: false };
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
|
-
// Public API
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
|
|
196
|
-
export const vtexAccountLoaders = {
|
|
197
|
-
personalData,
|
|
198
|
-
orders,
|
|
199
|
-
cards,
|
|
200
|
-
addresses,
|
|
201
|
-
authentication,
|
|
202
|
-
loggedIn,
|
|
203
|
-
} as const;
|
package/src/utils/authHelpers.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VTEX auth helpers — pure functions for cookie extraction, JWT parsing,
|
|
3
|
-
* Set-Cookie forwarding, and logout.
|
|
4
|
-
*
|
|
5
|
-
* These are consumed by site-level createServerFn wrappers in invoke.ts.
|
|
6
|
-
* createServerFn itself must live in site source (not node_modules) because
|
|
7
|
-
* TanStack Start's Vite plugin only transforms source files.
|
|
8
|
-
*/
|
|
9
|
-
import { getVtexConfig, getVtexFetch } from "../client";
|
|
10
|
-
import { extractVtexCookies } from "./cookieSanitizer";
|
|
11
|
-
|
|
12
|
-
const DOMAIN_RE = /;\s*domain=[^;]*/gi;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Extract VTEX-relevant cookies from a raw Cookie header string.
|
|
16
|
-
*
|
|
17
|
-
* Strict allowlist: drops any cookie not on `VTEX_COOKIE_PREFIXES`, plus
|
|
18
|
-
* any cookie whose value contains non-ASCII bytes (which would otherwise
|
|
19
|
-
* make VTEX's janus gateway return 503 Service Unavailable). Both filters
|
|
20
|
-
* live in `./cookieSanitizer` — this is a thin compatibility wrapper.
|
|
21
|
-
*/
|
|
22
|
-
export function extractVtexCookiesFromHeader(raw: string): string {
|
|
23
|
-
return extractVtexCookies(raw);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Strip Domain= from Set-Cookie headers so cookies are associated
|
|
28
|
-
* with the storefront domain instead of the VTEX domain.
|
|
29
|
-
*/
|
|
30
|
-
export function stripCookieDomain(cookies: string[]): string[] {
|
|
31
|
-
return cookies.map((c) => c.replace(DOMAIN_RE, ""));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Standard VTEX cookies to expire on logout. */
|
|
35
|
-
export const VTEX_LOGOUT_COOKIES = [
|
|
36
|
-
"checkout.vtex.com=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax",
|
|
37
|
-
"CheckoutOrderFormOwnership=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax",
|
|
38
|
-
"checkout.vtex.com__orderFormId=; Path=/; Max-Age=0",
|
|
39
|
-
"vtex_session=; Path=/; Max-Age=0",
|
|
40
|
-
"vtex_segment=; Path=/; Max-Age=0",
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Perform VTEX logout — calls the VTEX ID logout endpoint and returns
|
|
45
|
-
* the Set-Cookie headers (with domain stripped) to expire auth cookies.
|
|
46
|
-
*/
|
|
47
|
-
export async function performVtexLogout(cookies: string): Promise<{ setCookies: string[] }> {
|
|
48
|
-
const config = getVtexConfig();
|
|
49
|
-
const domain = config.domain ?? "com.br";
|
|
50
|
-
const logoutUrl = `https://${config.account}.vtexcommercestable.${domain}/api/vtexid/pub/logout?scope=${config.account}&returnUrl=/`;
|
|
51
|
-
|
|
52
|
-
const res = await getVtexFetch()(logoutUrl, {
|
|
53
|
-
method: "GET",
|
|
54
|
-
headers: { cookie: cookies },
|
|
55
|
-
redirect: "manual",
|
|
56
|
-
operation: "vtexid.logout",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const upstreamCookies = res.headers.getSetCookie?.() ?? [];
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
setCookies: [...stripCookieDomain(upstreamCookies), ...VTEX_LOGOUT_COOKIES],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Parse VTEX auth JWT to extract email and userId.
|
|
68
|
-
* Reads the VtexIdclientAutCookie_* cookie from a raw Cookie header.
|
|
69
|
-
*/
|
|
70
|
-
export function parseVtexAuthJwt(rawCookies: string): { email: string; userId: string } | null {
|
|
71
|
-
try {
|
|
72
|
-
const match = rawCookies.match(/VtexIdclientAutCookie_[^=]+=([^;]+)/);
|
|
73
|
-
if (!match) return null;
|
|
74
|
-
const token = match[1];
|
|
75
|
-
const parts = token.split(".");
|
|
76
|
-
if (parts.length < 2) return null;
|
|
77
|
-
const payload = JSON.parse(
|
|
78
|
-
Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8"),
|
|
79
|
-
);
|
|
80
|
-
if (!payload.sub) return null;
|
|
81
|
-
return { email: payload.sub, userId: payload.userId ?? "" };
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
}
|
package/src/utils/batch.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export const batch = <T>(iterable: IterableIterator<T> | T[], size: number): T[][] => {
|
|
2
|
-
const batches: T[][] = [];
|
|
3
|
-
|
|
4
|
-
let current = 0;
|
|
5
|
-
for (const item of iterable) {
|
|
6
|
-
if (batches[current]?.length === size) {
|
|
7
|
-
current++;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
if (!batches[current]) {
|
|
11
|
-
batches[current] = [];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
batches[current].push(item);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return batches;
|
|
18
|
-
};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project a VTEX OrderForm down to a requested `CartProjection`.
|
|
3
|
-
*
|
|
4
|
-
* The VTEX checkout API always returns the whole OrderForm from mutation
|
|
5
|
-
* endpoints (bounded only by `expectedOrderFormSections`) — there is no
|
|
6
|
-
* delta/patch response. So "return the minimum" is enforced **server-side**:
|
|
7
|
-
* we take whatever VTEX gave us and shape it into `none` / `summary` /
|
|
8
|
-
* `summary+items` / `minicart` / `raw` before it reaches the browser.
|
|
9
|
-
*
|
|
10
|
-
* Pure function — no I/O, fully unit-testable. All monetary values in the
|
|
11
|
-
* projected output are in major units (VTEX is cents).
|
|
12
|
-
*
|
|
13
|
-
* @see `@decocms/apps-commerce/types/cart` for the agnostic contract.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type {
|
|
17
|
-
CartItemSlim,
|
|
18
|
-
CartOk,
|
|
19
|
-
CartProjection,
|
|
20
|
-
CartSummary,
|
|
21
|
-
CartSummaryWithItems,
|
|
22
|
-
Minicart,
|
|
23
|
-
} from "@decocms/apps-commerce/types";
|
|
24
|
-
import type { OrderForm, OrderFormItem } from "../types";
|
|
25
|
-
import { type VtexOrderFormToMinicartOptions, vtexOrderFormToMinicart } from "./minicart";
|
|
26
|
-
|
|
27
|
-
const CENTS_PER_MAJOR = 100;
|
|
28
|
-
|
|
29
|
-
function fromCents(cents: number | undefined | null): number {
|
|
30
|
-
if (cents == null || !Number.isFinite(cents)) return 0;
|
|
31
|
-
return cents / CENTS_PER_MAJOR;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Sum of line quantities. */
|
|
35
|
-
function countItems(orderForm: OrderForm): number {
|
|
36
|
-
return (orderForm.items ?? []).reduce((sum, i) => sum + (i.quantity ?? 0), 0);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function toSummary(orderForm: OrderForm): CartSummary {
|
|
40
|
-
return {
|
|
41
|
-
orderFormId: orderForm.orderFormId ?? null,
|
|
42
|
-
totalItems: countItems(orderForm),
|
|
43
|
-
total: fromCents(orderForm.value),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Map a VTEX line to the slim, analytics-compatible `CartItemSlim`. */
|
|
48
|
-
function toSlimItem(item: OrderFormItem): CartItemSlim {
|
|
49
|
-
return {
|
|
50
|
-
item_id: item.id,
|
|
51
|
-
item_name: item.name ?? item.skuName ?? "",
|
|
52
|
-
item_variant: item.skuName,
|
|
53
|
-
image: item.imageUrl?.replace(/^http:/, "https:") ?? "",
|
|
54
|
-
price: fromCents(item.sellingPrice ?? item.price),
|
|
55
|
-
quantity: item.quantity,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export type ProjectOrderFormOptions = VtexOrderFormToMinicartOptions;
|
|
60
|
-
|
|
61
|
-
/** The projected result type, with VTEX-concrete `minicart`/`raw` payloads. */
|
|
62
|
-
export type VtexCartProjectionResult =
|
|
63
|
-
| CartOk
|
|
64
|
-
| CartSummary
|
|
65
|
-
| CartSummaryWithItems
|
|
66
|
-
| Minicart<OrderForm>
|
|
67
|
-
| OrderForm;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Shape a VTEX OrderForm into the requested projection.
|
|
71
|
-
*
|
|
72
|
-
* @param orderForm - Raw OrderForm from a VTEX checkout call.
|
|
73
|
-
* @param projection - Desired client-facing shape.
|
|
74
|
-
* @param opts - Storefront overrides forwarded to the `minicart` projection.
|
|
75
|
-
*/
|
|
76
|
-
export function projectOrderForm(
|
|
77
|
-
orderForm: OrderForm,
|
|
78
|
-
projection: CartProjection,
|
|
79
|
-
opts: ProjectOrderFormOptions = {},
|
|
80
|
-
): VtexCartProjectionResult {
|
|
81
|
-
switch (projection) {
|
|
82
|
-
case "none":
|
|
83
|
-
return { ok: true };
|
|
84
|
-
case "summary":
|
|
85
|
-
return toSummary(orderForm);
|
|
86
|
-
case "summary+items":
|
|
87
|
-
return {
|
|
88
|
-
...toSummary(orderForm),
|
|
89
|
-
items: (orderForm.items ?? []).map(toSlimItem),
|
|
90
|
-
};
|
|
91
|
-
case "minicart":
|
|
92
|
-
return vtexOrderFormToMinicart(orderForm, opts);
|
|
93
|
-
case "raw":
|
|
94
|
-
return orderForm;
|
|
95
|
-
}
|
|
96
|
-
}
|
package/src/utils/cookies.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
interface Cookie {
|
|
2
|
-
name: string;
|
|
3
|
-
value: string;
|
|
4
|
-
domain?: string;
|
|
5
|
-
path?: string;
|
|
6
|
-
expires?: Date;
|
|
7
|
-
maxAge?: number;
|
|
8
|
-
secure?: boolean;
|
|
9
|
-
httpOnly?: boolean;
|
|
10
|
-
sameSite?: "Strict" | "Lax" | "None";
|
|
11
|
-
}
|
|
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 eqi = attr.indexOf("=");
|
|
24
|
-
const k = (eqi >= 0 ? attr.slice(0, eqi) : attr).trim();
|
|
25
|
-
const v = eqi >= 0 ? attr.slice(eqi + 1).trim() : "";
|
|
26
|
-
const lower = k.toLowerCase();
|
|
27
|
-
if (lower === "domain") cookie.domain = v;
|
|
28
|
-
else if (lower === "path") cookie.path = v;
|
|
29
|
-
else if (lower === "secure") cookie.secure = true;
|
|
30
|
-
else if (lower === "httponly") cookie.httpOnly = true;
|
|
31
|
-
else if (lower === "samesite") cookie.sameSite = v as Cookie["sameSite"];
|
|
32
|
-
else if (lower === "max-age") {
|
|
33
|
-
const n = Number(v);
|
|
34
|
-
if (!Number.isNaN(n)) cookie.maxAge = n;
|
|
35
|
-
} else if (lower === "expires") {
|
|
36
|
-
const d = new Date(v);
|
|
37
|
-
if (!Number.isNaN(d.getTime())) cookie.expires = d;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return cookie;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Extract individual Set-Cookie values from a Headers object.
|
|
45
|
-
*
|
|
46
|
-
* Uses Headers.getSetCookie() (available in Cloudflare Workers and Node 18+)
|
|
47
|
-
* which returns each Set-Cookie as a separate string — unlike Headers.get()
|
|
48
|
-
* or Headers.forEach() which join multiple values with ", " and corrupt
|
|
49
|
-
* cookie strings that contain commas in Expires dates.
|
|
50
|
-
*/
|
|
51
|
-
function getSetCookies(headers: Headers): Cookie[] {
|
|
52
|
-
const rawCookies: string[] =
|
|
53
|
-
typeof headers.getSetCookie === "function"
|
|
54
|
-
? headers.getSetCookie()
|
|
55
|
-
: getRawSetCookiesFallback(headers);
|
|
56
|
-
|
|
57
|
-
const cookies: Cookie[] = [];
|
|
58
|
-
for (const raw of rawCookies) {
|
|
59
|
-
const cookie = parseSingleSetCookie(raw);
|
|
60
|
-
if (cookie) cookies.push(cookie);
|
|
61
|
-
}
|
|
62
|
-
return cookies;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Fallback for runtimes without Headers.getSetCookie().
|
|
67
|
-
* Splits the comma-joined string heuristically — not perfect for cookies
|
|
68
|
-
* with Expires containing commas, but better than the old approach.
|
|
69
|
-
*/
|
|
70
|
-
function getRawSetCookiesFallback(headers: Headers): string[] {
|
|
71
|
-
const joined = headers.get("set-cookie");
|
|
72
|
-
if (!joined) return [];
|
|
73
|
-
const results: string[] = [];
|
|
74
|
-
let current = "";
|
|
75
|
-
for (const segment of joined.split(",")) {
|
|
76
|
-
const trimmed = segment.trimStart();
|
|
77
|
-
const looksLikeNewCookie = /^[^=;]+=[^;]/.test(trimmed) && current.length > 0;
|
|
78
|
-
if (looksLikeNewCookie) {
|
|
79
|
-
results.push(current.trim());
|
|
80
|
-
current = trimmed;
|
|
81
|
-
} else {
|
|
82
|
-
current += (current ? "," : "") + segment;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (current.trim()) results.push(current.trim());
|
|
86
|
-
return results;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function setCookie(headers: Headers, cookie: Cookie): void {
|
|
90
|
-
let str = `${cookie.name}=${cookie.value}`;
|
|
91
|
-
if (cookie.domain) str += `; Domain=${cookie.domain}`;
|
|
92
|
-
if (cookie.path) str += `; Path=${cookie.path}`;
|
|
93
|
-
if (cookie.secure) str += "; Secure";
|
|
94
|
-
if (cookie.httpOnly) str += "; HttpOnly";
|
|
95
|
-
if (cookie.sameSite) str += `; SameSite=${cookie.sameSite}`;
|
|
96
|
-
if (cookie.maxAge != null) str += `; Max-Age=${cookie.maxAge}`;
|
|
97
|
-
if (cookie.expires) str += `; Expires=${cookie.expires.toUTCString()}`;
|
|
98
|
-
headers.append("Set-Cookie", str);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export const stringify = (cookies: Record<string, string>) =>
|
|
102
|
-
Object.entries(cookies)
|
|
103
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
104
|
-
.join("; ");
|
|
105
|
-
|
|
106
|
-
export const proxySetCookie = (from: Headers, to: Headers, toDomain?: URL | string) => {
|
|
107
|
-
const newDomain = toDomain && new URL(toDomain);
|
|
108
|
-
|
|
109
|
-
for (const cookie of getSetCookies(from)) {
|
|
110
|
-
const newCookie = newDomain
|
|
111
|
-
? {
|
|
112
|
-
...cookie,
|
|
113
|
-
domain: newDomain.hostname,
|
|
114
|
-
}
|
|
115
|
-
: cookie;
|
|
116
|
-
|
|
117
|
-
setCookie(to, newCookie);
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
export const CHECKOUT_DATA_ACCESS_COOKIE = "CheckoutDataAccess";
|
|
122
|
-
export const VTEX_CHKO_AUTH = "Vtex_CHKO_Auth";
|
|
123
|
-
|
|
124
|
-
// Re-export the canonical allowlist from cookieSanitizer so consumers that
|
|
125
|
-
// previously imported it from this module keep working. The single source
|
|
126
|
-
// of truth lives in cookieSanitizer.ts.
|
|
127
|
-
export { VTEX_COOKIE_PREFIXES } from "./cookieSanitizer";
|
|
128
|
-
|
|
129
|
-
import { extractVtexCookies } from "./cookieSanitizer";
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Filter a request's cookies to only VTEX-relevant ones.
|
|
133
|
-
*
|
|
134
|
-
* Strict allowlist: drops any cookie not on `VTEX_COOKIE_PREFIXES` plus any
|
|
135
|
-
* cookie whose value contains non-ASCII bytes (which would make VTEX's
|
|
136
|
-
* janus gateway return 503).
|
|
137
|
-
*/
|
|
138
|
-
export function getVtexCookies(request: Request): string {
|
|
139
|
-
return extractVtexCookies(request.headers.get("cookie") ?? "");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Ensure the unsuffixed VtexIdclientAutCookie is present alongside the
|
|
144
|
-
* account-suffixed variant (e.g. VtexIdclientAutCookie_myaccount).
|
|
145
|
-
*
|
|
146
|
-
* VTEX GraphQL requires both the suffixed AND unsuffixed cookie for
|
|
147
|
-
* authenticated mutations. The browser only stores the suffixed variant,
|
|
148
|
-
* so server-side code must synthesize the unsuffixed one.
|
|
149
|
-
*/
|
|
150
|
-
export function ensureUnsuffixedAuthCookie(cookieStr: string): string {
|
|
151
|
-
if (!cookieStr) return cookieStr;
|
|
152
|
-
const cookies = cookieStr.split(";").map((c) => c.trim());
|
|
153
|
-
let hasUnsuffixed = false;
|
|
154
|
-
let suffixedToken: string | null = null;
|
|
155
|
-
for (const c of cookies) {
|
|
156
|
-
const [name, ...rest] = c.split("=");
|
|
157
|
-
if (name === "VtexIdclientAutCookie") {
|
|
158
|
-
hasUnsuffixed = true;
|
|
159
|
-
} else if (name?.startsWith("VtexIdclientAutCookie_") && !suffixedToken) {
|
|
160
|
-
suffixedToken = rest.join("=");
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (!hasUnsuffixed && suffixedToken) {
|
|
164
|
-
return `VtexIdclientAutCookie=${suffixedToken}; ${cookieStr}`;
|
|
165
|
-
}
|
|
166
|
-
return cookieStr;
|
|
167
|
-
}
|