@vtex/faststore 0.0.1 → 0.0.3

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 (94) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +4 -2
  3. package/dist/catalog.json +286 -265
  4. package/dist/components.d.ts +2 -1
  5. package/dist/components.js +2 -1
  6. package/dist/content-types.json +16 -0
  7. package/dist/core/dist/actions/registry.d.ts +1 -1
  8. package/dist/core/dist/actions/registry.js +1 -1
  9. package/dist/core/dist/actions/registry.js.map +1 -1
  10. package/dist/docs/account-entry-point.md +1 -1
  11. package/dist/docs/cart.md +2 -2
  12. package/dist/docs/session.md +12 -6
  13. package/dist/docs/sign-in-redirect.md +137 -0
  14. package/dist/matter/access-code-sign-in/access-code-sign-in.d.ts +2 -2
  15. package/dist/matter/access-code-sign-in/access-code-sign-in.js +5 -2
  16. package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
  17. package/dist/matter/account-entry-point/account-entry-point.js +4 -16
  18. package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
  19. package/dist/matter/buy-button/buy-button.d.ts +1 -1
  20. package/dist/matter/buy-button/buy-button.js +3 -1
  21. package/dist/matter/buy-button/buy-button.js.map +1 -1
  22. package/dist/matter/cart/cart.d.ts +6 -6
  23. package/dist/matter/cart/cart.js +13 -6
  24. package/dist/matter/cart/cart.js.map +1 -1
  25. package/dist/matter/cart-item/cart-item.d.ts +2 -2
  26. package/dist/matter/cart-item/cart-item.js +5 -2
  27. package/dist/matter/cart-item/cart-item.js.map +1 -1
  28. package/dist/matter/client-preferences/client-preferences.js +3 -1
  29. package/dist/matter/client-preferences/client-preferences.js.map +1 -1
  30. package/dist/matter/client-profile/client-profile.d.ts +1 -1
  31. package/dist/matter/client-profile/client-profile.js +3 -1
  32. package/dist/matter/client-profile/client-profile.js.map +1 -1
  33. package/dist/matter/client-profile-form/client-profile-form.d.ts +1 -1
  34. package/dist/matter/client-profile-form/client-profile-form.js +3 -1
  35. package/dist/matter/client-profile-form/client-profile-form.js.map +1 -1
  36. package/dist/matter/delivery-options/delivery-options-accordion.d.ts +1 -1
  37. package/dist/matter/delivery-options/delivery-options-accordion.js +3 -1
  38. package/dist/matter/delivery-options/delivery-options-accordion.js.map +1 -1
  39. package/dist/matter/delivery-options/delivery-options-controller.d.ts +1 -1
  40. package/dist/matter/delivery-options/delivery-options-controller.js +3 -1
  41. package/dist/matter/delivery-options/delivery-options-controller.js.map +1 -1
  42. package/dist/matter/delivery-options/delivery-options.d.ts +1 -1
  43. package/dist/matter/delivery-options/delivery-options.js +3 -1
  44. package/dist/matter/delivery-options/delivery-options.js.map +1 -1
  45. package/dist/matter/email-identification/email-identification.d.ts +1 -1
  46. package/dist/matter/email-identification/email-identification.js +3 -1
  47. package/dist/matter/email-identification/email-identification.js.map +1 -1
  48. package/dist/matter/identity-summary/identity-summary.d.ts +1 -1
  49. package/dist/matter/identity-summary/identity-summary.js +3 -1
  50. package/dist/matter/identity-summary/identity-summary.js.map +1 -1
  51. package/dist/matter/product-list/product-list.d.ts +1 -1
  52. package/dist/matter/product-list/product-list.js +3 -1
  53. package/dist/matter/product-list/product-list.js.map +1 -1
  54. package/dist/matter/region/region.d.ts +1 -1
  55. package/dist/matter/region/region.js +5 -2
  56. package/dist/matter/region/region.js.map +1 -1
  57. package/dist/matter/session/session.js +1 -1
  58. package/dist/matter/session/session.js.map +1 -1
  59. package/dist/matter/shipping-address/shipping-address.d.ts +1 -1
  60. package/dist/matter/shipping-address/shipping-address.js +3 -1
  61. package/dist/matter/shipping-address/shipping-address.js.map +1 -1
  62. package/dist/matter/sign-in-form/sign-in-form.d.ts +1 -1
  63. package/dist/matter/sign-in-form/sign-in-form.js +3 -1
  64. package/dist/matter/sign-in-form/sign-in-form.js.map +1 -1
  65. package/dist/matter/sign-in-redirect/sign-in-redirect.d.ts +45 -0
  66. package/dist/matter/sign-in-redirect/sign-in-redirect.js +127 -0
  67. package/dist/matter/sign-in-redirect/sign-in-redirect.js.map +1 -0
  68. package/dist/sdk/actions/actions-override-context.js +37 -0
  69. package/dist/sdk/actions/actions-override-context.js.map +1 -0
  70. package/dist/sdk/actions/cart-action-provider.d.ts +8 -6
  71. package/dist/sdk/actions/cart-action-provider.js +10 -8
  72. package/dist/sdk/actions/cart-action-provider.js.map +1 -1
  73. package/dist/sdk/actions/identity-action-provider.d.ts +2 -2
  74. package/dist/sdk/actions/identity-action-provider.js +4 -4
  75. package/dist/sdk/actions/identity-action-provider.js.map +1 -1
  76. package/dist/sdk/actions/inject-actions.d.ts +1 -5
  77. package/dist/sdk/actions/inject-actions.js +23 -21
  78. package/dist/sdk/actions/inject-actions.js.map +1 -1
  79. package/dist/sdk/actions/region-action-provider.d.ts +4 -3
  80. package/dist/sdk/actions/region-action-provider.js +6 -5
  81. package/dist/sdk/actions/region-action-provider.js.map +1 -1
  82. package/dist/sdk/actions/session-action-provider.d.ts +4 -3
  83. package/dist/sdk/actions/session-action-provider.js +6 -5
  84. package/dist/sdk/actions/session-action-provider.js.map +1 -1
  85. package/dist/sdk/actions/shipping-action-provider.d.ts +4 -3
  86. package/dist/sdk/actions/shipping-action-provider.js +6 -5
  87. package/dist/sdk/actions/shipping-action-provider.js.map +1 -1
  88. package/dist/sdk/cart/use-cart-actions.js.map +1 -1
  89. package/dist/sdk/client/create-client.js +24 -2
  90. package/dist/sdk/client/create-client.js.map +1 -1
  91. package/dist/sdk/content/create-content-runtime.js +1 -1
  92. package/dist/utils/safe-href.js +107 -1
  93. package/dist/utils/safe-href.js.map +1 -1
  94. package/package.json +1 -1
@@ -1,17 +1,18 @@
1
1
  "use client";
2
- import { injectActions } from "./inject-actions.js";
2
+ import { useInjectActions } from "./inject-actions.js";
3
3
  import { useSessionActions } from "../session/use-session-actions.js";
4
4
  //#region src/sdk/actions/session-action-provider.tsx
5
5
  /**
6
6
  * Compiler-emitted runtime wrapper for the `session` action namespace — a line-for-line mirror of
7
7
  * `CartActionProvider`. The compiler wraps every element whose definition declares
8
8
  * `actions: { session: [...] }` with `<SessionActionProvider methods={[...]}>`, calls
9
- * `useSessionActions` once, narrows the surface to the declared `methods`, and threads it into the
10
- * single child via `$actions.session` (merging with any outer namespace provider). A namespace a
11
- * component does not declare is never wrapped, so its hook is never called (specs/actions.md FR-6).
9
+ * `useSessionActions` once, narrows the surface to the declared `methods`, and publishes it through
10
+ * `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to
11
+ * read further down. A namespace a component does not declare is never wrapped, so its hook is
12
+ * never called (specs/actions.md FR-6).
12
13
  */
13
14
  function SessionActionProvider(props) {
14
- return injectActions("session", useSessionActions(), props);
15
+ return useInjectActions("session", useSessionActions(), props);
15
16
  }
16
17
  //#endregion
17
18
  export { SessionActionProvider };
