@decocms/apps 0.27.0 → 0.28.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.
@@ -16,25 +16,37 @@ export interface CreateDocumentResult {
16
16
  DocumentId: string;
17
17
  }
18
18
 
19
- export async function createDocument(
20
- entity: string,
21
- data: Record<string, any>,
22
- ): Promise<CreateDocumentResult> {
19
+ export interface CreateDocumentProps {
20
+ entity: string;
21
+ data: Record<string, any>;
22
+ }
23
+
24
+ export async function createDocument(props: CreateDocumentProps): Promise<CreateDocumentResult> {
25
+ const { entity, data } = props;
23
26
  return vtexFetch<CreateDocumentResult>(`/api/dataentities/${entity}/documents`, {
24
27
  method: "POST",
25
28
  body: JSON.stringify(removeEmptyFields(data)),
26
29
  });
27
30
  }
28
31
 
29
- export async function getDocument<T = unknown>(entity: string, documentId: string): Promise<T> {
32
+ export interface GetDocumentProps {
33
+ entity: string;
34
+ documentId: string;
35
+ }
36
+
37
+ export async function getDocument<T = unknown>(props: GetDocumentProps): Promise<T> {
38
+ const { entity, documentId } = props;
30
39
  return vtexFetch<T>(`/api/dataentities/${entity}/documents/${documentId}`);
31
40
  }
32
41
 
33
- export async function patchDocument(
34
- entity: string,
35
- documentId: string,
36
- data: Record<string, any>,
37
- ): Promise<void> {
42
+ export interface PatchDocumentProps {
43
+ entity: string;
44
+ documentId: string;
45
+ data: Record<string, any>;
46
+ }
47
+
48
+ export async function patchDocument(props: PatchDocumentProps): Promise<void> {
49
+ const { entity, documentId, data } = props;
38
50
  await vtexFetch<any>(`/api/dataentities/${entity}/documents/${documentId}`, {
39
51
  method: "PATCH",
40
52
  body: JSON.stringify(removeEmptyFields(data)),
@@ -49,13 +61,18 @@ export interface MasterDataSearchResult {
49
61
  [key: string]: any;
50
62
  }
51
63
 
64
+ export interface SearchDocumentsProps {
65
+ entity: string;
66
+ filter: string;
67
+ }
68
+
52
69
  /**
53
70
  * Simple search — kept for backward compat.
54
71
  */
55
72
  export async function searchDocuments<T = MasterDataSearchResult>(
56
- entity: string,
57
- filter: string,
73
+ props: SearchDocumentsProps,
58
74
  ): Promise<T[]> {
75
+ const { entity, filter } = props;
59
76
  return vtexFetch<T[]>(`/api/dataentities/${entity}/search?_where=${encodeURIComponent(filter)}`);
60
77
  }
61
78
 
@@ -65,7 +82,7 @@ export async function searchDocuments<T = MasterDataSearchResult>(
65
82
  *
66
83
  * @see https://developers.vtex.com/docs/api-reference/masterdata-api#get-/api/dataentities/-acronym-/search
67
84
  */
68
- export interface SearchDocumentsOpts {
85
+ export interface SearchDocumentsFullProps {
69
86
  acronym: string;
70
87
  fields?: string;
71
88
  where?: string;
@@ -74,14 +91,12 @@ export interface SearchDocumentsOpts {
74
91
  take?: number;
75
92
  /** @default 0 */
76
93
  skip?: number;
77
- /** Auth cookie header for authenticated queries */
78
- cookieHeader?: string;
79
94
  }
80
95
 
81
96
  export async function searchDocumentsFull<T = Record<string, unknown>>(
82
- opts: SearchDocumentsOpts,
97
+ props: SearchDocumentsFullProps,
83
98
  ): Promise<T[]> {
84
- const { acronym, fields, where, sort, skip = 0, take = 10, cookieHeader } = opts;
99
+ const { acronym, fields, where, sort, skip = 0, take = 10 } = props;
85
100
  const from = Math.max(skip, 0);
86
101
  const to = from + Math.min(100, take);
87
102
 
@@ -95,7 +110,6 @@ export async function searchDocumentsFull<T = Record<string, unknown>>(
95
110
  "content-type": "application/json",
96
111
  "REST-Range": `resources=${from}-${to}`,
97
112
  };
98
- if (cookieHeader) headers.cookie = cookieHeader;
99
113
 
100
114
  return vtexFetchResponse(`/api/dataentities/${acronym}/search?${params}`, {
101
115
  headers,
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * VTEX Sessions API actions.
3
- * All session-mutating actions return Set-Cookie headers for propagation.
3
+ * Cookie forwarding happens automatically via RequestContext.responseHeaders.
4
4
  */
5
5
 
6
- import type { VtexFetchResult } from "../client";
7
6
  import { getVtexConfig, vtexFetchWithCookies, vtexIOGraphQL } from "../client";
8
7
  import { buildAuthCookieHeader } from "../utils/vtexId";
9
8
 
@@ -16,16 +15,15 @@ export interface SessionData {
16
15
  namespaces: Record<string, Record<string, { value: string }>>;
17
16
  }
18
17
 
19
- export async function createSession(
20
- data: Record<string, any>,
21
- cookieHeader?: string,
22
- ): Promise<VtexFetchResult<SessionData>> {
23
- const headers: Record<string, string> = {};
24
- if (cookieHeader) headers.cookie = cookieHeader;
18
+ export interface CreateSessionProps {
19
+ data: Record<string, any>;
20
+ }
21
+
22
+ export async function createSession(props: CreateSessionProps): Promise<SessionData> {
23
+ const { data } = props;
25
24
  return vtexFetchWithCookies<SessionData>("/api/sessions", {
26
25
  method: "POST",
27
26
  body: JSON.stringify(data),
28
- headers,
29
27
  });
30
28
  }
31
29
 
@@ -38,21 +36,17 @@ export interface EditSessionResponse {
38
36
  namespaces: Record<string, Record<string, { value: string }>>;
39
37
  }
40
38
 
39
+ export interface EditSessionProps {
40
+ public: Record<string, { value: string }>;
41
+ }
42
+
41
43
  /**
42
44
  * Edit the current VTEX session (public properties).
43
- * Returns data + Set-Cookie headers.
44
45
  */
45
- export async function editSession(
46
- publicProperties: Record<string, { value: string }>,
47
- cookieHeader?: string,
48
- ): Promise<VtexFetchResult<EditSessionResponse>> {
49
- const headers: Record<string, string> = {};
50
- if (cookieHeader) headers.cookie = cookieHeader;
51
-
46
+ export async function editSession(props: EditSessionProps): Promise<EditSessionResponse> {
52
47
  return vtexFetchWithCookies<EditSessionResponse>("/api/sessions", {
53
48
  method: "PATCH",
54
- body: JSON.stringify({ public: { ...publicProperties } }),
55
- headers,
49
+ body: JSON.stringify({ public: { ...props.public } }),
56
50
  });
57
51
  }
58
52
 
@@ -68,14 +62,17 @@ const DELETE_SESSION_MUTATION = `mutation LogOutFromSession($sessionId: ID) {
68
62
  logOutFromSession(sessionId: $sessionId) @context(provider: "vtex.store-graphql@2.x")
69
63
  }`;
70
64
 
65
+ export interface DeleteSessionProps {
66
+ sessionId: string;
67
+ authCookie: string;
68
+ }
69
+
71
70
  /**
72
71
  * Log out / delete a VTEX session via the store-graphql mutation.
73
72
  * Requires a valid auth cookie.
74
73
  */
75
- export async function deleteSession(
76
- sessionId: string,
77
- authCookie: string,
78
- ): Promise<DeleteSessionResponse> {
74
+ export async function deleteSession(props: DeleteSessionProps): Promise<DeleteSessionResponse> {
75
+ const { sessionId, authCookie } = props;
79
76
  if (!authCookie) throw new Error("Auth cookie is required to delete session");
80
77
  const { account } = getVtexConfig();
81
78
  return vtexIOGraphQL<DeleteSessionResponse>(
package/vtex/client.ts CHANGED
@@ -3,9 +3,29 @@
3
3
  * Uses VTEX's public REST APIs (Intelligent Search + Catalog + Checkout).
4
4
  */
5
5
 
6
+ import { RequestContext } from "@decocms/start/sdk/requestContext";
6
7
  import { type FetchCacheOptions, fetchWithCache } from "./utils/fetchCache";
7
8
  import { parseSegment, SEGMENT_COOKIE_NAME } from "./utils/segment";
8
9
 
10
+ /**
11
+ * Get the response headers from RequestContext.
12
+ * Uses `responseHeaders` when available (@decocms/start PR#57),
13
+ * falls back to the bag with a lazily-created Headers instance.
14
+ * TODO: Remove fallback once @decocms/start PR#57 is published.
15
+ */
16
+ function getResponseHeaders(): Headers | null {
17
+ const ctx = RequestContext.current;
18
+ if (!ctx) return null;
19
+ // biome-ignore lint/suspicious/noExplicitAny: forward-compat with upcoming responseHeaders property
20
+ if ((ctx as any).responseHeaders instanceof Headers) return (ctx as any).responseHeaders;
21
+ let headers = ctx.bag.get("responseHeaders") as Headers | undefined;
22
+ if (!headers) {
23
+ headers = new Headers();
24
+ ctx.bag.set("responseHeaders", headers);
25
+ }
26
+ return headers;
27
+ }
28
+
9
29
  // ---------------------------------------------------------------------------
10
30
  // URL sanitization (ported from deco-cx/apps vtex/utils/fetchVTEX.ts)
11
31
  // ---------------------------------------------------------------------------
@@ -82,8 +102,6 @@ export interface VtexConfig {
82
102
 
83
103
  let _config: VtexConfig | null = null;
84
104
  let _fetch: typeof fetch = globalThis.fetch;
85
- let _getCookieHeader: (() => string | undefined) | null = null;
86
- let _forwardSetCookies: ((cookies: string[]) => void) | null = null;
87
105
 
88
106
  export function configureVtex(config: VtexConfig) {
89
107
  _config = config;
@@ -105,37 +123,6 @@ export function setVtexFetch(fetchFn: typeof fetch) {
105
123
  _fetch = fetchFn;
106
124
  }
107
125
 
108
- /**
109
- * Register a provider that returns the Cookie header from the current request.
110
- * Called automatically by vtexFetchWithCookies when no explicit cookieHeader
111
- * is present in the request init — so checkout/session/auth actions
112
- * transparently forward browser cookies to the VTEX API.
113
- *
114
- * @example
115
- * ```ts
116
- * import { getRequestHeader } from "@tanstack/react-start/server";
117
- * setRequestCookieProvider(() => getRequestHeader("cookie") ?? undefined);
118
- * ```
119
- */
120
- export function setRequestCookieProvider(fn: () => string | undefined) {
121
- _getCookieHeader = fn;
122
- }
123
-
124
- /**
125
- * Register a callback that forwards VTEX Set-Cookie headers back to the browser.
126
- * Called automatically by vtexFetchWithCookies after every response that
127
- * carries Set-Cookie headers.
128
- *
129
- * @example
130
- * ```ts
131
- * import { setResponseHeader } from "@tanstack/react-start/server";
132
- * setResponseCookieForwarder((cookies) => setResponseHeader("set-cookie", cookies));
133
- * ```
134
- */
135
- export function setResponseCookieForwarder(fn: (cookies: string[]) => void) {
136
- _forwardSetCookies = fn;
137
- }
138
-
139
126
  export function getVtexConfig(): VtexConfig {
140
127
  if (!_config) throw new Error("VTEX not configured. Call configureVtex() first.");
141
128
  return _config;
@@ -174,11 +161,12 @@ function authHeaders(): Record<string, string> {
174
161
 
175
162
  /**
176
163
  * Read regionId from the current request's vtex_segment cookie.
177
- * Returns null when no cookie provider is registered or no regionId is set.
164
+ * Returns null when outside a request context or no regionId is set.
178
165
  */
179
166
  function extractRegionIdFromCookies(): string | null {
180
- if (!_getCookieHeader) return null;
181
- const cookies = _getCookieHeader();
167
+ const ctx = RequestContext.current;
168
+ if (!ctx) return null;
169
+ const cookies = ctx.request.headers.get("cookie");
182
170
  if (!cookies) return null;
183
171
  const match = cookies.match(new RegExp(`(?:^|;\\s*)${SEGMENT_COOKIE_NAME}=([^;]+)`));
184
172
  if (!match?.[1]) return null;
@@ -240,56 +228,43 @@ export async function vtexCachedFetch<T>(
240
228
  }
241
229
 
242
230
  /**
243
- * Result type for actions that need to propagate VTEX Set-Cookie headers.
244
- * In TanStack Start, the caller (server function) is responsible for
245
- * forwarding these cookies to the client via `setCookie` from vinxi/http.
246
- */
247
- export interface VtexFetchResult<T> {
248
- data: T;
249
- setCookies: string[];
250
- }
251
-
252
- /**
253
- * Like vtexFetch, but also returns Set-Cookie headers from the response.
231
+ * Like vtexFetch, but also forwards Set-Cookie headers via RequestContext.
254
232
  * Use for checkout, session, and auth actions that set cookies.
233
+ *
234
+ * Cookie propagation happens automatically:
235
+ * - Reads the browser's Cookie header from RequestContext.request
236
+ * - Writes upstream Set-Cookie headers to RequestContext.responseHeaders
237
+ * - The invoke handler copies responseHeaders into the HTTP Response
238
+ *
239
+ * This mirrors deco-cx/deco's `proxySetCookie(response.headers, ctx.response.headers)`.
255
240
  */
256
- export async function vtexFetchWithCookies<T>(
257
- path: string,
258
- init?: RequestInit,
259
- ): Promise<VtexFetchResult<T>> {
260
- // Auto-inject request cookies when no explicit cookie header is set
261
- if (_getCookieHeader) {
262
- const existingHeaders = init?.headers as Record<string, string> | undefined;
263
- if (!existingHeaders?.["cookie"]) {
264
- const cookies = _getCookieHeader();
265
- if (cookies) {
266
- init = {
267
- ...init,
268
- headers: { ...existingHeaders, cookie: cookies },
269
- };
270
- }
241
+ export async function vtexFetchWithCookies<T>(path: string, init?: RequestInit): Promise<T> {
242
+ // Auto-inject request cookies from RequestContext
243
+ const existingHeaders = init?.headers as Record<string, string> | undefined;
244
+ if (!existingHeaders?.["cookie"]) {
245
+ const ctx = RequestContext.current;
246
+ const cookies = ctx?.request.headers.get("cookie");
247
+ if (cookies) {
248
+ init = { ...init, headers: { ...existingHeaders, cookie: cookies } };
271
249
  }
272
250
  }
273
251
 
274
252
  const response = await vtexFetchResponse(path, init);
275
253
  const data = (await response.json()) as T;
276
- const setCookies: string[] = [];
277
- if (typeof response.headers.getSetCookie === "function") {
278
- setCookies.push(...response.headers.getSetCookie());
279
- } else {
280
- response.headers.forEach((value, key) => {
281
- if (key.toLowerCase() === "set-cookie") {
282
- setCookies.push(value);
283
- }
284
- });
285
- }
286
254
 
287
- // Auto-forward Set-Cookie headers to the browser response
288
- if (_forwardSetCookies && setCookies.length) {
289
- _forwardSetCookies(setCookies);
255
+ // Forward Set-Cookie headers to RequestContext.responseHeaders
256
+ // (mirrors proxySetCookie from deco-cx/deco)
257
+ const responseHeaders = getResponseHeaders();
258
+ if (responseHeaders) {
259
+ const setCookies =
260
+ typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [];
261
+ for (const cookie of setCookies) {
262
+ const stripped = cookie.replace(/;\s*domain=[^;]*/gi, "");
263
+ responseHeaders.append("set-cookie", stripped);
264
+ }
290
265
  }
291
266
 
292
- return { data, setCookies };
267
+ return data;
293
268
  }
294
269
 
295
270
  export async function intelligentSearch<T>(
package/vtex/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * VTEX app entry point for @decocms/apps.
3
- * Re-exports client config + initializer.
3
+ * Re-exports client config + initializer + app contract.
4
4
  *
5
5
  * For actions/loaders/utils, use sub-path imports:
6
6
  * import { addItemsToCart } from "@decocms/apps/vtex/actions/checkout"
@@ -12,3 +12,4 @@
12
12
  * import { searchProducts } from "@decocms/apps/vtex/loaders"
13
13
  */
14
14
  export * from "./client";
15
+ export { configure, type VtexState } from "./mod";
@@ -0,0 +1,75 @@
1
+ // AUTO-GENERATED by scripts/generate-manifests.ts — DO NOT EDIT
2
+ // This file is checked into source control and updated via: npm run generate:manifests
3
+
4
+ import * as actions_address from "./actions/address";
5
+ import * as actions_auth from "./actions/auth";
6
+ import * as actions_checkout from "./actions/checkout";
7
+ import * as actions_masterData from "./actions/masterData";
8
+ import * as actions_misc from "./actions/misc";
9
+ import * as actions_newsletter from "./actions/newsletter";
10
+ import * as actions_orders from "./actions/orders";
11
+ import * as actions_profile from "./actions/profile";
12
+ import * as actions_session from "./actions/session";
13
+ import * as actions_trigger from "./actions/trigger";
14
+ import * as actions_wishlist from "./actions/wishlist";
15
+ import * as loaders_address from "./loaders/address";
16
+ import * as loaders_brands from "./loaders/brands";
17
+ import * as loaders_cart from "./loaders/cart";
18
+ import * as loaders_catalog from "./loaders/catalog";
19
+ import * as loaders_collections from "./loaders/collections";
20
+ import * as loaders_legacy from "./loaders/legacy";
21
+ import * as loaders_logistics from "./loaders/logistics";
22
+ import * as loaders_navbar from "./loaders/navbar";
23
+ import * as loaders_orders from "./loaders/orders";
24
+ import * as loaders_pageType from "./loaders/pageType";
25
+ import * as loaders_payment from "./loaders/payment";
26
+ import * as loaders_profile from "./loaders/profile";
27
+ import * as loaders_promotion from "./loaders/promotion";
28
+ import * as loaders_search from "./loaders/search";
29
+ import * as loaders_session from "./loaders/session";
30
+ import * as loaders_user from "./loaders/user";
31
+ import * as loaders_wishlist from "./loaders/wishlist";
32
+ import * as loaders_wishlistProducts from "./loaders/wishlistProducts";
33
+ import * as loaders_workflow from "./loaders/workflow";
34
+
35
+ const manifest = {
36
+ name: "vtex",
37
+ loaders: {
38
+ "vtex/loaders/address": loaders_address,
39
+ "vtex/loaders/brands": loaders_brands,
40
+ "vtex/loaders/cart": loaders_cart,
41
+ "vtex/loaders/catalog": loaders_catalog,
42
+ "vtex/loaders/collections": loaders_collections,
43
+ "vtex/loaders/legacy": loaders_legacy,
44
+ "vtex/loaders/logistics": loaders_logistics,
45
+ "vtex/loaders/navbar": loaders_navbar,
46
+ "vtex/loaders/orders": loaders_orders,
47
+ "vtex/loaders/pageType": loaders_pageType,
48
+ "vtex/loaders/payment": loaders_payment,
49
+ "vtex/loaders/profile": loaders_profile,
50
+ "vtex/loaders/promotion": loaders_promotion,
51
+ "vtex/loaders/search": loaders_search,
52
+ "vtex/loaders/session": loaders_session,
53
+ "vtex/loaders/user": loaders_user,
54
+ "vtex/loaders/wishlist": loaders_wishlist,
55
+ "vtex/loaders/wishlistProducts": loaders_wishlistProducts,
56
+ "vtex/loaders/workflow": loaders_workflow,
57
+ },
58
+ actions: {
59
+ "vtex/actions/address": actions_address,
60
+ "vtex/actions/auth": actions_auth,
61
+ "vtex/actions/checkout": actions_checkout,
62
+ "vtex/actions/masterData": actions_masterData,
63
+ "vtex/actions/misc": actions_misc,
64
+ "vtex/actions/newsletter": actions_newsletter,
65
+ "vtex/actions/orders": actions_orders,
66
+ "vtex/actions/profile": actions_profile,
67
+ "vtex/actions/session": actions_session,
68
+ "vtex/actions/trigger": actions_trigger,
69
+ "vtex/actions/wishlist": actions_wishlist,
70
+ },
71
+ sections: {},
72
+ } as const;
73
+
74
+ export type Manifest = typeof manifest;
75
+ export default manifest;
package/vtex/mod.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * VTEX app module — standard autoconfig contract.
3
+ *
4
+ * Exports `configure` following the AppModContract pattern.
5
+ * The framework's `autoconfigApps()` calls these generically.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import * as vtexApp from "@decocms/apps/vtex/mod";
10
+ *
11
+ * const app = await vtexApp.configure(blocks.vtex, resolveSecret);
12
+ * if (app) {
13
+ * // app.manifest, app.state, app.middleware are available
14
+ * }
15
+ * ```
16
+ */
17
+
18
+ import type { AppDefinition, AppMiddleware, ResolveSecretFn } from "../commerce/app-types";
19
+ import { configureVtex, type VtexConfig } from "./client";
20
+ import manifest from "./manifest.gen";
21
+ import { extractVtexContext, propagateISCookies, vtexCacheControl } from "./middleware";
22
+
23
+ // -------------------------------------------------------------------------
24
+ // State
25
+ // -------------------------------------------------------------------------
26
+
27
+ export interface VtexState {
28
+ config: VtexConfig;
29
+ }
30
+
31
+ // -------------------------------------------------------------------------
32
+ // Middleware
33
+ // -------------------------------------------------------------------------
34
+
35
+ const vtexMiddleware: AppMiddleware = async (request, next) => {
36
+ const ctx = extractVtexContext(request);
37
+ const response = await next();
38
+ response.headers.set("Cache-Control", vtexCacheControl(ctx));
39
+ propagateISCookies(ctx, response);
40
+ return response;
41
+ };
42
+
43
+ // -------------------------------------------------------------------------
44
+ // Configure
45
+ // -------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Configure the VTEX app from CMS block data.
49
+ * Returns an AppDefinition or null if required fields are missing.
50
+ */
51
+ export async function configure(
52
+ block: any,
53
+ resolveSecret: ResolveSecretFn,
54
+ ): Promise<AppDefinition<VtexState> | null> {
55
+ if (!block?.account) return null;
56
+
57
+ const appKey = await resolveSecret(block.appKey, "VTEX_APP_KEY");
58
+ const appToken = await resolveSecret(block.appToken, "VTEX_APP_TOKEN");
59
+
60
+ const config: VtexConfig = {
61
+ account: block.account,
62
+ publicUrl: block.publicUrl,
63
+ salesChannel: block.salesChannel || "1",
64
+ locale: block.locale || block.defaultLocale,
65
+ appKey: appKey ?? undefined,
66
+ appToken: appToken ?? undefined,
67
+ country: block.country,
68
+ domain: block.domain,
69
+ };
70
+
71
+ // Bridge: maintain global singleton for backward compat
72
+ configureVtex(config);
73
+
74
+ return {
75
+ name: "vtex",
76
+ manifest,
77
+ state: { config },
78
+ middleware: vtexMiddleware,
79
+ };
80
+ }
81
+
82
+ /** Placeholder preview for CMS editor — evolves when admin supports it. */
83
+ export const preview = undefined;