@@ -1 +1 @@
1
- {"version":3,"file":"session-action-provider.js","names":[],"sources":["../../../src/sdk/actions/session-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useSessionActions } from '../session/use-session-actions.js';\nimport { type ActionProviderProps, injectActions } from './inject-actions.js';\n\nexport type SessionActionProviderProps = ActionProviderProps;\n\n/**\n * Compiler-emitted runtime wrapper for the `session` action namespace — a line-for-line mirror of\n * `CartActionProvider`. The compiler wraps every element whose definition declares\n * `actions: { session: [...] }` with `<SessionActionProvider methods={[...]}>`, calls\n * `useSessionActions` once, narrows the surface to the declared `methods`, and threads it into the\n * single child via `$actions.session` (merging with any outer namespace provider). A namespace a\n * component does not declare is never wrapped, so its hook is never called (specs/actions.md FR-6).\n */\nexport function SessionActionProvider(props: SessionActionProviderProps): ReactNode {\n return injectActions('session', useSessionActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,SAAgB,sBAAsB,OAA8C;CAClF,OAAO,cAAc,WAAW,kBAAkB,GAAG,KAAK;AAC5D"}
1
+ {"version":3,"file":"session-action-provider.js","names":[],"sources":["../../../src/sdk/actions/session-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useSessionActions } from '../session/use-session-actions.js';\nimport { type ActionProviderProps, useInjectActions } from './inject-actions.js';\n\nexport type SessionActionProviderProps = ActionProviderProps;\n\n/**\n * Compiler-emitted runtime wrapper for the `session` action namespace — a line-for-line mirror of\n * `CartActionProvider`. The compiler wraps every element whose definition declares\n * `actions: { session: [...] }` with `<SessionActionProvider methods={[...]}>`, calls\n * `useSessionActions` once, narrows the surface to the declared `methods`, and publishes it through\n * `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to\n * read further down. A namespace a component does not declare is never wrapped, so its hook is\n * never called (specs/actions.md FR-6).\n */\nexport function SessionActionProvider(props: SessionActionProviderProps): ReactNode {\n return useInjectActions('session', useSessionActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,SAAgB,sBAAsB,OAA8C;CAClF,OAAO,iBAAiB,WAAW,kBAAkB,GAAG,KAAK;AAC/D"}
@@ -6,9 +6,10 @@ export type ShippingActionProviderProps = ActionProviderProps;
6
6
  * Compiler-emitted runtime wrapper for the `shipping` action namespace — a line-for-line mirror of
7
7
  * `SessionActionProvider`. The compiler wraps every element whose definition declares
8
8
  * `actions: { shipping: [...] }` with `<ShippingActionProvider methods={[...]}>`, calls
9
- * `useShippingActions` once, narrows the surface to the declared `methods`, and threads it into the
10
- * single child via `$actions.shipping` (merging with any outer namespace provider). A namespace a
11
- * component does not declare is never wrapped, so its hook is never called (specs/actions.md FR-6).
9
+ * `useShippingActions` once, narrows the surface to the declared `methods`, and publishes it through
10
+ * `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to
11
+ * read further down. A namespace a component does not declare is never wrapped, so its hook is
12
+ * never called (specs/actions.md FR-6).
12
13
  */
13
14
  export declare function ShippingActionProvider(props: ShippingActionProviderProps): ReactNode;
14
15
  //#endregion
@@ -1,17 +1,18 @@
1
1
  "use client";
2
- import { injectActions } from "./inject-actions.js";
2
+ import { useInjectActions } from "./inject-actions.js";
3
3
  import { useShippingActions } from "../shipping/use-shipping-actions.js";
4
4
  //#region src/sdk/actions/shipping-action-provider.tsx
5
5
  /**
6
6
  * Compiler-emitted runtime wrapper for the `shipping` action namespace — a line-for-line mirror of
7
7
  * `SessionActionProvider`. The compiler wraps every element whose definition declares
8
8
  * `actions: { shipping: [...] }` with `<ShippingActionProvider methods={[...]}>`, calls
9
- * `useShippingActions` once, narrows the surface to the declared `methods`, and threads it into the
10
- * single child via `$actions.shipping` (merging with any outer namespace provider). A namespace a
11
- * component does not declare is never wrapped, so its hook is never called (specs/actions.md FR-6).
9
+ * `useShippingActions` once, narrows the surface to the declared `methods`, and publishes it through
10
+ * `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to
11
+ * read further down. A namespace a component does not declare is never wrapped, so its hook is
12
+ * never called (specs/actions.md FR-6).
12
13
  */
13
14
  function ShippingActionProvider(props) {
14
- return injectActions("shipping", useShippingActions(), props);
15
+ return useInjectActions("shipping", useShippingActions(), props);
15
16
  }
16
17
  //#endregion
17
18
  export { ShippingActionProvider };
@@ -1 +1 @@
1
- {"version":3,"file":"shipping-action-provider.js","names":[],"sources":["../../../src/sdk/actions/shipping-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useShippingActions } from '../shipping/use-shipping-actions.js';\nimport { type ActionProviderProps, injectActions } from './inject-actions.js';\n\nexport type ShippingActionProviderProps = ActionProviderProps;\n\n/**\n * Compiler-emitted runtime wrapper for the `shipping` action namespace — a line-for-line mirror of\n * `SessionActionProvider`. The compiler wraps every element whose definition declares\n * `actions: { shipping: [...] }` with `<ShippingActionProvider methods={[...]}>`, calls\n * `useShippingActions` once, narrows the surface to the declared `methods`, and threads it into the\n * single child via `$actions.shipping` (merging with any outer namespace provider). A namespace a\n * component does not declare is never wrapped, so its hook is never called (specs/actions.md FR-6).\n */\nexport function ShippingActionProvider(props: ShippingActionProviderProps): ReactNode {\n return injectActions('shipping', useShippingActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,SAAgB,uBAAuB,OAA+C;CACpF,OAAO,cAAc,YAAY,mBAAmB,GAAG,KAAK;AAC9D"}
1
+ {"version":3,"file":"shipping-action-provider.js","names":[],"sources":["../../../src/sdk/actions/shipping-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useShippingActions } from '../shipping/use-shipping-actions.js';\nimport { type ActionProviderProps, useInjectActions } from './inject-actions.js';\n\nexport type ShippingActionProviderProps = ActionProviderProps;\n\n/**\n * Compiler-emitted runtime wrapper for the `shipping` action namespace — a line-for-line mirror of\n * `SessionActionProvider`. The compiler wraps every element whose definition declares\n * `actions: { shipping: [...] }` with `<ShippingActionProvider methods={[...]}>`, calls\n * `useShippingActions` once, narrows the surface to the declared `methods`, and publishes it through\n * `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to\n * read further down. A namespace a component does not declare is never wrapped, so its hook is\n * never called (specs/actions.md FR-6).\n */\nexport function ShippingActionProvider(props: ShippingActionProviderProps): ReactNode {\n return useInjectActions('shipping', useShippingActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,SAAgB,uBAAuB,OAA+C;CACpF,OAAO,iBAAiB,YAAY,mBAAmB,GAAG,KAAK;AACjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-cart-actions.js","names":[],"sources":["../../../src/sdk/cart/use-cart-actions.ts"],"sourcesContent":["'use client';\n\nimport type { ApolloClient } from '@apollo/client';\nimport { useApolloClient, useMutation } from '@apollo/client/react';\nimport { defineActions } from '@vtex/fs-core';\nimport { useCallback, useMemo } from 'react';\nimport { cartCouponErrorsVar, cartLastAddVar, cartUserErrorsVar, orderFormIdVar } from '../state/cart.js';\nimport {\n CART_COUPON_APPLY,\n CART_COUPON_REMOVE,\n CART_ITEM_ADD,\n CART_ITEM_QUANTITY_DECREASE,\n CART_ITEM_QUANTITY_INCREASE,\n CART_ITEM_REMOVE,\n CART_ITEMS_REMOVE,\n CART_VALIDATE,\n type CartEntity,\n CartFragment,\n type CartItemEntity,\n type CartUserError,\n} from './operations.js';\nimport {\n buildOptimisticDecrease,\n buildOptimisticIncrease,\n buildOptimisticRemove,\n buildOptimisticRemoveItems,\n} from './optimistic.js';\nimport { markAvailabilityFresh, trackCartWrite } from './write-gate.js';\n\n/**\n * The `cart` action namespace — the consolidated, context-free owner of every cart write\n * (specs/actions.md, docs/storefront-core/adr-0010-actions-own-the-write-side.md).\n *\n * It replaces `useCartMutations` + the mutation handlers `CartRoot` used to publish through\n * `CartDataContext`. A handler reads the current cart from the normalized Apollo cache (keyed by\n * `orderFormIdVar`), not from component props or React context, so any component anywhere in the\n * tree can dispatch a write through the compiler-injected `$actions.cart.*` prop — the cache is the\n * only meeting point between the read side and the write side (ADR-0010).\n *\n * `userErrors` from every call are published to `cartUserErrorsVar` (cleared at the start of the\n * next call) so `CartError` can surface them without a data context, and also returned to the caller.\n */\n\n/**\n * Thrown by the existing-line handlers (remove / quantity) when invoked before the cart resolves.\n * The first `addItem` mints the cart and hydrates `orderFormIdVar`; until then there is no cached\n * cart entity to read for the optimistic response.\n */\nexport class CartNotReadyError extends Error {\n constructor(message?: string) {\n super(message ?? 'CartNotReadyError: no cart in cache yet; add an item (or load the cart) before mutating a line.');\n this.name = 'CartNotReadyError';\n }\n}\n\nexport type AddItemPayload = Readonly<{\n variantId: string;\n quantity?: number;\n /** Seller global Node ID, e.g. base64(\"Seller:1\"). The server rejects raw ids with INVALID_SELLER_ID. */\n seller: string;\n}>;\n\n/** Existing-line payloads address the line by its opaque cart-item `id`; the handler reads the rest from cache. */\nexport type CartLinePayload = Readonly<{ id: string }>;\n\n/** Batch payloads address several lines by their opaque cart-item `id`s. */\nexport type CartLinesPayload = Readonly<{ ids: readonly string[] }>;\n\nexport type CartQuantityPayload = Readonly<{ id: string; by?: number }>;\n\n/** Coupon writes address the coupon by its `code`; the server resolves the cart from the cookie. */\nexport type CartCouponPayload = Readonly<{ code: string }>;\n\n/**\n * The `cart` namespace surface. Item writes return the resulting `userErrors` (empty on success)\n * and publish them to `cartUserErrorsVar`; coupon writes publish to the dedicated\n * `cartCouponErrorsVar` (specs/fspg-830-coupon-entry.md AD-2).\n */\nexport type CartActions = Readonly<{\n /** Add an item to the cart. Async (no optimistic response). Creates the cart on first add. */\n addItem: (payload: AddItemPayload) => Promise<readonly CartUserError[]>;\n /** Remove a line entirely. Optimistic. */\n removeItem: (payload: CartLinePayload) => Promise<readonly CartUserError[]>;\n /** Remove several lines in one mutation. Optimistic. Ids missing from the cache are skipped. */\n removeItems: (payload: CartLinesPayload) => Promise<readonly CartUserError[]>;\n /** Increase a line's quantity by N (default 1). Optimistic. */\n increaseQuantity: (payload: CartQuantityPayload) => Promise<readonly CartUserError[]>;\n /** Decrease a line's quantity by N (default 1). Optimistic. Removes the line when result <= 0. */\n decreaseQuantity: (payload: CartQuantityPayload) => Promise<readonly CartUserError[]>;\n /** Apply a coupon code. Async (no optimistic). Publishes userErrors to cartCouponErrorsVar. */\n applyCoupon: (payload: CartCouponPayload) => Promise<readonly CartUserError[]>;\n /** Remove a coupon code by code. Async (no optimistic). Publishes userErrors to cartCouponErrorsVar. */\n removeCoupon: (payload: CartCouponPayload) => Promise<readonly CartUserError[]>;\n /**\n * Re-check the active cart against the platform and adopt corrected availability. Async (no\n * optimistic), read-only upstream. Returns userErrors (empty on success) to its caller and stamps\n * the freshness window on success. Unlike the item writes it does NOT publish to\n * `cartUserErrorsVar` — a speculative open-time failure must raise no error banner (Decision 6).\n */\n validate: () => Promise<readonly CartUserError[]>;\n}>;\n\nconst NETWORK_USER_ERROR: CartUserError = {\n code: 'NETWORK_ERROR',\n message: 'Unable to update cart. Please try again.',\n};\n\nfunction publish(errors: readonly CartUserError[]): readonly CartUserError[] {\n cartUserErrorsVar(errors);\n return errors;\n}\n\n/** Coupon-scoped publish — keeps coupon feedback off the shared item-error channel (AD-2). */\nfunction publishCoupon(errors: readonly CartUserError[]): readonly CartUserError[] {\n cartCouponErrorsVar(errors);\n return errors;\n}\n\n/**\n * Publish a write's outcome and, when it came back clean, stamp the availability freshness window —\n * a clean write's payload carries the platform's recomputed cart, which is what lets the\n * validate-on-open call skip (specs/mini-cart-validation-wiring.md, Decision 2). Used only on paths\n * where a mutation actually reached the server: a `userErrors` response carries `cart: null` and a\n * no-op (unknown line) issues no request, so neither refreshes availability.\n */\nfunction publishWrite(errors: readonly CartUserError[]): readonly CartUserError[] {\n if (errors.length === 0) markAvailabilityFresh();\n return publish(errors);\n}\n\n/** Coupon-channel counterpart of `publishWrite`. */\nfunction publishCouponWrite(errors: readonly CartUserError[]): readonly CartUserError[] {\n if (errors.length === 0) markAvailabilityFresh();\n return publishCoupon(errors);\n}\n\n/**\n * Backfill `orderFormIdVar` from a mutation's returned `cart.id` when local state is still cold.\n * A coupon op can be the first cart write (SSR cold / drawer closed on first load); without this the\n * later `requireLine` paths short-circuit on `orderFormIdVar() === null` and throw `CartNotReadyError`.\n * Mirrors the `addItem` first-add handoff.\n */\nfunction hydrateCartId(cartId: string | null): void {\n if (cartId && orderFormIdVar() === null) orderFormIdVar(cartId);\n}\n\n/**\n * Read the live cart entity from the normalized cache (ADR-0010), keyed by `orderFormIdVar`. Returns\n * null before the cart is minted/loaded. Module-level so it can be shared with the read-only callers\n * that live outside this hook — the pre-checkout gate re-reads the corrected cart this way after\n * `validate` resolves, because Apollo commits the mutation's cache write before the promise settles\n * (specs/mini-cart-validation-wiring.md, Decision 3). The codegen `CartFragment` document embeds its\n * nested fragments, so it is a self-contained read.\n */\nexport function readCartFromCache(client: ApolloClient): CartEntity | null {\n const cartId = orderFormIdVar();\n if (!cartId) return null;\n const cacheId = client.cache.identify({ __typename: 'Cart', id: cartId });\n if (!cacheId) return null;\n return (\n client.cache.readFragment<CartEntity>({ id: cacheId, fragment: CartFragment, fragmentName: 'CartFragment' }) ?? null\n );\n}\n\n/**\n * The `cart` namespace hook. Registered with the first-party action registry via `cartActions`\n * below; the compiler-emitted `CartActionProvider` calls it and threads the narrowed methods into\n * each consumer's `$actions.cart`.\n */\nexport function useCartActions(): CartActions {\n const client = useApolloClient();\n\n const [addMutation] = useMutation(CART_ITEM_ADD);\n const [removeMutation] = useMutation(CART_ITEM_REMOVE);\n const [removeItemsMutation] = useMutation(CART_ITEMS_REMOVE);\n const [increaseMutation] = useMutation(CART_ITEM_QUANTITY_INCREASE);\n const [decreaseMutation] = useMutation(CART_ITEM_QUANTITY_DECREASE);\n const [applyCouponMutation] = useMutation(CART_COUPON_APPLY);\n const [removeCouponMutation] = useMutation(CART_COUPON_REMOVE);\n const [validateMutation] = useMutation(CART_VALIDATE);\n\n const readCart = useCallback((): CartEntity | null => readCartFromCache(client), [client]);\n\n const requireLine = useCallback(\n (id: string): { cart: CartEntity; line: CartItemEntity } | null => {\n const cart = readCart();\n if (!cart) throw new CartNotReadyError();\n const line = cart.items.find((item) => item.id === id);\n if (!line) return null;\n return { cart, line };\n },\n [readCart],\n );\n\n const addItem = useCallback<CartActions['addItem']>(\n async (payload) => {\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n addMutation({\n variables: {\n input: { variantId: payload.variantId, quantity: payload.quantity ?? 1, seller: payload.seller },\n },\n }),\n );\n // Very-first add: the server minted a cart from the empty transport context. Hydrate the\n // var so the existing-line handlers unlock without requiring a cart query.\n if (orderFormIdVar() === null) {\n const newId = result.data?.cartItemAdd?.cart?.id ?? null;\n if (newId) orderFormIdVar(newId);\n }\n const errors = result.data?.cartItemAdd?.userErrors ?? [];\n // Bump the add-success pulse only on the clean branch; the post-add behavior components\n // (CartOpenOnAdd / CartRedirectOnAdd) key their effect on it. The userErrors and\n // network-catch branches leave it untouched (specs/fspg-822-minicart-open-on-add.md).\n if (errors.length === 0) cartLastAddVar(cartLastAddVar() + 1);\n return publishWrite(errors);\n } catch (error) {\n console.error('[cart.addItem] cartItemAdd failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [addMutation],\n );\n\n const removeItem = useCallback<CartActions['removeItem']>(\n async (payload) => {\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n removeMutation({\n variables: { input: { variantId: found.line.variant.variantId } },\n optimisticResponse: buildOptimisticRemove(found.cart, found.line),\n }),\n );\n return publishWrite(result.data?.cartItemRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeItem] cartItemRemove failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, removeMutation],\n );\n\n const removeItems = useCallback<CartActions['removeItems']>(\n async (payload) => {\n const cart = readCart();\n if (!cart) throw new CartNotReadyError();\n // Client-side tolerance mirroring removeItem's not-found no-op: ids without a cached line\n // are skipped, and the surviving set is deduped before the single mutation.\n const lines = cart.items.filter((item) => payload.ids.includes(item.id));\n if (lines.length === 0) return publish([]);\n const variantIds = [...new Set(lines.map((line) => line.variant.variantId))];\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n removeItemsMutation({\n variables: { input: { variantIds } },\n optimisticResponse: buildOptimisticRemoveItems(cart, lines),\n }),\n );\n return publishWrite(result.data?.cartItemsRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeItems] cartItemsRemove failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [readCart, removeItemsMutation],\n );\n\n const increaseQuantity = useCallback<CartActions['increaseQuantity']>(\n async (payload) => {\n const by = payload.by ?? 1;\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n increaseMutation({\n variables: { input: { variantId: found.line.variant.variantId, quantityToAdd: by } },\n optimisticResponse: buildOptimisticIncrease(found.cart, found.line, by),\n }),\n );\n return publishWrite(result.data?.cartItemQuantityIncrease?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.increaseQuantity] cartItemQuantityIncrease failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, increaseMutation],\n );\n\n const decreaseQuantity = useCallback<CartActions['decreaseQuantity']>(\n async (payload) => {\n const by = payload.by ?? 1;\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n decreaseMutation({\n variables: { input: { variantId: found.line.variant.variantId, quantityToRemove: by } },\n optimisticResponse: buildOptimisticDecrease(found.cart, found.line, by),\n }),\n );\n return publishWrite(result.data?.cartItemQuantityDecrease?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.decreaseQuantity] cartItemQuantityDecrease failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, decreaseMutation],\n );\n\n // Coupons are cart-level: no `requireLine` gate (AD-9). No optimistic response and no client\n // rollback — totals and `isUnmatchedCondition` are server-computed and the server rolls back its\n // own single-coupon rejections; the returned cart (spreading the extended CartFragment)\n // self-updates the cache and every reactive subscriber (specs/fspg-830-coupon-entry.md AD-3/AD-8).\n const applyCoupon = useCallback<CartActions['applyCoupon']>(\n async (payload) => {\n cartCouponErrorsVar([]);\n try {\n const result = await trackCartWrite(applyCouponMutation({ variables: { input: { code: payload.code } } }));\n hydrateCartId(result.data?.cartCouponApply?.cart?.id ?? null);\n return publishCouponWrite(result.data?.cartCouponApply?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.applyCoupon] cartCouponApply failed', error);\n return publishCoupon([NETWORK_USER_ERROR]);\n }\n },\n [applyCouponMutation],\n );\n\n const removeCoupon = useCallback<CartActions['removeCoupon']>(\n async (payload) => {\n cartCouponErrorsVar([]);\n try {\n const result = await trackCartWrite(removeCouponMutation({ variables: { input: { code: payload.code } } }));\n hydrateCartId(result.data?.cartCouponRemove?.cart?.id ?? null);\n return publishCouponWrite(result.data?.cartCouponRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeCoupon] cartCouponRemove failed', error);\n return publishCoupon([NETWORK_USER_ERROR]);\n }\n },\n [removeCouponMutation],\n );\n\n // Read-only upstream (specs/mini-cart-validate-operation.md): re-simulates the cart and returns it\n // with `variant.availability` corrected, spreading CartFragment so the guard, the per-line alert,\n // and the badge re-render off the cache. Deliberately unlike the item writes: it does NOT register\n // as an in-flight write (it would gate itself) and it does NOT publish to cartUserErrorsVar — the\n // open call is speculative and the pre-checkout call is fail-open, so a failure must raise no error\n // banner (Decision 6). It stamps the freshness window on success so open-on-add does not re-check.\n const validate = useCallback<CartActions['validate']>(async () => {\n try {\n const result = await validateMutation();\n // Hydrate as the coupon writes do: the corrected cart is now in the cache, and the pre-checkout\n // gate finds it by `orderFormIdVar`. Without this a cold var would leave the gate reading null\n // and passing a cart it never actually checked.\n hydrateCartId(result.data?.cartValidate?.cart?.id ?? null);\n const errors = result.data?.cartValidate?.userErrors ?? [];\n if (errors.length === 0) markAvailabilityFresh();\n return errors;\n } catch (error) {\n console.error('[cart.validate] cartValidate failed', error);\n return [NETWORK_USER_ERROR];\n }\n }, [validateMutation]);\n\n return useMemo(\n () => ({\n addItem,\n removeItem,\n removeItems,\n increaseQuantity,\n decreaseQuantity,\n applyCoupon,\n removeCoupon,\n validate,\n }),\n [addItem, removeItem, removeItems, increaseQuantity, decreaseQuantity, applyCoupon, removeCoupon, validate],\n );\n}\n\n/**\n * Declares the `cart` action namespace — the single source of truth for its method/payload metadata,\n * runtime provider, and `use` hook (mirrors `defineComponent`). The native catalog builder\n * (`@vtex/sukima-node`'s `buildPackageCatalog`) statically reads this call to emit `actions.json`;\n * at runtime `CartActionProvider` calls `useCartActions` directly.\n */\nexport const cartActions = defineActions({\n namespace: 'cart',\n description: 'Mutation-backed writes against the current cart (consolidates useCartMutations / CartRoot).',\n provider: { name: 'CartActionProvider', importPath: '@vtex/faststore/internal/client' },\n methods: {\n addItem: {\n description: 'Add an item to the cart (async strategy). Creates the cart on first add.',\n payload: [\n { name: 'variantId', type: 'string' },\n { name: 'seller', type: 'string' },\n { name: 'quantity', type: 'number', optional: true },\n ],\n },\n removeItem: {\n description: 'Remove a line from the cart (optimistic strategy).',\n payload: [{ name: 'id', type: 'string' }],\n },\n removeItems: {\n description: 'Remove several lines from the cart in one call (optimistic strategy).',\n payload: [{ name: 'ids', type: 'string[]' }],\n },\n increaseQuantity: {\n description: \"Increase a line's quantity by N (default 1, optimistic strategy).\",\n payload: [\n { name: 'id', type: 'string' },\n { name: 'by', type: 'number', optional: true },\n ],\n },\n decreaseQuantity: {\n description: \"Decrease a line's quantity by N (default 1, optimistic). Removes the line at <= 0.\",\n payload: [\n { name: 'id', type: 'string' },\n { name: 'by', type: 'number', optional: true },\n ],\n },\n applyCoupon: {\n description: 'Apply a coupon code to the cart (async strategy). Server recomputes totals.',\n payload: [{ name: 'code', type: 'string' }],\n },\n removeCoupon: {\n description: 'Remove a coupon code from the cart (async strategy). Server recomputes totals.',\n payload: [{ name: 'code', type: 'string' }],\n },\n validate: {\n description:\n 'Re-check the active cart against the platform and adopt corrected availability (async, read-only upstream).',\n payload: [],\n },\n },\n use: useCartActions,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,SAAkB;EAC5B,MAAM,WAAW,iGAAiG;EAClH,KAAK,OAAO;CACd;AACF;AAiDA,MAAM,qBAAoC;CACxC,MAAM;CACN,SAAS;AACX;AAEA,SAAS,QAAQ,QAA4D;CAC3E,kBAAkB,MAAM;CACxB,OAAO;AACT;;AAGA,SAAS,cAAc,QAA4D;CACjF,oBAAoB,MAAM;CAC1B,OAAO;AACT;;;;;;;;AASA,SAAS,aAAa,QAA4D;CAChF,IAAI,OAAO,WAAW,GAAG,sBAAsB;CAC/C,OAAO,QAAQ,MAAM;AACvB;;AAGA,SAAS,mBAAmB,QAA4D;CACtF,IAAI,OAAO,WAAW,GAAG,sBAAsB;CAC/C,OAAO,cAAc,MAAM;AAC7B;;;;;;;AAQA,SAAS,cAAc,QAA6B;CAClD,IAAI,UAAU,eAAe,MAAM,MAAM,eAAe,MAAM;AAChE;;;;;;;;;AAUA,SAAgB,kBAAkB,QAAyC;CACzE,MAAM,SAAS,eAAe;CAC9B,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,UAAU,OAAO,MAAM,SAAS;EAAE,YAAY;EAAQ,IAAI;CAAO,CAAC;CACxE,IAAI,CAAC,SAAS,OAAO;CACrB,OACE,OAAO,MAAM,aAAyB;EAAE,IAAI;EAAS,UAAU;EAAc,cAAc;CAAe,CAAC,KAAK;AAEpH;;;;;;AAOA,SAAgB,iBAA8B;CAC5C,MAAM,SAAS,gBAAgB;CAE/B,MAAM,CAAC,eAAe,YAAY,aAAa;CAC/C,MAAM,CAAC,kBAAkB,YAAY,gBAAgB;CACrD,MAAM,CAAC,uBAAuB,YAAY,iBAAiB;CAC3D,MAAM,CAAC,oBAAoB,YAAY,2BAA2B;CAClE,MAAM,CAAC,oBAAoB,YAAY,2BAA2B;CAClE,MAAM,CAAC,uBAAuB,YAAY,iBAAiB;CAC3D,MAAM,CAAC,wBAAwB,YAAY,kBAAkB;CAC7D,MAAM,CAAC,oBAAoB,YAAY,aAAa;CAEpD,MAAM,WAAW,kBAAqC,kBAAkB,MAAM,GAAG,CAAC,MAAM,CAAC;CAEzF,MAAM,cAAc,aACjB,OAAkE;EACjE,MAAM,OAAO,SAAS;EACtB,IAAI,CAAC,MAAM,MAAM,IAAI,kBAAkB;EACvC,MAAM,OAAO,KAAK,MAAM,MAAM,SAAS,KAAK,OAAO,EAAE;EACrD,IAAI,CAAC,MAAM,OAAO;EAClB,OAAO;GAAE;GAAM;EAAK;CACtB,GACA,CAAC,QAAQ,CACX;CAEA,MAAM,UAAU,YACd,OAAO,YAAY;EACjB,kBAAkB,CAAC,CAAC;EACpB,IAAI;GACF,MAAM,SAAS,MAAM,eACnB,YAAY,EACV,WAAW,EACT,OAAO;IAAE,WAAW,QAAQ;IAAW,UAAU,QAAQ,YAAY;IAAG,QAAQ,QAAQ;GAAO,EACjG,EACF,CAAC,CACH;GAGA,IAAI,eAAe,MAAM,MAAM;IAC7B,MAAM,QAAQ,OAAO,MAAM,aAAa,MAAM,MAAM;IACpD,IAAI,OAAO,eAAe,KAAK;GACjC;GACA,MAAM,SAAS,OAAO,MAAM,aAAa,cAAc,CAAC;GAIxD,IAAI,OAAO,WAAW,GAAG,eAAe,eAAe,IAAI,CAAC;GAC5D,OAAO,aAAa,MAAM;EAC5B,SAAS,OAAO;GACd,QAAQ,MAAM,qCAAqC,KAAK;GACxD,OAAO,QAAQ,CAAC,kBAAkB,CAAC;EACrC;CACF,GACA,CAAC,WAAW,CACd;CAEA,MAAM,aAAa,YACjB,OAAO,YAAY;EACjB,MAAM,QAAQ,YAAY,QAAQ,EAAE;EACpC,IAAI,CAAC,OAAO,OAAO,QAAQ,CAAC,CAAC;EAC7B,kBAAkB,CAAC,CAAC;EACpB,IAAI;GAOF,OAAO,cAAa,MANC,eACnB,eAAe;IACb,WAAW,EAAE,OAAO,EAAE,WAAW,MAAM,KAAK,QAAQ,UAAU,EAAE;IAChE,oBAAoB,sBAAsB,MAAM,MAAM,MAAM,IAAI;GAClE,CAAC,CACH,EAAA,CAC2B,MAAM,gBAAgB,cAAc,CAAC,CAAC;EACnE,SAAS,OAAO;GACd,QAAQ,MAAM,2CAA2C,KAAK;GAC9D,OAAO,QAAQ,CAAC,kBAAkB,CAAC;EACrC;CACF,GACA,CAAC,aAAa,cAAc,CAC9B;CAEA,MAAM,cAAc,YAClB,OAAO,YAAY;EACjB,MAAM,OAAO,SAAS;EACtB,IAAI,CAAC,MAAM,MAAM,IAAI,kBAAkB;EAGvC,MAAM,QAAQ,KAAK,MAAM,QAAQ,SAAS,QAAQ,IAAI,SAAS,KAAK,EAAE,CAAC;EACvE,IAAI,MAAM,WAAW,GAAG,OAAO,QAAQ,CAAC,CAAC;EACzC,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS,CAAC,CAAC;EAC3E,kBAAkB,CAAC,CAAC;EACpB,IAAI;GAOF,OAAO,cAAa,MANC,eACnB,oBAAoB;IAClB,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE;IACnC,oBAAoB,2BAA2B,MAAM,KAAK;GAC5D,CAAC,CACH,EAAA,CAC2B,MAAM,iBAAiB,cAAc,CAAC,CAAC;EACpE,SAAS,OAAO;GACd,QAAQ,MAAM,6CAA6C,KAAK;GAChE,OAAO,QAAQ,CAAC,kBAAkB,CAAC;EACrC;CACF,GACA,CAAC,UAAU,mBAAmB,CAChC;CAEA,MAAM,mBAAmB,YACvB,OAAO,YAAY;EACjB,MAAM,KAAK,QAAQ,MAAM;EACzB,MAAM,QAAQ,YAAY,QAAQ,EAAE;EACpC,IAAI,CAAC,OAAO,OAAO,QAAQ,CAAC,CAAC;EAC7B,kBAAkB,CAAC,CAAC;EACpB,IAAI;GAOF,OAAO,cAAa,MANC,eACnB,iBAAiB;IACf,WAAW,EAAE,OAAO;KAAE,WAAW,MAAM,KAAK,QAAQ;KAAW,eAAe;IAAG,EAAE;IACnF,oBAAoB,wBAAwB,MAAM,MAAM,MAAM,MAAM,EAAE;GACxE,CAAC,CACH,EAAA,CAC2B,MAAM,0BAA0B,cAAc,CAAC,CAAC;EAC7E,SAAS,OAAO;GACd,QAAQ,MAAM,2DAA2D,KAAK;GAC9E,OAAO,QAAQ,CAAC,kBAAkB,CAAC;EACrC;CACF,GACA,CAAC,aAAa,gBAAgB,CAChC;CAEA,MAAM,mBAAmB,YACvB,OAAO,YAAY;EACjB,MAAM,KAAK,QAAQ,MAAM;EACzB,MAAM,QAAQ,YAAY,QAAQ,EAAE;EACpC,IAAI,CAAC,OAAO,OAAO,QAAQ,CAAC,CAAC;EAC7B,kBAAkB,CAAC,CAAC;EACpB,IAAI;GAOF,OAAO,cAAa,MANC,eACnB,iBAAiB;IACf,WAAW,EAAE,OAAO;KAAE,WAAW,MAAM,KAAK,QAAQ;KAAW,kBAAkB;IAAG,EAAE;IACtF,oBAAoB,wBAAwB,MAAM,MAAM,MAAM,MAAM,EAAE;GACxE,CAAC,CACH,EAAA,CAC2B,MAAM,0BAA0B,cAAc,CAAC,CAAC;EAC7E,SAAS,OAAO;GACd,QAAQ,MAAM,2DAA2D,KAAK;GAC9E,OAAO,QAAQ,CAAC,kBAAkB,CAAC;EACrC;CACF,GACA,CAAC,aAAa,gBAAgB,CAChC;CAMA,MAAM,cAAc,YAClB,OAAO,YAAY;EACjB,oBAAoB,CAAC,CAAC;EACtB,IAAI;GACF,MAAM,SAAS,MAAM,eAAe,oBAAoB,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,QAAQ,KAAK,EAAE,EAAE,CAAC,CAAC;GACzG,cAAc,OAAO,MAAM,iBAAiB,MAAM,MAAM,IAAI;GAC5D,OAAO,mBAAmB,OAAO,MAAM,iBAAiB,cAAc,CAAC,CAAC;EAC1E,SAAS,OAAO;GACd,QAAQ,MAAM,6CAA6C,KAAK;GAChE,OAAO,cAAc,CAAC,kBAAkB,CAAC;EAC3C;CACF,GACA,CAAC,mBAAmB,CACtB;CAEA,MAAM,eAAe,YACnB,OAAO,YAAY;EACjB,oBAAoB,CAAC,CAAC;EACtB,IAAI;GACF,MAAM,SAAS,MAAM,eAAe,qBAAqB,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,QAAQ,KAAK,EAAE,EAAE,CAAC,CAAC;GAC1G,cAAc,OAAO,MAAM,kBAAkB,MAAM,MAAM,IAAI;GAC7D,OAAO,mBAAmB,OAAO,MAAM,kBAAkB,cAAc,CAAC,CAAC;EAC3E,SAAS,OAAO;GACd,QAAQ,MAAM,+CAA+C,KAAK;GAClE,OAAO,cAAc,CAAC,kBAAkB,CAAC;EAC3C;CACF,GACA,CAAC,oBAAoB,CACvB;CAQA,MAAM,WAAW,YAAqC,YAAY;EAChE,IAAI;GACF,MAAM,SAAS,MAAM,iBAAiB;GAItC,cAAc,OAAO,MAAM,cAAc,MAAM,MAAM,IAAI;GACzD,MAAM,SAAS,OAAO,MAAM,cAAc,cAAc,CAAC;GACzD,IAAI,OAAO,WAAW,GAAG,sBAAsB;GAC/C,OAAO;EACT,SAAS,OAAO;GACd,QAAQ,MAAM,uCAAuC,KAAK;GAC1D,OAAO,CAAC,kBAAkB;EAC5B;CACF,GAAG,CAAC,gBAAgB,CAAC;CAErB,OAAO,eACE;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,IACA;EAAC;EAAS;EAAY;EAAa;EAAkB;EAAkB;EAAa;EAAc;CAAQ,CAC5G;AACF;AAQ2B,cAAc;CACvC,WAAW;CACX,aAAa;CACb,UAAU;EAAE,MAAM;EAAsB,YAAY;CAAkC;CACtF,SAAS;EACP,SAAS;GACP,aAAa;GACb,SAAS;IACP;KAAE,MAAM;KAAa,MAAM;IAAS;IACpC;KAAE,MAAM;KAAU,MAAM;IAAS;IACjC;KAAE,MAAM;KAAY,MAAM;KAAU,UAAU;IAAK;GACrD;EACF;EACA,YAAY;GACV,aAAa;GACb,SAAS,CAAC;IAAE,MAAM;IAAM,MAAM;GAAS,CAAC;EAC1C;EACA,aAAa;GACX,aAAa;GACb,SAAS,CAAC;IAAE,MAAM;IAAO,MAAM;GAAW,CAAC;EAC7C;EACA,kBAAkB;GAChB,aAAa;GACb,SAAS,CACP;IAAE,MAAM;IAAM,MAAM;GAAS,GAC7B;IAAE,MAAM;IAAM,MAAM;IAAU,UAAU;GAAK,CAC/C;EACF;EACA,kBAAkB;GAChB,aAAa;GACb,SAAS,CACP;IAAE,MAAM;IAAM,MAAM;GAAS,GAC7B;IAAE,MAAM;IAAM,MAAM;IAAU,UAAU;GAAK,CAC/C;EACF;EACA,aAAa;GACX,aAAa;GACb,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM;GAAS,CAAC;EAC5C;EACA,cAAc;GACZ,aAAa;GACb,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM;GAAS,CAAC;EAC5C;EACA,UAAU;GACR,aACE;GACF,SAAS,CAAC;EACZ;CACF;CACA,KAAK;AACP,CAAC"}
1
+ {"version":3,"file":"use-cart-actions.js","names":[],"sources":["../../../src/sdk/cart/use-cart-actions.ts"],"sourcesContent":["'use client';\n\nimport type { ApolloClient } from '@apollo/client';\nimport { useApolloClient, useMutation } from '@apollo/client/react';\nimport { defineActions } from '@vtex/fs-core';\nimport { useCallback, useMemo } from 'react';\nimport { cartCouponErrorsVar, cartLastAddVar, cartUserErrorsVar, orderFormIdVar } from '../state/cart.js';\nimport {\n CART_COUPON_APPLY,\n CART_COUPON_REMOVE,\n CART_ITEM_ADD,\n CART_ITEM_QUANTITY_DECREASE,\n CART_ITEM_QUANTITY_INCREASE,\n CART_ITEM_REMOVE,\n CART_ITEMS_REMOVE,\n CART_VALIDATE,\n type CartEntity,\n CartFragment,\n type CartItemEntity,\n type CartUserError,\n} from './operations.js';\nimport {\n buildOptimisticDecrease,\n buildOptimisticIncrease,\n buildOptimisticRemove,\n buildOptimisticRemoveItems,\n} from './optimistic.js';\nimport { markAvailabilityFresh, trackCartWrite } from './write-gate.js';\n\n/**\n * The `cart` action namespace — the consolidated, context-free owner of every cart write\n * (specs/actions.md, docs/storefront-core/adr-0010-actions-own-the-write-side.md).\n *\n * It replaces `useCartMutations` + the mutation handlers `CartRoot` used to publish through\n * `CartDataContext`. A handler reads the current cart from the normalized Apollo cache (keyed by\n * `orderFormIdVar`), not from component props or React context, so any component anywhere in the\n * tree can dispatch a write through the compiler-injected `$actions.cart.*` prop — the cache is the\n * only meeting point between the read side and the write side (ADR-0010).\n *\n * `userErrors` from every call are published to `cartUserErrorsVar` (cleared at the start of the\n * next call) so `CartError` can surface them without a data context, and also returned to the caller.\n */\n\n/**\n * Thrown by the existing-line handlers (remove / quantity) when invoked before the cart resolves.\n * The first `addItem` mints the cart and hydrates `orderFormIdVar`; until then there is no cached\n * cart entity to read for the optimistic response.\n */\nexport class CartNotReadyError extends Error {\n constructor(message?: string) {\n super(message ?? 'CartNotReadyError: no cart in cache yet; add an item (or load the cart) before mutating a line.');\n this.name = 'CartNotReadyError';\n }\n}\n\nexport type AddItemPayload = Readonly<{\n variantId: string;\n quantity?: number;\n /** Seller global Node ID, e.g. base64(\"Seller:1\"). The server rejects raw ids with INVALID_SELLER_ID. */\n seller: string;\n}>;\n\n/** Existing-line payloads address the line by its opaque cart-item `id`; the handler reads the rest from cache. */\nexport type CartLinePayload = Readonly<{ id: string }>;\n\n/** Batch payloads address several lines by their opaque cart-item `id`s. */\nexport type CartLinesPayload = Readonly<{ ids: readonly string[] }>;\n\nexport type CartQuantityPayload = Readonly<{ id: string; by?: number }>;\n\n/** Coupon writes address the coupon by its `code`; the server resolves the cart from the cookie. */\nexport type CartCouponPayload = Readonly<{ code: string }>;\n\n/**\n * The `cart` namespace surface. Item writes return the resulting `userErrors` (empty on success)\n * and publish them to `cartUserErrorsVar`; coupon writes publish to the dedicated\n * `cartCouponErrorsVar` (specs/fspg-830-coupon-entry.md AD-2).\n */\nexport type CartActions = Readonly<{\n /** Add an item to the cart. Async (no optimistic response). Creates the cart on first add. */\n addItem: (payload: AddItemPayload) => Promise<readonly CartUserError[]>;\n /** Remove a line entirely. Optimistic. */\n removeItem: (payload: CartLinePayload) => Promise<readonly CartUserError[]>;\n /** Remove several lines in one mutation. Optimistic. Ids missing from the cache are skipped. */\n removeItems: (payload: CartLinesPayload) => Promise<readonly CartUserError[]>;\n /** Increase a line's quantity by N (default 1). Optimistic. */\n increaseQuantity: (payload: CartQuantityPayload) => Promise<readonly CartUserError[]>;\n /** Decrease a line's quantity by N (default 1). Optimistic. Removes the line when result <= 0. */\n decreaseQuantity: (payload: CartQuantityPayload) => Promise<readonly CartUserError[]>;\n /** Apply a coupon code. Async (no optimistic). Publishes userErrors to cartCouponErrorsVar. */\n applyCoupon: (payload: CartCouponPayload) => Promise<readonly CartUserError[]>;\n /** Remove a coupon code by code. Async (no optimistic). Publishes userErrors to cartCouponErrorsVar. */\n removeCoupon: (payload: CartCouponPayload) => Promise<readonly CartUserError[]>;\n /**\n * Re-check the active cart against the platform and adopt corrected availability. Async (no\n * optimistic), read-only upstream. Returns userErrors (empty on success) to its caller and stamps\n * the freshness window on success. Unlike the item writes it does NOT publish to\n * `cartUserErrorsVar` — a speculative open-time failure must raise no error banner (Decision 6).\n */\n validate: () => Promise<readonly CartUserError[]>;\n}>;\n\nconst NETWORK_USER_ERROR: CartUserError = {\n code: 'NETWORK_ERROR',\n message: 'Unable to update cart. Please try again.',\n};\n\nfunction publish(errors: readonly CartUserError[]): readonly CartUserError[] {\n cartUserErrorsVar(errors);\n return errors;\n}\n\n/** Coupon-scoped publish — keeps coupon feedback off the shared item-error channel (AD-2). */\nfunction publishCoupon(errors: readonly CartUserError[]): readonly CartUserError[] {\n cartCouponErrorsVar(errors);\n return errors;\n}\n\n/**\n * Publish a write's outcome and, when it came back clean, stamp the availability freshness window —\n * a clean write's payload carries the platform's recomputed cart, which is what lets the\n * validate-on-open call skip (specs/mini-cart-validation-wiring.md, Decision 2). Used only on paths\n * where a mutation actually reached the server: a `userErrors` response carries `cart: null` and a\n * no-op (unknown line) issues no request, so neither refreshes availability.\n */\nfunction publishWrite(errors: readonly CartUserError[]): readonly CartUserError[] {\n if (errors.length === 0) markAvailabilityFresh();\n return publish(errors);\n}\n\n/** Coupon-channel counterpart of `publishWrite`. */\nfunction publishCouponWrite(errors: readonly CartUserError[]): readonly CartUserError[] {\n if (errors.length === 0) markAvailabilityFresh();\n return publishCoupon(errors);\n}\n\n/**\n * Backfill `orderFormIdVar` from a mutation's returned `cart.id` when local state is still cold.\n * A coupon op can be the first cart write (SSR cold / drawer closed on first load); without this the\n * later `requireLine` paths short-circuit on `orderFormIdVar() === null` and throw `CartNotReadyError`.\n * Mirrors the `addItem` first-add handoff.\n */\nfunction hydrateCartId(cartId: string | null): void {\n if (cartId && orderFormIdVar() === null) orderFormIdVar(cartId);\n}\n\n/**\n * Read the live cart entity from the normalized cache (ADR-0010), keyed by `orderFormIdVar`. Returns\n * null before the cart is minted/loaded. Module-level so it can be shared with the read-only callers\n * that live outside this hook — the pre-checkout gate re-reads the corrected cart this way after\n * `validate` resolves, because Apollo commits the mutation's cache write before the promise settles\n * (specs/mini-cart-validation-wiring.md, Decision 3). The codegen `CartFragment` document embeds its\n * nested fragments, so it is a self-contained read.\n */\nexport function readCartFromCache(client: ApolloClient): CartEntity | null {\n const cartId = orderFormIdVar();\n if (!cartId) return null;\n const cacheId = client.cache.identify({ __typename: 'Cart', id: cartId });\n if (!cacheId) return null;\n return (\n client.cache.readFragment<CartEntity>({ id: cacheId, fragment: CartFragment, fragmentName: 'CartFragment' }) ?? null\n );\n}\n\n/**\n * The `cart` namespace hook. Registered with the first-party action registry via `cartActions`\n * below; the compiler-emitted `CartActionProvider` calls it and threads the narrowed methods into\n * each consumer's `$actions.cart`.\n */\nexport function useCartActions(): CartActions {\n const client = useApolloClient();\n\n const [addMutation] = useMutation(CART_ITEM_ADD);\n const [removeMutation] = useMutation(CART_ITEM_REMOVE);\n const [removeItemsMutation] = useMutation(CART_ITEMS_REMOVE);\n const [increaseMutation] = useMutation(CART_ITEM_QUANTITY_INCREASE);\n const [decreaseMutation] = useMutation(CART_ITEM_QUANTITY_DECREASE);\n const [applyCouponMutation] = useMutation(CART_COUPON_APPLY);\n const [removeCouponMutation] = useMutation(CART_COUPON_REMOVE);\n const [validateMutation] = useMutation(CART_VALIDATE);\n\n const readCart = useCallback((): CartEntity | null => readCartFromCache(client), [client]);\n\n const requireLine = useCallback(\n (id: string): { cart: CartEntity; line: CartItemEntity } | null => {\n const cart = readCart();\n if (!cart) throw new CartNotReadyError();\n const line = cart.items.find((item) => item.id === id);\n if (!line) return null;\n return { cart, line };\n },\n [readCart],\n );\n\n const addItem = useCallback<CartActions['addItem']>(\n async (payload) => {\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n addMutation({\n variables: {\n input: { variantId: payload.variantId, quantity: payload.quantity ?? 1, seller: payload.seller },\n },\n }),\n );\n // Very-first add: the server minted a cart from the empty transport context. Hydrate the\n // var so the existing-line handlers unlock without requiring a cart query.\n if (orderFormIdVar() === null) {\n const newId = result.data?.cartItemAdd?.cart?.id ?? null;\n if (newId) orderFormIdVar(newId);\n }\n const errors = result.data?.cartItemAdd?.userErrors ?? [];\n // Bump the add-success pulse only on the clean branch; the post-add behavior components\n // (CartOpenOnAdd / CartRedirectOnAdd) key their effect on it. The userErrors and\n // network-catch branches leave it untouched (specs/fspg-822-minicart-open-on-add.md).\n if (errors.length === 0) cartLastAddVar(cartLastAddVar() + 1);\n return publishWrite(errors);\n } catch (error) {\n console.error('[cart.addItem] cartItemAdd failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [addMutation],\n );\n\n const removeItem = useCallback<CartActions['removeItem']>(\n async (payload) => {\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n removeMutation({\n variables: { input: { variantId: found.line.variant.variantId } },\n optimisticResponse: buildOptimisticRemove(found.cart, found.line),\n }),\n );\n return publishWrite(result.data?.cartItemRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeItem] cartItemRemove failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, removeMutation],\n );\n\n const removeItems = useCallback<CartActions['removeItems']>(\n async (payload) => {\n const cart = readCart();\n if (!cart) throw new CartNotReadyError();\n // Client-side tolerance mirroring removeItem's not-found no-op: ids without a cached line\n // are skipped, and the surviving set is deduped before the single mutation.\n const lines = cart.items.filter((item) => payload.ids.includes(item.id));\n if (lines.length === 0) return publish([]);\n const variantIds = [...new Set(lines.map((line) => line.variant.variantId))];\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n removeItemsMutation({\n variables: { input: { variantIds } },\n optimisticResponse: buildOptimisticRemoveItems(cart, lines),\n }),\n );\n return publishWrite(result.data?.cartItemsRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeItems] cartItemsRemove failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [readCart, removeItemsMutation],\n );\n\n const increaseQuantity = useCallback<CartActions['increaseQuantity']>(\n async (payload) => {\n const by = payload.by ?? 1;\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n increaseMutation({\n variables: { input: { variantId: found.line.variant.variantId, quantityToAdd: by } },\n optimisticResponse: buildOptimisticIncrease(found.cart, found.line, by),\n }),\n );\n return publishWrite(result.data?.cartItemQuantityIncrease?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.increaseQuantity] cartItemQuantityIncrease failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, increaseMutation],\n );\n\n const decreaseQuantity = useCallback<CartActions['decreaseQuantity']>(\n async (payload) => {\n const by = payload.by ?? 1;\n const found = requireLine(payload.id);\n if (!found) return publish([]);\n cartUserErrorsVar([]);\n try {\n const result = await trackCartWrite(\n decreaseMutation({\n variables: { input: { variantId: found.line.variant.variantId, quantityToRemove: by } },\n optimisticResponse: buildOptimisticDecrease(found.cart, found.line, by),\n }),\n );\n return publishWrite(result.data?.cartItemQuantityDecrease?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.decreaseQuantity] cartItemQuantityDecrease failed', error);\n return publish([NETWORK_USER_ERROR]);\n }\n },\n [requireLine, decreaseMutation],\n );\n\n // Coupons are cart-level: no `requireLine` gate (AD-9). No optimistic response and no client\n // rollback — totals and `isUnmatchedCondition` are server-computed and the server rolls back its\n // own single-coupon rejections; the returned cart (spreading the extended CartFragment)\n // self-updates the cache and every reactive subscriber (specs/fspg-830-coupon-entry.md AD-3/AD-8).\n const applyCoupon = useCallback<CartActions['applyCoupon']>(\n async (payload) => {\n cartCouponErrorsVar([]);\n try {\n const result = await trackCartWrite(applyCouponMutation({ variables: { input: { code: payload.code } } }));\n hydrateCartId(result.data?.cartCouponApply?.cart?.id ?? null);\n return publishCouponWrite(result.data?.cartCouponApply?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.applyCoupon] cartCouponApply failed', error);\n return publishCoupon([NETWORK_USER_ERROR]);\n }\n },\n [applyCouponMutation],\n );\n\n const removeCoupon = useCallback<CartActions['removeCoupon']>(\n async (payload) => {\n cartCouponErrorsVar([]);\n try {\n const result = await trackCartWrite(removeCouponMutation({ variables: { input: { code: payload.code } } }));\n hydrateCartId(result.data?.cartCouponRemove?.cart?.id ?? null);\n return publishCouponWrite(result.data?.cartCouponRemove?.userErrors ?? []);\n } catch (error) {\n console.error('[cart.removeCoupon] cartCouponRemove failed', error);\n return publishCoupon([NETWORK_USER_ERROR]);\n }\n },\n [removeCouponMutation],\n );\n\n // Read-only upstream (specs/mini-cart-validate-operation.md): re-simulates the cart and returns it\n // with `variant.availability` corrected, spreading CartFragment so the guard, the per-line alert,\n // and the badge re-render off the cache. Deliberately unlike the item writes: it does NOT register\n // as an in-flight write (it would gate itself) and it does NOT publish to cartUserErrorsVar — the\n // open call is speculative and the pre-checkout call is fail-open, so a failure must raise no error\n // banner (Decision 6). It stamps the freshness window on success so open-on-add does not re-check.\n const validate = useCallback<CartActions['validate']>(async () => {\n try {\n const result = await validateMutation();\n // Hydrate as the coupon writes do: the corrected cart is now in the cache, and the pre-checkout\n // gate finds it by `orderFormIdVar`. Without this a cold var would leave the gate reading null\n // and passing a cart it never actually checked.\n hydrateCartId(result.data?.cartValidate?.cart?.id ?? null);\n const errors = result.data?.cartValidate?.userErrors ?? [];\n if (errors.length === 0) markAvailabilityFresh();\n return errors;\n } catch (error) {\n console.error('[cart.validate] cartValidate failed', error);\n return [NETWORK_USER_ERROR];\n }\n }, [validateMutation]);\n\n return useMemo(\n () => ({\n addItem,\n removeItem,\n removeItems,\n increaseQuantity,\n decreaseQuantity,\n applyCoupon,\n removeCoupon,\n validate,\n }),\n [addItem, removeItem, removeItems, increaseQuantity, decreaseQuantity, applyCoupon, removeCoupon, validate],\n );\n}\n\n/**\n * Declares the `cart` action namespace — the single source of truth for its method/payload metadata,\n * runtime provider, and `use` hook (mirrors `defineComponent`). The native catalog builder\n * (`@vtex/faststore-catalog`'s `buildPackageCatalog`) statically reads this call to emit `actions.json`;\n * at runtime `CartActionProvider` calls `useCartActions` directly.\n */\nexport const cartActions = defineActions({\n namespace: 'cart',\n description: 'Mutation-backed writes against the current cart (consolidates useCartMutations / CartRoot).',\n provider: { name: 'CartActionProvider', importPath: '@vtex/faststore/internal/client' },\n methods: {\n addItem: {\n description: 'Add an item to the cart (async strategy). Creates the cart on first add.',\n payload: [\n { name: 'variantId', type: 'string' },\n { name: 'seller', type: 'string' },\n { name: 'quantity', type: 'number', optional: true },\n ],\n },\n removeItem: {\n description: 'Remove a line from the cart (optimistic strategy).',\n payload: [{ name: 'id', type: 'string' }],\n },\n removeItems: {\n description: 'Remove several lines from the cart in one call (optimistic strategy).',\n payload: [{ name: 'ids', type: 'string[]' }],\n },\n increaseQuantity: {\n description: \"Increase a line's quantity by N (default 1, optimistic strategy).\",\n payload: [\n { name: 'id', type: 'string' },\n { name: 'by', type: 'number', optional: true },\n ],\n },\n decreaseQuantity: {\n description: \"Decrease a line's quantity by N (default 1, optimistic). Removes the line at <= 0.\",\n payload: [\n { name: 'id', type: 'string' },\n { name: 'by', type: 'number', optional: true },\n ],\n },\n applyCoupon: {\n description: 'Apply a coupon code to the cart (async strategy). Server recomputes totals.',\n payload: [{ name: 'code', type: 'string' }],\n },\n removeCoupon: {\n description: 'Remove a coupon code from the cart (async strategy). Server recomputes totals.',\n payload: [{ name: 'code', type: 'string' }],\n },\n validate: {\n description:\n 'Re-check the active cart against the platform and adopt corrected availability (async, read-only upstream).',\n payload: [],\n },\n },\n use: useCartActions,\n});\n"],"mappings":"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"}
@@ -2,8 +2,18 @@ import { buildStorefrontContextHeaders } from "../context/headers.js";
2
2
  import { buildCache } from "../cache/storefront-cache.js";
3
3
  import { assertValidApiUrl } from "./assert-api-url.js";
4
4
  import { ApolloClient, HttpLink } from "@apollo/client";
5
+ import { SetContextLink } from "@apollo/client/link/context";
6
+ import { cookies } from "next/headers";
5
7
  //#region src/sdk/client/create-client.ts
6
8
  /**
9
+ * Whether a caller already decided the `Cookie` header themselves, in which case the request's own
10
+ * cookie must not replace it. Matched case-insensitively: header names are, and Apollo lowercases
11
+ * them last-write-wins before the fetch, so a caller's `cookie` would otherwise lose to our `Cookie`.
12
+ */
13
+ function hasCookieHeader(headers) {
14
+ return headers !== void 0 && Object.keys(headers).some((name) => name.toLowerCase() === "cookie");
15
+ }
16
+ /**
7
17
  * Returns a fresh, stateless Apollo Client suitable for server-side
8
18
  * rendering / RSC calls. Every invocation builds a new cache — callers
9
19
  * that need "one client per lambda" are responsible for caching the
@@ -21,13 +31,25 @@ function createClient(options = {}) {
21
31
  ...options.headers,
22
32
  ...buildStorefrontContextHeaders(options.context)
23
33
  } : options.headers;
24
- const link = new HttpLink({
34
+ const httpLink = new HttpLink({
25
35
  uri: apiUrl,
26
36
  ...headers ? { headers } : {}
27
37
  });
38
+ const cookieForwardingLink = new SetContextLink(async (prevContext) => {
39
+ if (hasCookieHeader(headers) || hasCookieHeader(prevContext.headers)) return prevContext;
40
+ try {
41
+ const cookieHeader = (await cookies()).toString();
42
+ return cookieHeader ? { headers: {
43
+ ...prevContext.headers,
44
+ Cookie: cookieHeader
45
+ } } : prevContext;
46
+ } catch {
47
+ return prevContext;
48
+ }
49
+ });
28
50
  return new ApolloClient({
29
51
  cache: buildCache(),
30
- link,
52
+ link: cookieForwardingLink.concat(httpLink),
31
53
  defaultOptions: {
32
54
  query: {
33
55
  errorPolicy: "all",
@@ -1 +1 @@
1
- {"version":3,"file":"create-client.js","names":[],"sources":["../../../src/sdk/client/create-client.ts"],"sourcesContent":["import { ApolloClient, HttpLink } from '@apollo/client';\nimport type { StorefrontContext } from '@vtex/fs-core';\nimport { buildCache } from '../cache/storefront-cache.js';\nimport { buildStorefrontContextHeaders } from '../context/headers.js';\nimport { assertValidApiUrl } from './assert-api-url.js';\n\nexport type CreateClientOptions = {\n apiUrl?: string;\n /** The compiler-emitted context; its three headers ride every operation this client sends. */\n context?: StorefrontContext;\n headers?: Record<string, string>;\n};\n\n/**\n * Returns a fresh, stateless Apollo Client suitable for server-side\n * rendering / RSC calls. Every invocation builds a new cache — callers\n * that need \"one client per lambda\" are responsible for caching the\n * returned instance themselves (see the generated `utils/query-client.ts`).\n *\n * The client shares its `typePolicies` + `possibleTypes` with the\n * browser-side client via `buildCache()` so normalized entities line up\n * across SSR / CSR boundaries.\n */\nexport function createClient(options: CreateClientOptions = {}): ApolloClient {\n const apiUrl = options.apiUrl ?? process.env.STOREFRONT_API_URL;\n\n if (!apiUrl) {\n throw new Error(\n 'createClient: `apiUrl` is required. Pass it via options or set the ' +\n '`STOREFRONT_API_URL` environment variable.',\n );\n }\n\n assertValidApiUrl(apiUrl, 'createClient');\n\n // Context headers merge last: a caller cannot contradict the context the app rendered. To request a\n // different locale, binding or sales channel, pass a different `context`.\n const headers = options.context\n ? { ...options.headers, ...buildStorefrontContextHeaders(options.context) }\n : options.headers;\n\n const link = new HttpLink({\n uri: apiUrl,\n ...(headers ? { headers } : {}),\n });\n\n return new ApolloClient({\n cache: buildCache(),\n link,\n defaultOptions: {\n query: { errorPolicy: 'all', fetchPolicy: 'no-cache' },\n watchQuery: { errorPolicy: 'all', fetchPolicy: 'no-cache' },\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuBA,SAAgB,aAAa,UAA+B,CAAC,GAAiB;CAC5E,MAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI;CAE7C,IAAI,CAAC,QACH,MAAM,IAAI,MACR,+GAEF;CAGF,kBAAkB,QAAQ,cAAc;CAIxC,MAAM,UAAU,QAAQ,UACpB;EAAE,GAAG,QAAQ;EAAS,GAAG,8BAA8B,QAAQ,OAAO;CAAE,IACxE,QAAQ;CAEZ,MAAM,OAAO,IAAI,SAAS;EACxB,KAAK;EACL,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;CAC/B,CAAC;CAED,OAAO,IAAI,aAAa;EACtB,OAAO,WAAW;EAClB;EACA,gBAAgB;GACd,OAAO;IAAE,aAAa;IAAO,aAAa;GAAW;GACrD,YAAY;IAAE,aAAa;IAAO,aAAa;GAAW;EAC5D;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"create-client.js","names":[],"sources":["../../../src/sdk/client/create-client.ts"],"sourcesContent":["import { ApolloClient, HttpLink } from '@apollo/client';\nimport { SetContextLink } from '@apollo/client/link/context';\nimport type { StorefrontContext } from '@vtex/fs-core';\nimport { cookies } from 'next/headers';\nimport { buildCache } from '../cache/storefront-cache.js';\nimport { buildStorefrontContextHeaders } from '../context/headers.js';\nimport { assertValidApiUrl } from './assert-api-url.js';\n\nexport type CreateClientOptions = {\n apiUrl?: string;\n /** The compiler-emitted context; its three headers ride every operation this client sends. */\n context?: StorefrontContext;\n headers?: Record<string, string>;\n};\n\n/**\n * Whether a caller already decided the `Cookie` header themselves, in which case the request's own\n * cookie must not replace it. Matched case-insensitively: header names are, and Apollo lowercases\n * them last-write-wins before the fetch, so a caller's `cookie` would otherwise lose to our `Cookie`.\n */\nfunction hasCookieHeader(headers: Record<string, string> | undefined): boolean {\n return headers !== undefined && Object.keys(headers).some((name) => name.toLowerCase() === 'cookie');\n}\n\n/**\n * Returns a fresh, stateless Apollo Client suitable for server-side\n * rendering / RSC calls. Every invocation builds a new cache — callers\n * that need \"one client per lambda\" are responsible for caching the\n * returned instance themselves (see the generated `utils/query-client.ts`).\n *\n * The client shares its `typePolicies` + `possibleTypes` with the\n * browser-side client via `buildCache()` so normalized entities line up\n * across SSR / CSR boundaries.\n */\nexport function createClient(options: CreateClientOptions = {}): ApolloClient {\n const apiUrl = options.apiUrl ?? process.env.STOREFRONT_API_URL;\n\n if (!apiUrl) {\n throw new Error(\n 'createClient: `apiUrl` is required. Pass it via options or set the ' +\n '`STOREFRONT_API_URL` environment variable.',\n );\n }\n\n assertValidApiUrl(apiUrl, 'createClient');\n\n // Context headers merge last: a caller cannot contradict the context the app rendered. To request a\n // different locale, binding or sales channel, pass a different `context`.\n const headers = options.context\n ? { ...options.headers, ...buildStorefrontContextHeaders(options.context) }\n : options.headers;\n\n const httpLink = new HttpLink({\n uri: apiUrl,\n ...(headers ? { headers } : {}),\n });\n\n // Forwards the incoming request's cookie into the SSR call, mirroring the generated app's\n // `/api/graphql` rewrite, which already forwards that same header unfiltered for client-side\n // traffic. `storefront-api-core`'s allowlist stays the one boundary deciding what reaches VTEX ID\n // and Session Manager, so this link deliberately carries no allowlist of its own.\n //\n // The read is lazy, at query-execution time, so `createClient` and the `queryClientFor` that calls\n // it stay synchronous and no compiler-emitted call site has to await anything new.\n //\n // `cookies()` also throws during a static prerender, to interrupt it. Swallowing that here is safe:\n // Next marks the route dynamic (`revalidate = 0`) before throwing, so the page still re-renders per\n // request — it just renders that one build-time pass with no shopper.\n const cookieForwardingLink = new SetContextLink(async (prevContext) => {\n if (hasCookieHeader(headers) || hasCookieHeader(prevContext.headers)) return prevContext;\n\n try {\n const requestCookies = await cookies();\n // `.toString()` re-encodes each value the way `RequestCookies` decoded it on read, so a value\n // that legitimately contains `;` or `=` round-trips intact. Rebuilding the header manually from\n // `getAll()` would forward the already-decoded value verbatim instead, corrupting it.\n const cookieHeader = requestCookies.toString();\n\n return cookieHeader ? { headers: { ...prevContext.headers, Cookie: cookieHeader } } : prevContext;\n } catch {\n return prevContext;\n }\n });\n\n return new ApolloClient({\n cache: buildCache(),\n link: cookieForwardingLink.concat(httpLink),\n defaultOptions: {\n query: { errorPolicy: 'all', fetchPolicy: 'no-cache' },\n watchQuery: { errorPolicy: 'all', fetchPolicy: 'no-cache' },\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAS,gBAAgB,SAAsD;CAC7E,OAAO,YAAY,KAAA,KAAa,OAAO,KAAK,OAAO,CAAC,CAAC,MAAM,SAAS,KAAK,YAAY,MAAM,QAAQ;AACrG;;;;;;;;;;;AAYA,SAAgB,aAAa,UAA+B,CAAC,GAAiB;CAC5E,MAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI;CAE7C,IAAI,CAAC,QACH,MAAM,IAAI,MACR,+GAEF;CAGF,kBAAkB,QAAQ,cAAc;CAIxC,MAAM,UAAU,QAAQ,UACpB;EAAE,GAAG,QAAQ;EAAS,GAAG,8BAA8B,QAAQ,OAAO;CAAE,IACxE,QAAQ;CAEZ,MAAM,WAAW,IAAI,SAAS;EAC5B,KAAK;EACL,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;CAC/B,CAAC;CAaD,MAAM,uBAAuB,IAAI,eAAe,OAAO,gBAAgB;EACrE,IAAI,gBAAgB,OAAO,KAAK,gBAAgB,YAAY,OAAO,GAAG,OAAO;EAE7E,IAAI;GAKF,MAAM,gBAAe,MAJQ,QAAQ,EAAA,CAID,SAAS;GAE7C,OAAO,eAAe,EAAE,SAAS;IAAE,GAAG,YAAY;IAAS,QAAQ;GAAa,EAAE,IAAI;EACxF,QAAQ;GACN,OAAO;EACT;CACF,CAAC;CAED,OAAO,IAAI,aAAa;EACtB,OAAO,WAAW;EAClB,MAAM,qBAAqB,OAAO,QAAQ;EAC1C,gBAAgB;GACd,OAAO;IAAE,aAAa;IAAO,aAAa;GAAW;GACrD,YAAY;IAAE,aAAa;IAAO,aAAa;GAAW;EAC5D;CACF,CAAC;AACH"}
@@ -1,6 +1,6 @@
1
1
  import { cache } from "react";
2
- import { createHmac, timingSafeEqual } from "node:crypto";
3
2
  import { cookies, draftMode } from "next/headers";
3
+ import { createHmac, timingSafeEqual } from "node:crypto";
4
4
  import { connection } from "next/server";
5
5
  //#region src/sdk/content/create-content-runtime.tsx
6
6
  const ENTRY_REQUEST_TIMEOUT_MS = 5e3;
@@ -49,6 +49,112 @@ function resolveSafeHref(raw) {
49
49
  reason: "forbidden-scheme"
50
50
  };
51
51
  }
52
+ /** The storefront's own sign-in route, which forwards to the platform's login. A fixed convention,
53
+ * never an authored value. */
54
+ const SIGN_IN_ROUTE = "/login";
55
+ /** The parameter carrying the shopper's way back, read by the sign-in route and the platform's login. */
56
+ const RETURN_URL_PARAM = "returnUrl";
57
+ /**
58
+ * `resolveSafeHref` narrowed to the site-relative subset: the destinations a shopper may be sent back to
59
+ * across the auth hop. The allowlist admits absolute http(s) values, which are legitimate for an authored
60
+ * link but are exactly what would turn the hop into an open redirect. Every other rejection travels out
61
+ * unchanged, so a caller logging the reason still sees which spelling arrived.
62
+ *
63
+ * Both ends of the flow narrow through here rather than only the sender, because the sender cannot vouch
64
+ * for a URL the shopper typed: the account button builds the destination through `resolveSignInHref`, and
65
+ * the sign-in route revalidates what arrives through `resolveReturnHref`.
66
+ */
67
+ function resolveSiteRelativeHref(raw) {
68
+ const destination = resolveSafeHref(raw);
69
+ if (!destination.safe) return destination;
70
+ return destination.href.startsWith("/") ? destination : {
71
+ safe: false,
72
+ reason: "absolute"
73
+ };
74
+ }
75
+ /** The subset of the allowlist that resolves to a page a document can be handed over to. */
76
+ const NAVIGABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
77
+ /**
78
+ * `resolveSafeHref` narrowed to the destinations that resolve to a page: site-relative, or absolute
79
+ * `http(s)`. `mailto:` and `tel:` are legitimate for an authored link and refused here, since handing a
80
+ * document over to one leaves the shopper in a mail client instead of a login.
81
+ */
82
+ function resolveNavigableHref(raw) {
83
+ const destination = resolveSafeHref(raw);
84
+ if (!destination.safe || destination.href.startsWith("/")) return destination;
85
+ const { protocol } = new URL(destination.href);
86
+ return NAVIGABLE_PROTOCOLS.has(protocol) ? destination : {
87
+ safe: false,
88
+ reason: "forbidden-scheme"
89
+ };
90
+ }
91
+ /**
92
+ * `resolveSiteRelativeHref` narrowed further, for a return arriving at the sign-in route: the route
93
+ * itself is refused. Admitting it sends a shopper who just authenticated back to the trampoline, which
94
+ * hands them to the platform a second time — a sign-in that never lands where the parameter promised.
95
+ */
96
+ function resolveReturnHref(raw) {
97
+ const destination = resolveSiteRelativeHref(raw);
98
+ if (!destination.safe) return destination;
99
+ return isSignInRoute(destination.href) ? {
100
+ safe: false,
101
+ reason: "sign-in-route"
102
+ } : destination;
103
+ }
104
+ /** Whether a site-relative value names the storefront's own sign-in route, in any of its spellings. */
105
+ function isSignInRoute(href) {
106
+ return pathOf(href) === SIGN_IN_ROUTE;
107
+ }
108
+ /**
109
+ * `resolveNavigableHref` narrowed for the address a handover targets: the storefront's own sign-in
110
+ * route is refused. Sending `/login` to `/login` reloads the same document, which mounts the same
111
+ * handover again — a loop the shopper never gets past, and the mirror of the refusal
112
+ * `resolveReturnHref` already makes on the way back.
113
+ *
114
+ * Only the site-relative spelling is decidable here, being same-origin by construction. An absolute
115
+ * address needs the document's own origin to compare against, which this module does not have and a
116
+ * prerender does not either, so `isSignInRoute` is exported for the caller that does. A cross-origin
117
+ * login page whose path happens to be `/login` is a different page and is left alone.
118
+ */
119
+ function resolveLoginAddressHref(raw) {
120
+ const address = resolveNavigableHref(raw);
121
+ if (!address.safe || !address.href.startsWith("/")) return address;
122
+ return isSignInRoute(address.href) ? {
123
+ safe: false,
124
+ reason: "sign-in-route"
125
+ } : address;
126
+ }
127
+ /**
128
+ * Builds the sign-in destination, carrying the current path so the shopper can be returned where they
129
+ * started. The sender's half only: nothing stops a hand-crafted link, so the sign-in route is to narrow
130
+ * the parameter again on arrival. A path the narrowing refuses drops the parameter instead of
131
+ * travelling encoded, and so does a router reporting no path — `usePathname()`'s type is why this takes
132
+ * a nullable one.
133
+ */
134
+ function resolveSignInHref(pathname) {
135
+ if (!pathname) return SIGN_IN_ROUTE;
136
+ const destination = resolveSiteRelativeHref(pathname);
137
+ if (!destination.safe) return SIGN_IN_ROUTE;
138
+ return `${SIGN_IN_ROUTE}?${RETURN_URL_PARAM}=${encodeURIComponent(destination.href)}`;
139
+ }
140
+ /** Opaque base for resolving a site-relative value to a path. Reserved by RFC 6761, so it never names a
141
+ * real host; nothing is navigated to it and only `pathname` is read back. */
142
+ const PATH_COMPARISON_BASE = "https://store.invalid";
143
+ /**
144
+ * The path of a site-relative value, without its query or fragment and without a trailing slash, so a
145
+ * route comparison reads the route rather than one of its spellings. `/loginhelp` is a different path
146
+ * and stays one.
147
+ *
148
+ * Parsed rather than sliced, because `/account/../login`, `/account\..\login` and `/account/%2e%2e/login`
149
+ * are all spellings a URL parser resolves to `/login`. A slice compares them literally and admits them,
150
+ * and the value then reaches a receiver that does resolve them — which puts the shopper back on the
151
+ * trampoline the refusal exists to prevent. Only the comparison is normalized: the caller returns the
152
+ * value as it arrived, so the promise that nothing is silently rewritten still holds.
153
+ */
154
+ function pathOf(href) {
155
+ const { pathname } = new URL(href, PATH_COMPARISON_BASE);
156
+ return pathname.replace(/\/+$/, "") || "/";
157
+ }
52
158
  /** Any C0 control or space anywhere in the value — the set a URL parser removes or trims. */
53
159
  function hasStrippableCharacter(value) {
54
160
  for (const character of value) {
@@ -58,6 +164,6 @@ function hasStrippableCharacter(value) {
58
164
  return false;
59
165
  }
60
166
  //#endregion
61
- export { resolveSafeHref };
167
+ export { RETURN_URL_PARAM, isSignInRoute, resolveLoginAddressHref, resolveNavigableHref, resolveReturnHref, resolveSafeHref, resolveSignInHref, resolveSiteRelativeHref };
62
168
 
63
169
  //# sourceMappingURL=safe-href.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"safe-href.js","names":[],"sources":["../../src/utils/safe-href.ts"],"sourcesContent":["export type SafeHrefRejection = 'blank' | 'malformed' | 'protocol-relative' | 'forbidden-scheme';\n\nexport type SafeHrefResult = { safe: true; href: string } | { safe: false; reason: SafeHrefRejection };\n\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * The only gate on a CMS-authored destination: `http:`, `https:`, `mailto:`, `tel:`, and paths\n * starting with a single `/`. Neither the runtime slice nor the sukima preflight enforces a model's\n * `pattern`, so an authored `javascript:` value reaches the component intact and this is what stands\n * between it and an `href`.\n *\n * Rejects rather than sanitizes: a destination that comes back safe is character-for-character the\n * trimmed authored value, so no value is ever silently rewritten into a different working one.\n */\nexport function resolveSafeHref(raw: string): SafeHrefResult {\n const href = raw.trim();\n if (href.length === 0) {\n return { safe: false, reason: 'blank' };\n }\n // Browsers strip ASCII tabs and newlines from a URL before parsing it, so `java<TAB>script:alert(1)`\n // executes. Refusing the whole value is the only defense that does not depend on stripping the same\n // characters in the same order the parser does.\n if (hasStrippableCharacter(href)) {\n return { safe: false, reason: 'malformed' };\n }\n // A URL parser reads `\\` as `/` under a special scheme, so `/\\evil.example` resolves against the\n // store's origin as an authority rather than a path. Any pair of leading slashes is an off-site\n // jump however it is spelled, and only `//` looks like one.\n if (/^[/\\\\]{2}/.test(href)) {\n return { safe: false, reason: 'protocol-relative' };\n }\n if (href.startsWith('/')) {\n return { safe: true, href };\n }\n\n let protocol: string;\n try {\n protocol = new URL(href).protocol;\n } catch {\n return { safe: false, reason: 'malformed' };\n }\n // `new URL` lowercases the protocol, so `JavaScript:` and `javascript:` are the same rejection.\n return ALLOWED_PROTOCOLS.has(protocol) ? { safe: true, href } : { safe: false, reason: 'forbidden-scheme' };\n}\n\n/** Any C0 control or space anywhere in the value — the set a URL parser removes or trims. */\nfunction hasStrippableCharacter(value: string): boolean {\n for (const character of value) {\n const code = character.codePointAt(0) ?? 0;\n if (code <= 0x20 || code === 0x7f) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";AAIA,MAAM,oCAAoB,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;AAAM,CAAC;;;;;;;;;;AAWxE,SAAgB,gBAAgB,KAA6B;CAC3D,MAAM,OAAO,IAAI,KAAK;CACtB,IAAI,KAAK,WAAW,GAClB,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAQ;CAKxC,IAAI,uBAAuB,IAAI,GAC7B,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAY;CAK5C,IAAI,YAAY,KAAK,IAAI,GACvB,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAoB;CAEpD,IAAI,KAAK,WAAW,GAAG,GACrB,OAAO;EAAE,MAAM;EAAM;CAAK;CAG5B,IAAI;CACJ,IAAI;EACF,WAAW,IAAI,IAAI,IAAI,CAAC,CAAC;CAC3B,QAAQ;EACN,OAAO;GAAE,MAAM;GAAO,QAAQ;EAAY;CAC5C;CAEA,OAAO,kBAAkB,IAAI,QAAQ,IAAI;EAAE,MAAM;EAAM;CAAK,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAmB;AAC5G;;AAGA,SAAS,uBAAuB,OAAwB;CACtD,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,OAAO,UAAU,YAAY,CAAC,KAAK;EACzC,IAAI,QAAQ,MAAQ,SAAS,KAC3B,OAAO;CAEX;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"safe-href.js","names":[],"sources":["../../src/utils/safe-href.ts"],"sourcesContent":["export type SafeHrefRejection =\n | 'blank'\n | 'malformed'\n | 'protocol-relative'\n | 'forbidden-scheme'\n | 'absolute'\n | 'sign-in-route';\n\nexport type SafeHrefResult = { safe: true; href: string } | { safe: false; reason: SafeHrefRejection };\n\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * The only gate on a CMS-authored destination: `http:`, `https:`, `mailto:`, `tel:`, and paths\n * starting with a single `/`. Neither the runtime slice nor the sukima preflight enforces a model's\n * `pattern`, so an authored `javascript:` value reaches the component intact and this is what stands\n * between it and an `href`.\n *\n * Rejects rather than sanitizes: a destination that comes back safe is character-for-character the\n * trimmed authored value, so no value is ever silently rewritten into a different working one.\n */\nexport function resolveSafeHref(raw: string): SafeHrefResult {\n const href = raw.trim();\n if (href.length === 0) {\n return { safe: false, reason: 'blank' };\n }\n // Browsers strip ASCII tabs and newlines from a URL before parsing it, so `java<TAB>script:alert(1)`\n // executes. Refusing the whole value is the only defense that does not depend on stripping the same\n // characters in the same order the parser does.\n if (hasStrippableCharacter(href)) {\n return { safe: false, reason: 'malformed' };\n }\n // A URL parser reads `\\` as `/` under a special scheme, so `/\\evil.example` resolves against the\n // store's origin as an authority rather than a path. Any pair of leading slashes is an off-site\n // jump however it is spelled, and only `//` looks like one.\n if (/^[/\\\\]{2}/.test(href)) {\n return { safe: false, reason: 'protocol-relative' };\n }\n if (href.startsWith('/')) {\n return { safe: true, href };\n }\n\n let protocol: string;\n try {\n protocol = new URL(href).protocol;\n } catch {\n return { safe: false, reason: 'malformed' };\n }\n // `new URL` lowercases the protocol, so `JavaScript:` and `javascript:` are the same rejection.\n return ALLOWED_PROTOCOLS.has(protocol) ? { safe: true, href } : { safe: false, reason: 'forbidden-scheme' };\n}\n\n/** The storefront's own sign-in route, which forwards to the platform's login. A fixed convention,\n * never an authored value. */\nconst SIGN_IN_ROUTE = '/login';\n\n/** The parameter carrying the shopper's way back, read by the sign-in route and the platform's login. */\nexport const RETURN_URL_PARAM = 'returnUrl';\n\n/**\n * `resolveSafeHref` narrowed to the site-relative subset: the destinations a shopper may be sent back to\n * across the auth hop. The allowlist admits absolute http(s) values, which are legitimate for an authored\n * link but are exactly what would turn the hop into an open redirect. Every other rejection travels out\n * unchanged, so a caller logging the reason still sees which spelling arrived.\n *\n * Both ends of the flow narrow through here rather than only the sender, because the sender cannot vouch\n * for a URL the shopper typed: the account button builds the destination through `resolveSignInHref`, and\n * the sign-in route revalidates what arrives through `resolveReturnHref`.\n */\nexport function resolveSiteRelativeHref(raw: string): SafeHrefResult {\n const destination = resolveSafeHref(raw);\n if (!destination.safe) {\n return destination;\n }\n return destination.href.startsWith('/') ? destination : { safe: false, reason: 'absolute' };\n}\n\n/** The subset of the allowlist that resolves to a page a document can be handed over to. */\nconst NAVIGABLE_PROTOCOLS = new Set(['http:', 'https:']);\n\n/**\n * `resolveSafeHref` narrowed to the destinations that resolve to a page: site-relative, or absolute\n * `http(s)`. `mailto:` and `tel:` are legitimate for an authored link and refused here, since handing a\n * document over to one leaves the shopper in a mail client instead of a login.\n */\nexport function resolveNavigableHref(raw: string): SafeHrefResult {\n const destination = resolveSafeHref(raw);\n if (!destination.safe || destination.href.startsWith('/')) {\n return destination;\n }\n // The allowlist already parsed this value, so the protocol is readable without a second guard.\n const { protocol } = new URL(destination.href);\n return NAVIGABLE_PROTOCOLS.has(protocol) ? destination : { safe: false, reason: 'forbidden-scheme' };\n}\n\n/**\n * `resolveSiteRelativeHref` narrowed further, for a return arriving at the sign-in route: the route\n * itself is refused. Admitting it sends a shopper who just authenticated back to the trampoline, which\n * hands them to the platform a second time — a sign-in that never lands where the parameter promised.\n */\nexport function resolveReturnHref(raw: string): SafeHrefResult {\n const destination = resolveSiteRelativeHref(raw);\n if (!destination.safe) {\n return destination;\n }\n return isSignInRoute(destination.href) ? { safe: false, reason: 'sign-in-route' } : destination;\n}\n\n/** Whether a site-relative value names the storefront's own sign-in route, in any of its spellings. */\nexport function isSignInRoute(href: string): boolean {\n return pathOf(href) === SIGN_IN_ROUTE;\n}\n\n/**\n * `resolveNavigableHref` narrowed for the address a handover targets: the storefront's own sign-in\n * route is refused. Sending `/login` to `/login` reloads the same document, which mounts the same\n * handover again — a loop the shopper never gets past, and the mirror of the refusal\n * `resolveReturnHref` already makes on the way back.\n *\n * Only the site-relative spelling is decidable here, being same-origin by construction. An absolute\n * address needs the document's own origin to compare against, which this module does not have and a\n * prerender does not either, so `isSignInRoute` is exported for the caller that does. A cross-origin\n * login page whose path happens to be `/login` is a different page and is left alone.\n */\nexport function resolveLoginAddressHref(raw: string): SafeHrefResult {\n const address = resolveNavigableHref(raw);\n if (!address.safe || !address.href.startsWith('/')) {\n return address;\n }\n return isSignInRoute(address.href) ? { safe: false, reason: 'sign-in-route' } : address;\n}\n\n/**\n * Builds the sign-in destination, carrying the current path so the shopper can be returned where they\n * started. The sender's half only: nothing stops a hand-crafted link, so the sign-in route is to narrow\n * the parameter again on arrival. A path the narrowing refuses drops the parameter instead of\n * travelling encoded, and so does a router reporting no path — `usePathname()`'s type is why this takes\n * a nullable one.\n */\nexport function resolveSignInHref(pathname: string | null): string {\n if (!pathname) {\n return SIGN_IN_ROUTE;\n }\n const destination = resolveSiteRelativeHref(pathname);\n if (!destination.safe) {\n return SIGN_IN_ROUTE;\n }\n return `${SIGN_IN_ROUTE}?${RETURN_URL_PARAM}=${encodeURIComponent(destination.href)}`;\n}\n\n/** Opaque base for resolving a site-relative value to a path. Reserved by RFC 6761, so it never names a\n * real host; nothing is navigated to it and only `pathname` is read back. */\nconst PATH_COMPARISON_BASE = 'https://store.invalid';\n\n/**\n * The path of a site-relative value, without its query or fragment and without a trailing slash, so a\n * route comparison reads the route rather than one of its spellings. `/loginhelp` is a different path\n * and stays one.\n *\n * Parsed rather than sliced, because `/account/../login`, `/account\\..\\login` and `/account/%2e%2e/login`\n * are all spellings a URL parser resolves to `/login`. A slice compares them literally and admits them,\n * and the value then reaches a receiver that does resolve them — which puts the shopper back on the\n * trampoline the refusal exists to prevent. Only the comparison is normalized: the caller returns the\n * value as it arrived, so the promise that nothing is silently rewritten still holds.\n */\nfunction pathOf(href: string): string {\n const { pathname } = new URL(href, PATH_COMPARISON_BASE);\n return pathname.replace(/\\/+$/, '') || '/';\n}\n\n/** Any C0 control or space anywhere in the value — the set a URL parser removes or trims. */\nfunction hasStrippableCharacter(value: string): boolean {\n for (const character of value) {\n const code = character.codePointAt(0) ?? 0;\n if (code <= 0x20 || code === 0x7f) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";AAUA,MAAM,oCAAoB,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;AAAM,CAAC;;;;;;;;;;AAWxE,SAAgB,gBAAgB,KAA6B;CAC3D,MAAM,OAAO,IAAI,KAAK;CACtB,IAAI,KAAK,WAAW,GAClB,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAQ;CAKxC,IAAI,uBAAuB,IAAI,GAC7B,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAY;CAK5C,IAAI,YAAY,KAAK,IAAI,GACvB,OAAO;EAAE,MAAM;EAAO,QAAQ;CAAoB;CAEpD,IAAI,KAAK,WAAW,GAAG,GACrB,OAAO;EAAE,MAAM;EAAM;CAAK;CAG5B,IAAI;CACJ,IAAI;EACF,WAAW,IAAI,IAAI,IAAI,CAAC,CAAC;CAC3B,QAAQ;EACN,OAAO;GAAE,MAAM;GAAO,QAAQ;EAAY;CAC5C;CAEA,OAAO,kBAAkB,IAAI,QAAQ,IAAI;EAAE,MAAM;EAAM;CAAK,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAmB;AAC5G;;;AAIA,MAAM,gBAAgB;;AAGtB,MAAa,mBAAmB;;;;;;;;;;;AAYhC,SAAgB,wBAAwB,KAA6B;CACnE,MAAM,cAAc,gBAAgB,GAAG;CACvC,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,YAAY,KAAK,WAAW,GAAG,IAAI,cAAc;EAAE,MAAM;EAAO,QAAQ;CAAW;AAC5F;;AAGA,MAAM,sCAAsB,IAAI,IAAI,CAAC,SAAS,QAAQ,CAAC;;;;;;AAOvD,SAAgB,qBAAqB,KAA6B;CAChE,MAAM,cAAc,gBAAgB,GAAG;CACvC,IAAI,CAAC,YAAY,QAAQ,YAAY,KAAK,WAAW,GAAG,GACtD,OAAO;CAGT,MAAM,EAAE,aAAa,IAAI,IAAI,YAAY,IAAI;CAC7C,OAAO,oBAAoB,IAAI,QAAQ,IAAI,cAAc;EAAE,MAAM;EAAO,QAAQ;CAAmB;AACrG;;;;;;AAOA,SAAgB,kBAAkB,KAA6B;CAC7D,MAAM,cAAc,wBAAwB,GAAG;CAC/C,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,cAAc,YAAY,IAAI,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAgB,IAAI;AACtF;;AAGA,SAAgB,cAAc,MAAuB;CACnD,OAAO,OAAO,IAAI,MAAM;AAC1B;;;;;;;;;;;;AAaA,SAAgB,wBAAwB,KAA6B;CACnE,MAAM,UAAU,qBAAqB,GAAG;CACxC,IAAI,CAAC,QAAQ,QAAQ,CAAC,QAAQ,KAAK,WAAW,GAAG,GAC/C,OAAO;CAET,OAAO,cAAc,QAAQ,IAAI,IAAI;EAAE,MAAM;EAAO,QAAQ;CAAgB,IAAI;AAClF;;;;;;;;AASA,SAAgB,kBAAkB,UAAiC;CACjE,IAAI,CAAC,UACH,OAAO;CAET,MAAM,cAAc,wBAAwB,QAAQ;CACpD,IAAI,CAAC,YAAY,MACf,OAAO;CAET,OAAO,GAAG,cAAc,GAAG,iBAAiB,GAAG,mBAAmB,YAAY,IAAI;AACpF;;;AAIA,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,SAAS,OAAO,MAAsB;CACpC,MAAM,EAAE,aAAa,IAAI,IAAI,MAAM,oBAAoB;CACvD,OAAO,SAAS,QAAQ,QAAQ,EAAE,KAAK;AACzC;;AAGA,SAAS,uBAAuB,OAAwB;CACtD,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,OAAO,UAAU,YAAY,CAAC,KAAK;EACzC,IAAI,QAAQ,MAAQ,SAAS,KAC3B,OAAO;CAEX;CACA,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "FastStore AI Native component library built on Base UI with AI-editable styles",
5
5
  "license": "MIT",
6
6
  "homepage": "https://developers.vtex.com/docs/guides/faststore",