@shopify/hydrogen 2026.10.0-preview.2 → 2026.10.0-preview.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 (93) hide show
  1. package/README.md +32 -1
  2. package/dist/cli/index.mjs +464 -73
  3. package/dist/core/cart/actions.mjs +2 -0
  4. package/dist/core/cart/actions.mjs.map +1 -1
  5. package/dist/core/cart/cart.mjs +16 -6
  6. package/dist/core/cart/cart.mjs.map +1 -1
  7. package/dist/core/product/form.d.mts +21 -2
  8. package/dist/core/product/form.mjs +13 -0
  9. package/dist/core/product/form.mjs.map +1 -1
  10. package/dist/core/product/index.d.mts +1 -1
  11. package/dist/core/request-context.d.mts +6 -0
  12. package/dist/core/request-context.mjs +6 -2
  13. package/dist/core/request-context.mjs.map +1 -1
  14. package/dist/core/request-routing/handle-shopify-routes.mjs +2 -0
  15. package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
  16. package/dist/core/request-routing/interceptors/checkout.mjs +4 -1
  17. package/dist/core/request-routing/interceptors/checkout.mjs.map +1 -1
  18. package/dist/core/request-routing/interceptors/proxy.mjs +19 -8
  19. package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
  20. package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
  21. package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
  22. package/dist/core/shopify-scripts/analytics.mjs +1 -1
  23. package/dist/core/shopify-scripts/analytics.mjs.map +1 -1
  24. package/dist/core/shopify-scripts/constants.mjs +3 -1
  25. package/dist/core/shopify-scripts/constants.mjs.map +1 -1
  26. package/dist/core/shopify-scripts/deprecated-cookies.mjs +2 -1
  27. package/dist/core/shopify-scripts/deprecated-cookies.mjs.map +1 -1
  28. package/dist/core/shopify-scripts/global.mjs +5 -4
  29. package/dist/core/shopify-scripts/global.mjs.map +1 -1
  30. package/dist/core/shopify-scripts/index.d.mts +1 -0
  31. package/dist/core/shopify-scripts/index.mjs +14 -12
  32. package/dist/core/shopify-scripts/index.mjs.map +1 -1
  33. package/dist/core/shopify-scripts/types.d.mts +5 -0
  34. package/dist/core/url.mjs +2 -1
  35. package/dist/core/url.mjs.map +1 -1
  36. package/dist/customer-account/client.mjs +1 -1
  37. package/dist/customer-account/session.mjs +1 -1
  38. package/dist/development/core/cart/actions.mjs +2 -0
  39. package/dist/development/core/cart/actions.mjs.map +1 -1
  40. package/dist/development/core/cart/cart.mjs +16 -6
  41. package/dist/development/core/cart/cart.mjs.map +1 -1
  42. package/dist/development/core/cart/queries.d.mts +57 -57
  43. package/dist/development/core/predictive-search/queries.d.mts +2 -2
  44. package/dist/development/core/product/form.d.mts +21 -2
  45. package/dist/development/core/product/form.mjs +13 -0
  46. package/dist/development/core/product/form.mjs.map +1 -1
  47. package/dist/development/core/product/index.d.mts +1 -1
  48. package/dist/development/core/request-context.d.mts +6 -0
  49. package/dist/development/core/request-context.mjs +6 -2
  50. package/dist/development/core/request-context.mjs.map +1 -1
  51. package/dist/development/core/request-routing/handle-shopify-routes.mjs +2 -0
  52. package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
  53. package/dist/development/core/request-routing/interceptors/checkout.mjs +4 -1
  54. package/dist/development/core/request-routing/interceptors/checkout.mjs.map +1 -1
  55. package/dist/development/core/request-routing/interceptors/proxy.mjs +19 -8
  56. package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
  57. package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
  58. package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
  59. package/dist/development/core/shopify-scripts/analytics.mjs +1 -1
  60. package/dist/development/core/shopify-scripts/analytics.mjs.map +1 -1
  61. package/dist/development/core/shopify-scripts/constants.mjs +3 -1
  62. package/dist/development/core/shopify-scripts/constants.mjs.map +1 -1
  63. package/dist/development/core/shopify-scripts/deprecated-cookies.mjs +2 -1
  64. package/dist/development/core/shopify-scripts/deprecated-cookies.mjs.map +1 -1
  65. package/dist/development/core/shopify-scripts/global.mjs +5 -4
  66. package/dist/development/core/shopify-scripts/global.mjs.map +1 -1
  67. package/dist/development/core/shopify-scripts/index.d.mts +1 -0
  68. package/dist/development/core/shopify-scripts/index.mjs +14 -12
  69. package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
  70. package/dist/development/core/shopify-scripts/types.d.mts +5 -0
  71. package/dist/development/core/url.mjs +2 -1
  72. package/dist/development/core/url.mjs.map +1 -1
  73. package/dist/development/globals.d.mts +4 -1
  74. package/dist/development/react/index.d.mts +2 -2
  75. package/dist/development/vue/shopify-scripts.d.mts +9 -0
  76. package/dist/development/vue/shopify-scripts.mjs +5 -0
  77. package/dist/development/vue/shopify-scripts.mjs.map +1 -1
  78. package/dist/globals.d.mts +4 -1
  79. package/dist/react/index.d.mts +2 -2
  80. package/dist/vue/shopify-scripts.d.mts +10 -1
  81. package/dist/vue/shopify-scripts.mjs +5 -0
  82. package/dist/vue/shopify-scripts.mjs.map +1 -1
  83. package/package.json +1 -1
  84. package/skills/hydrogen-cart-metafields/SKILL.md +133 -0
  85. package/skills/hydrogen-cart-ui/SKILL.md +2 -1
  86. package/skills/hydrogen-setup/steps/11-verify.md +1 -0
  87. package/skills/hydrogen-setup/steps/2-scaffold.md +21 -0
  88. package/skills/hydrogen-storefront-client/SKILL.md +4 -0
  89. package/skills/hydrogen-variant-form/SKILL.md +10 -4
  90. package/dist/core/shopify-scripts/consent.mjs +0 -17
  91. package/dist/core/shopify-scripts/consent.mjs.map +0 -1
  92. package/dist/development/core/shopify-scripts/consent.mjs +0 -17
  93. package/dist/development/core/shopify-scripts/consent.mjs.map +0 -1
@@ -29,11 +29,22 @@ interface ProductAddToCartProps {
29
29
  name: "add-to-cart";
30
30
  type: "submit";
31
31
  }
32
+ type AttributeValueName = `attributes.${string}`;
33
+ /** Props returned by {@link ProductFormRegister} for a line-item attribute input. */
34
+ interface ProductAttributeValueProps {
35
+ name: AttributeValueName;
36
+ value: string;
37
+ }
38
+ /** Props returned by {@link ProductFormRegister} for an uncontrolled line-item attribute input. */
39
+ interface ProductAttributeDefaultValueProps {
40
+ name: AttributeValueName;
41
+ defaultValue: string;
42
+ }
32
43
  /**
33
44
  * Register function returned by `useProductForm`.
34
45
  *
35
46
  * Product forms only register product-relevant fields:
36
- * `merchandiseId`, `quantity`, `optionValue`, and `addToCart`.
47
+ * `merchandiseId`, `quantity`, `optionValue`, `attributeValue`, and `addToCart`.
37
48
  *
38
49
  * Option values return form identity and activation handlers. UI props such as
39
50
  * `type`, `checked`, `disabled`, and `aria-pressed` belong to the caller — use
@@ -51,6 +62,14 @@ type ProductFormRegister = {
51
62
  optionName: string;
52
63
  value: string;
53
64
  }): ProductOptionValueProps;
65
+ (field: "attributeValue", opts: {
66
+ key: string;
67
+ value: string;
68
+ }): ProductAttributeValueProps;
69
+ (field: "attributeValue", opts: {
70
+ key: string;
71
+ defaultValue: string;
72
+ }): ProductAttributeDefaultValueProps;
54
73
  (field: "addToCart", opts: {}): ProductAddToCartProps;
55
74
  };
56
75
  /**
@@ -59,5 +78,5 @@ type ProductFormRegister = {
59
78
  */
60
79
  declare function createProductFormRegister(selectedVariant: ProductVariantInput | null, selectOption: (name: string, value: string) => void): ProductFormRegister;
61
80
  //#endregion
62
- export { ProductAddToCartProps, ProductFormRegister, ProductMerchandiseIdProps, ProductOptionValueProps, ProductQuantityDefaultProps, ProductQuantityProps, createProductFormRegister };
81
+ export { ProductAddToCartProps, ProductAttributeDefaultValueProps, ProductAttributeValueProps, ProductFormRegister, ProductMerchandiseIdProps, ProductOptionValueProps, ProductQuantityDefaultProps, ProductQuantityProps, createProductFormRegister };
63
82
  //# sourceMappingURL=form.d.mts.map
@@ -31,6 +31,19 @@ function createProductFormRegister(selectedVariant, selectOption) {
31
31
  onClick: handleSelect
32
32
  };
33
33
  }
34
+ if (field === "attributeValue") {
35
+ const key = String(opts?.key ?? "");
36
+ if (!key) throw new TypeError("Product attribute values require a non-empty \"key\".");
37
+ const name = `attributes.${key}`;
38
+ if (opts && "defaultValue" in opts) return {
39
+ name,
40
+ defaultValue: String(opts.defaultValue)
41
+ };
42
+ return {
43
+ name,
44
+ value: String(opts?.value ?? "")
45
+ };
46
+ }
34
47
  if (field === "addToCart") return {
35
48
  name: "add-to-cart",
36
49
  type: "submit"
@@ -1 +1 @@
1
- {"version":3,"file":"form.mjs","names":[],"sources":["../../../src/core/product/form.ts"],"sourcesContent":["import type { ProductVariantInput } from \"./state\";\n\n/** Props returned by {@link ProductFormRegister} for the `merchandiseId` input. */\nexport interface ProductMerchandiseIdProps {\n name: \"merchandiseId\";\n /** The currently selected variant ID, or an empty string when no variant is resolved. */\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for a quantity input. */\nexport interface ProductQuantityProps {\n name: \"quantity\";\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for an uncontrolled quantity input. */\nexport interface ProductQuantityDefaultProps {\n name: \"quantity\";\n defaultValue: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for a variant option value control. */\nexport interface ProductOptionValueProps {\n name: string;\n value: string;\n onChange: () => void;\n onClick: () => void;\n}\n\n/** Props returned by {@link ProductFormRegister} for the add-to-cart submit button. */\nexport interface ProductAddToCartProps {\n name: \"add-to-cart\";\n type: \"submit\";\n}\n\n/**\n * Register function returned by `useProductForm`.\n *\n * Product forms only register product-relevant fields:\n * `merchandiseId`, `quantity`, `optionValue`, and `addToCart`.\n *\n * Option values return form identity and activation handlers. UI props such as\n * `type`, `checked`, `disabled`, and `aria-pressed` belong to the caller — use\n * the state available on `options` (e.g. `value.selected`, `value.available`).\n */\nexport type ProductFormRegister = {\n (field: \"merchandiseId\", opts: {}): ProductMerchandiseIdProps;\n (field: \"quantity\", opts: { value: number }): ProductQuantityProps;\n (field: \"quantity\", opts: { defaultValue: number }): ProductQuantityDefaultProps;\n (field: \"optionValue\", opts: { optionName: string; value: string }): ProductOptionValueProps;\n (field: \"addToCart\", opts: {}): ProductAddToCartProps;\n};\n\n/**\n * Creates a {@link ProductFormRegister} function bound to the current variant\n * state and `selectOption` dispatcher.\n */\nexport function createProductFormRegister(\n selectedVariant: ProductVariantInput | null,\n selectOption: (name: string, value: string) => void,\n): ProductFormRegister {\n return ((field: string, opts?: Record<string, unknown>) => {\n if (field === \"merchandiseId\") {\n return { name: \"merchandiseId\", value: selectedVariant?.id ?? \"\" };\n }\n\n if (field === \"quantity\") {\n if (typeof opts?.defaultValue === \"number\") {\n return { name: \"quantity\" as const, defaultValue: String(opts.defaultValue) };\n }\n const value = typeof opts?.value === \"number\" ? opts.value : 1;\n return { name: \"quantity\" as const, value: String(value) };\n }\n\n if (field === \"optionValue\") {\n const optionName = String(opts?.optionName ?? \"\");\n const value = String(opts?.value ?? \"\");\n const handleSelect = () => selectOption(optionName, value);\n return { name: optionName, value, onChange: handleSelect, onClick: handleSelect };\n }\n\n if (field === \"addToCart\") {\n return { name: \"add-to-cart\", type: \"submit\" } satisfies ProductAddToCartProps;\n }\n\n throw new Error(`Unknown product form field: \"${field}\".`);\n }) as ProductFormRegister;\n}\n"],"mappings":";;;;;AAyDA,SAAgB,0BACd,iBACA,cACqB;AACrB,UAAS,OAAe,SAAmC;AACzD,MAAI,UAAU,gBACZ,QAAO;GAAE,MAAM;GAAiB,OAAO,iBAAiB,MAAM;GAAI;AAGpE,MAAI,UAAU,YAAY;AACxB,OAAI,OAAO,MAAM,iBAAiB,SAChC,QAAO;IAAE,MAAM;IAAqB,cAAc,OAAO,KAAK,aAAa;IAAE;GAE/E,MAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAC7D,UAAO;IAAE,MAAM;IAAqB,OAAO,OAAO,MAAM;IAAE;;AAG5D,MAAI,UAAU,eAAe;GAC3B,MAAM,aAAa,OAAO,MAAM,cAAc,GAAG;GACjD,MAAM,QAAQ,OAAO,MAAM,SAAS,GAAG;GACvC,MAAM,qBAAqB,aAAa,YAAY,MAAM;AAC1D,UAAO;IAAE,MAAM;IAAY;IAAO,UAAU;IAAc,SAAS;IAAc;;AAGnF,MAAI,UAAU,YACZ,QAAO;GAAE,MAAM;GAAe,MAAM;GAAU;AAGhD,QAAM,IAAI,MAAM,gCAAgC,MAAM,IAAI"}
1
+ {"version":3,"file":"form.mjs","names":[],"sources":["../../../src/core/product/form.ts"],"sourcesContent":["import type { ProductVariantInput } from \"./state\";\n\n/** Props returned by {@link ProductFormRegister} for the `merchandiseId` input. */\nexport interface ProductMerchandiseIdProps {\n name: \"merchandiseId\";\n /** The currently selected variant ID, or an empty string when no variant is resolved. */\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for a quantity input. */\nexport interface ProductQuantityProps {\n name: \"quantity\";\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for an uncontrolled quantity input. */\nexport interface ProductQuantityDefaultProps {\n name: \"quantity\";\n defaultValue: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for a variant option value control. */\nexport interface ProductOptionValueProps {\n name: string;\n value: string;\n onChange: () => void;\n onClick: () => void;\n}\n\n/** Props returned by {@link ProductFormRegister} for the add-to-cart submit button. */\nexport interface ProductAddToCartProps {\n name: \"add-to-cart\";\n type: \"submit\";\n}\n\ntype AttributeValueName = `attributes.${string}`;\n\n/** Props returned by {@link ProductFormRegister} for a line-item attribute input. */\nexport interface ProductAttributeValueProps {\n name: AttributeValueName;\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for an uncontrolled line-item attribute input. */\nexport interface ProductAttributeDefaultValueProps {\n name: AttributeValueName;\n defaultValue: string;\n}\n\n/**\n * Register function returned by `useProductForm`.\n *\n * Product forms only register product-relevant fields:\n * `merchandiseId`, `quantity`, `optionValue`, `attributeValue`, and `addToCart`.\n *\n * Option values return form identity and activation handlers. UI props such as\n * `type`, `checked`, `disabled`, and `aria-pressed` belong to the caller — use\n * the state available on `options` (e.g. `value.selected`, `value.available`).\n */\nexport type ProductFormRegister = {\n (field: \"merchandiseId\", opts: {}): ProductMerchandiseIdProps;\n (field: \"quantity\", opts: { value: number }): ProductQuantityProps;\n (field: \"quantity\", opts: { defaultValue: number }): ProductQuantityDefaultProps;\n (field: \"optionValue\", opts: { optionName: string; value: string }): ProductOptionValueProps;\n (field: \"attributeValue\", opts: { key: string; value: string }): ProductAttributeValueProps;\n (\n field: \"attributeValue\",\n opts: { key: string; defaultValue: string },\n ): ProductAttributeDefaultValueProps;\n (field: \"addToCart\", opts: {}): ProductAddToCartProps;\n};\n\n/**\n * Creates a {@link ProductFormRegister} function bound to the current variant\n * state and `selectOption` dispatcher.\n */\nexport function createProductFormRegister(\n selectedVariant: ProductVariantInput | null,\n selectOption: (name: string, value: string) => void,\n): ProductFormRegister {\n return ((field: string, opts?: Record<string, unknown>) => {\n if (field === \"merchandiseId\") {\n return { name: \"merchandiseId\", value: selectedVariant?.id ?? \"\" };\n }\n\n if (field === \"quantity\") {\n if (typeof opts?.defaultValue === \"number\") {\n return { name: \"quantity\" as const, defaultValue: String(opts.defaultValue) };\n }\n const value = typeof opts?.value === \"number\" ? opts.value : 1;\n return { name: \"quantity\" as const, value: String(value) };\n }\n\n if (field === \"optionValue\") {\n const optionName = String(opts?.optionName ?? \"\");\n const value = String(opts?.value ?? \"\");\n const handleSelect = () => selectOption(optionName, value);\n return { name: optionName, value, onChange: handleSelect, onClick: handleSelect };\n }\n\n if (field === \"attributeValue\") {\n const key = String(opts?.key ?? \"\");\n if (!key) throw new TypeError('Product attribute values require a non-empty \"key\".');\n const name = `attributes.${key}` as AttributeValueName;\n if (opts && \"defaultValue\" in opts) {\n return { name, defaultValue: String(opts.defaultValue) };\n }\n return { name, value: String(opts?.value ?? \"\") };\n }\n\n if (field === \"addToCart\") {\n return { name: \"add-to-cart\", type: \"submit\" } satisfies ProductAddToCartProps;\n }\n\n throw new Error(`Unknown product form field: \"${field}\".`);\n }) as ProductFormRegister;\n}\n"],"mappings":";;;;;AA4EA,SAAgB,0BACd,iBACA,cACqB;AACrB,UAAS,OAAe,SAAmC;AACzD,MAAI,UAAU,gBACZ,QAAO;GAAE,MAAM;GAAiB,OAAO,iBAAiB,MAAM;GAAI;AAGpE,MAAI,UAAU,YAAY;AACxB,OAAI,OAAO,MAAM,iBAAiB,SAChC,QAAO;IAAE,MAAM;IAAqB,cAAc,OAAO,KAAK,aAAa;IAAE;GAE/E,MAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAC7D,UAAO;IAAE,MAAM;IAAqB,OAAO,OAAO,MAAM;IAAE;;AAG5D,MAAI,UAAU,eAAe;GAC3B,MAAM,aAAa,OAAO,MAAM,cAAc,GAAG;GACjD,MAAM,QAAQ,OAAO,MAAM,SAAS,GAAG;GACvC,MAAM,qBAAqB,aAAa,YAAY,MAAM;AAC1D,UAAO;IAAE,MAAM;IAAY;IAAO,UAAU;IAAc,SAAS;IAAc;;AAGnF,MAAI,UAAU,kBAAkB;GAC9B,MAAM,MAAM,OAAO,MAAM,OAAO,GAAG;AACnC,OAAI,CAAC,IAAK,OAAM,IAAI,UAAU,wDAAsD;GACpF,MAAM,OAAO,cAAc;AAC3B,OAAI,QAAQ,kBAAkB,KAC5B,QAAO;IAAE;IAAM,cAAc,OAAO,KAAK,aAAa;IAAE;AAE1D,UAAO;IAAE;IAAM,OAAO,OAAO,MAAM,SAAS,GAAG;IAAE;;AAGnD,MAAI,UAAU,YACZ,QAAO;GAAE,MAAM;GAAe,MAAM;GAAU;AAGhD,QAAM,IAAI,MAAM,gCAAgC,MAAM,IAAI"}
@@ -2,4 +2,4 @@ import { ProductInput, ProductOptionInput, ProductOptionValueInput, ProductVaria
2
2
  import { CreateProductFormStoreOptions, ProductFormErrors, ProductFormOptions, ProductFormStore, ProductFormStoreState, ValidProductSelectionResult, VariantSelectionResult, canAddToCart, createProductFormStore, findCartLineByMerchandiseId, getSelectedVariant } from "./product-form.mjs";
3
3
  import { getSelectedProductOptions } from "./options.mjs";
4
4
  import { ProductSelectionLinkStyle, buildProductSelectionSearchParams } from "./url.mjs";
5
- import { ProductAddToCartProps, ProductFormRegister, ProductMerchandiseIdProps, ProductOptionValueProps, ProductQuantityDefaultProps, ProductQuantityProps, createProductFormRegister } from "./form.mjs";
5
+ import { ProductAddToCartProps, ProductAttributeDefaultValueProps, ProductAttributeValueProps, ProductFormRegister, ProductMerchandiseIdProps, ProductOptionValueProps, ProductQuantityDefaultProps, ProductQuantityProps, createProductFormRegister } from "./form.mjs";
@@ -58,6 +58,12 @@ type ShopifyRequestContextBase = {
58
58
  * @internal
59
59
  */
60
60
  markResponseAsPersonalized(reason: string): void;
61
+ /**
62
+ * Mark this request as a sanctioned session-establishing endpoint (like the
63
+ * consent flow), permitting Shopify state to return even on a cold session.
64
+ * @internal
65
+ */
66
+ markResponseAsSessionEstablishing(reason: string): void;
61
67
  i18n: NormalizedI18nConfig; /** Return incoming request headers plus request lifecycle headers for proxy/origin handoff. */
62
68
  getForwardedRequestHeaders(): Headers; /** Apply important response headers for the correct functioning of Hydrogen storefronts. */
63
69
  applyResponseHeaders(headers: Headers): void;
@@ -31,6 +31,7 @@ function createShopifyRequestContext(input) {
31
31
  };
32
32
  let capturedSubrequestHeaders;
33
33
  let personalizedResponseReason;
34
+ let sessionEstablishingReason;
34
35
  const captureSubrequestHeaders = (headers) => {
35
36
  capturedSubrequestHeaders ??= {
36
37
  serverTiming: headers.get("Server-Timing") ?? "",
@@ -66,10 +67,13 @@ function createShopifyRequestContext(input) {
66
67
  markResponseAsPersonalized(reason) {
67
68
  personalizedResponseReason ??= reason;
68
69
  },
70
+ markResponseAsSessionEstablishing(reason) {
71
+ sessionEstablishingReason ??= reason;
72
+ },
69
73
  applyResponseHeaders(headers) {
70
74
  headers.set("powered-by", "Shopify, Hydrogen");
71
75
  const isDocumentResponse = context.documentRequest || (headers.get("content-type")?.startsWith("text/html") ?? false);
72
- const mayReturnShopifyState = requestMethod !== void 0 && requestMethod !== "GET" && requestMethod !== "HEAD" && !isDocumentResponse && (hasEssentialCookie || isConsentManagementRequest);
76
+ const mayReturnShopifyState = requestMethod !== void 0 && requestMethod !== "GET" && requestMethod !== "HEAD" && !isDocumentResponse && (hasEssentialCookie || isConsentManagementRequest || sessionEstablishingReason !== void 0);
73
77
  if (capturedSubrequestHeaders && mayReturnShopifyState) {
74
78
  const existingSetCookies = new Set(headers.getSetCookie());
75
79
  for (const value of capturedSubrequestHeaders.setCookie) {
@@ -96,7 +100,7 @@ function applyStorefrontRequestHeaders(context, headers) {
96
100
  headers.set(SDK_VARIANT_HEADER, "hydrogen");
97
101
  headers.set(SDK_VARIANT_SOURCE_HEADER, "kit");
98
102
  headers.set(SDK_VERSION_HEADER, STOREFRONT_API_VERSION);
99
- headers.set(HYDROGEN_VERSION_HEADER, "2026.10.0-preview.2");
103
+ headers.set(HYDROGEN_VERSION_HEADER, "2026.10.0-preview.3");
100
104
  headers.set(REQUEST_GROUP_ID_HEADER, context.requestGroupId);
101
105
  if (context.cookie) headers.set("cookie", context.cookie);
102
106
  else headers.delete("cookie");
@@ -1 +1 @@
1
- {"version":3,"file":"request-context.mjs","names":[],"sources":["../../src/core/request-context.ts"],"sourcesContent":["import type {\n CountryCode as CustomerAccountCountryCode,\n LanguageCode as CustomerAccountLanguageCode,\n} from \"../graphql/generated/customer-account-api-types\";\nimport type {\n CountryCode as StorefrontCountryCode,\n LanguageCode as StorefrontLanguageCode,\n} from \"../graphql/generated/storefront-api-types\";\nimport { STOREFRONT_API_VERSION } from \"./constants\";\nimport {\n applyPrivateResponseCacheHeaders,\n CONSENT_MANAGEMENT_HEADER,\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SERVER_TIMING_HEADER,\n SHOPIFY_STOREFRONT_ORIGIN_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n STOREFRONT_URL_HEADER,\n} from \"./headers\";\nimport { normalizePathPrefix } from \"./standard-routes/path\";\n\nconst SHOPIFY_ESSENTIAL_COOKIE = \"_shopify_essential\";\nconst SHOPIFY_TRACKING_COOKIES = [\"_shopify_analytics\", \"_shopify_marketing\"];\nconst SHOPIFY_COOKIES = new Set([SHOPIFY_ESSENTIAL_COOKIE, ...SHOPIFY_TRACKING_COOKIES]);\n\ntype StorefrontRequest = Pick<Request, \"headers\"> &\n Partial<Pick<Request, \"method\" | \"signal\" | \"url\">>;\n\nexport type ShopifyLanguageCode = Extract<StorefrontLanguageCode, CustomerAccountLanguageCode>;\nexport type ShopifyCountryCode = Extract<StorefrontCountryCode, CustomerAccountCountryCode>;\n\nexport type I18nConfig = {\n language: ShopifyLanguageCode;\n country: ShopifyCountryCode;\n /** Optional app route prefix for localized paths, for example \"/es-es\". */\n pathPrefix?: string;\n};\n\ntype NormalizedI18nConfig<I18n extends I18nConfig = I18nConfig> = Omit<I18n, \"pathPrefix\"> & {\n pathPrefix: string;\n};\n\ntype ShopifyRequestContextInputBase<I18n extends I18nConfig = I18nConfig> = {\n request: StorefrontRequest;\n i18n: I18n;\n};\n\ntype ShopifyRequestContextInput<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextInputBase<I18n> & { buyerIp?: never };\n\ntype ShopifyRequestContextWithBuyerIpInput<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextInputBase<I18n> & { buyerIp: string };\n\ntype ShopifyRequestContextBase = {\n // -- Private fields --\n /**\n * Compile-time brand so callers use createShopifyRequestContext().\n * @internal\n */\n readonly __hydrogenShopifyRequestContextBrand: never;\n /** @internal */\n cookie?: string;\n /** @internal */\n uniqueToken?: string;\n /** @internal */\n visitToken?: string;\n /** @internal */\n legacyTokens?: boolean;\n /** @internal */\n readonly buyerIp?: string;\n /** @internal */\n requestGroupId: string;\n /** @internal */\n signal?: AbortSignal;\n /** @internal */\n url?: string;\n /** @internal */\n storefrontOrigin?: string;\n /**\n * Apply request-scoped headers required by every Shopify storefront subrequest.\n * @internal\n */\n applyStorefrontRequestHeaders(headers: Headers): void;\n /**\n * Capture the first fresh storefront response headers for replay onto the final app response.\n * @internal\n */\n captureSubrequestHeaders(headers: Headers): void;\n /**\n * Consume storefront proxy response state for gated replay onto the final app response.\n * @internal\n */\n consumeStorefrontResponseHeaders(headers: Headers): void;\n /**\n * Mark the final app response as influenced by private customer state.\n * @internal\n */\n markResponseAsPersonalized(reason: string): void;\n\n // -- Public fields --\n i18n: NormalizedI18nConfig;\n /** Return incoming request headers plus request lifecycle headers for proxy/origin handoff. */\n getForwardedRequestHeaders(): Headers;\n /** Apply important response headers for the correct functioning of Hydrogen storefronts. */\n applyResponseHeaders(headers: Headers): void;\n};\n\nexport type ShopifyRequestContext<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextBase & {\n i18n: NormalizedI18nConfig<I18n>;\n };\n\nexport type ShopifyRequestContextWithBuyerIp<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContext<I18n> & { readonly buyerIp: string };\n\ntype Context<I18n extends I18nConfig = I18nConfig> = {\n cookie?: string;\n uniqueToken?: string;\n visitToken?: string;\n legacyTokens?: boolean;\n buyerIp?: string;\n requestGroupId: string;\n signal?: AbortSignal;\n url?: string;\n storefrontOrigin?: string;\n i18n: NormalizedI18nConfig<I18n>;\n documentRequest?: boolean;\n};\n\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextWithBuyerIpInput<I18n>,\n): ShopifyRequestContextWithBuyerIp<I18n>;\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextInput<I18n>,\n): ShopifyRequestContext<I18n>;\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextInputBase<I18n> & { buyerIp?: string },\n): ShopifyRequestContext<I18n> {\n const { request } = input;\n\n if (!input.i18n?.country || !input.i18n?.language) {\n throw new Error(\"i18n with country and language is required for Shopify request contexts.\");\n }\n\n if (input.buyerIp !== undefined && !input.buyerIp) {\n throw new Error(\"buyerIp must be non-empty when provided\");\n }\n\n const requestMethod = request.method?.toUpperCase();\n const i18n = normalizeI18n(input.i18n);\n const cookieHeader = request.headers.get(\"cookie\") || undefined;\n const inboundCookies = parseCookieHeader(cookieHeader);\n const hasEssentialCookie = inboundCookies.has(SHOPIFY_ESSENTIAL_COOKIE);\n const hasTrackingCookie = SHOPIFY_TRACKING_COOKIES.some((name) => inboundCookies.has(name));\n const isConsentManagementRequest = request.headers.get(CONSENT_MANAGEMENT_HEADER) === \"1\";\n const url = request.url ?? request.headers.get(STOREFRONT_URL_HEADER) ?? undefined;\n const storefrontOrigin = getUrlOrigin(url);\n const context = {\n ...(cookieHeader && { cookie: cookieHeader }),\n i18n,\n ...(url && { url }),\n ...(storefrontOrigin && { storefrontOrigin }),\n ...(input.buyerIp && { buyerIp: input.buyerIp }),\n requestGroupId:\n request.headers.get(REQUEST_GROUP_ID_HEADER) ??\n request.headers.get(\"x-request-id\") ??\n request.headers.get(\"request-id\") ??\n crypto.randomUUID(),\n ...(isDocumentRequest(request) && { documentRequest: true }),\n ...(request.signal && { signal: request.signal }),\n } as Context<I18n>;\n\n let capturedSubrequestHeaders:\n | {\n serverTiming: string;\n setCookie: string[];\n }\n | undefined;\n let personalizedResponseReason: string | undefined;\n\n const captureSubrequestHeaders = (headers: Headers): void => {\n // Capture this the first time we get a fresh response to increase the\n // chance of returning it from the main server response. The main response\n // needs headers set at send time, while the body can stream later, so this\n // may not be used if subrequests finish after the main response is sent.\n capturedSubrequestHeaders ??= {\n serverTiming: headers.get(SERVER_TIMING_HEADER) ?? \"\",\n setCookie: headers.getSetCookie(),\n };\n };\n\n if (!hasTrackingCookie) {\n const legacyUniqueToken = inboundCookies.get(\"_shopify_y\");\n const legacyVisitToken = inboundCookies.get(\"_shopify_s\");\n const headerUniqueToken = request.headers.get(SHOPIFY_UNIQUE_TOKEN_HEADER) ?? undefined;\n const headerVisitToken = request.headers.get(SHOPIFY_VISIT_TOKEN_HEADER) ?? undefined;\n\n if (legacyUniqueToken || legacyVisitToken) context.legacyTokens = true;\n\n context.uniqueToken = legacyUniqueToken ?? headerUniqueToken;\n context.visitToken = legacyVisitToken ?? headerVisitToken;\n }\n\n return {\n ...context,\n getForwardedRequestHeaders() {\n const headers = new Headers(request.headers);\n applyStorefrontRequestHeaders(context, headers);\n if (context.url) headers.set(STOREFRONT_URL_HEADER, context.url);\n return headers;\n },\n applyStorefrontRequestHeaders(headers) {\n applyStorefrontRequestHeaders(context, headers);\n },\n captureSubrequestHeaders,\n consumeStorefrontResponseHeaders(headers) {\n if (headers.has(SERVER_TIMING_HEADER) || headers.getSetCookie().length > 0) {\n captureSubrequestHeaders(headers);\n }\n headers.delete(\"set-cookie\");\n headers.delete(SERVER_TIMING_HEADER);\n },\n markResponseAsPersonalized(reason) {\n personalizedResponseReason ??= reason;\n },\n applyResponseHeaders(headers) {\n headers.set(\"powered-by\", \"Shopify, Hydrogen\");\n\n // Documents may be shared or streamed, so they must not carry buyer-specific state.\n const isDocumentResponse =\n context.documentRequest || (headers.get(\"content-type\")?.startsWith(\"text/html\") ?? false);\n\n // Keep GET and HEAD responses cacheable, and fail closed when the request method is unknown.\n // Cold sessions may only be established by the explicitly marked consent request.\n const mayReturnShopifyState =\n requestMethod !== undefined &&\n requestMethod !== \"GET\" &&\n requestMethod !== \"HEAD\" &&\n !isDocumentResponse &&\n (hasEssentialCookie || isConsentManagementRequest);\n\n // Replay state captured from fresh SFAPI and proxy responses when allowed.\n if (capturedSubrequestHeaders && mayReturnShopifyState) {\n const existingSetCookies = new Set(headers.getSetCookie());\n for (const value of capturedSubrequestHeaders.setCookie) {\n if (existingSetCookies.has(value)) continue;\n headers.append(\"set-cookie\", value);\n existingSetCookies.add(value);\n }\n\n const capturedServerTiming = capturedSubrequestHeaders.serverTiming;\n const existingServerTiming = headers.get(SERVER_TIMING_HEADER) ?? \"\";\n const shouldAppendServerTiming =\n capturedServerTiming !== \"\" &&\n existingServerTiming !== capturedServerTiming &&\n !existingServerTiming.startsWith(`${capturedServerTiming}, `) &&\n !existingServerTiming.endsWith(`, ${capturedServerTiming}`);\n if (shouldAppendServerTiming) {\n headers.set(\n SERVER_TIMING_HEADER,\n existingServerTiming\n ? `${existingServerTiming}, ${capturedServerTiming}`\n : capturedServerTiming,\n );\n }\n }\n\n // Responses containing buyer-specific or replayed state must not enter shared caches.\n const returnsCapturedState =\n mayReturnShopifyState &&\n Boolean(\n capturedSubrequestHeaders?.serverTiming || capturedSubrequestHeaders?.setCookie.length,\n );\n\n if (\n personalizedResponseReason ||\n returnsCapturedState ||\n headers.getSetCookie().some(isShopifySetCookie)\n ) {\n applyPrivateResponseCacheHeaders(headers);\n }\n },\n } as ShopifyRequestContext<I18n>;\n}\n\nfunction normalizeI18n<I18n extends I18nConfig>(i18n: I18n): NormalizedI18nConfig<I18n> {\n return {\n ...i18n,\n pathPrefix: normalizePathPrefix(i18n.pathPrefix),\n } as NormalizedI18nConfig<I18n>;\n}\n\nfunction applyStorefrontRequestHeaders(context: Context, headers: Headers): void {\n headers.set(SDK_VARIANT_HEADER, \"hydrogen\");\n headers.set(SDK_VARIANT_SOURCE_HEADER, \"kit\");\n headers.set(SDK_VERSION_HEADER, STOREFRONT_API_VERSION);\n headers.set(HYDROGEN_VERSION_HEADER, __HYDROGEN_VERSION__);\n headers.set(REQUEST_GROUP_ID_HEADER, context.requestGroupId);\n\n if (context.cookie) headers.set(\"cookie\", context.cookie);\n else headers.delete(\"cookie\");\n if (context.storefrontOrigin) {\n headers.set(SHOPIFY_STOREFRONT_ORIGIN_HEADER, context.storefrontOrigin);\n } else headers.delete(SHOPIFY_STOREFRONT_ORIGIN_HEADER);\n\n // Some Storefront API consumers still rely on these headers instead of cookies.\n if (context.uniqueToken) headers.set(SHOPIFY_UNIQUE_TOKEN_HEADER, context.uniqueToken);\n if (context.visitToken) headers.set(SHOPIFY_VISIT_TOKEN_HEADER, context.visitToken);\n if (context.legacyTokens && context.uniqueToken) {\n headers.set(SHOPIFY_STOREFRONT_Y_HEADER, context.uniqueToken);\n }\n if (context.legacyTokens && context.visitToken) {\n headers.set(SHOPIFY_STOREFRONT_S_HEADER, context.visitToken);\n }\n}\n\nfunction getUrlOrigin(url: string | undefined): string | undefined {\n if (!url) return undefined;\n try {\n return new URL(url).origin;\n } catch {\n return undefined;\n }\n}\n\nfunction isDocumentRequest(request: StorefrontRequest): boolean {\n if (request.method && request.method !== \"GET\" && request.method !== \"HEAD\") return false;\n\n const destination = request.headers.get(\"sec-fetch-dest\");\n if (destination === \"document\") return true;\n\n return request.headers.get(\"accept\")?.includes(\"text/html\") ?? false;\n}\n\nfunction parseCookieHeader(cookieHeader: string | undefined): Map<string, string> {\n const cookies = new Map<string, string>();\n if (!cookieHeader) return cookies;\n\n for (const cookie of cookieHeader.split(\";\")) {\n const separator = cookie.indexOf(\"=\");\n if (separator < 0) continue;\n\n const name = cookie.slice(0, separator).trim();\n if (!cookies.has(name)) cookies.set(name, cookie.slice(separator + 1));\n }\n\n return cookies;\n}\n\nfunction isShopifySetCookie(value: string): boolean {\n const name = value.match(/^\\s*([^=;\\s]+)\\s*=/)?.[1];\n return name !== undefined && SHOPIFY_COOKIES.has(name);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"request-context.mjs","names":[],"sources":["../../src/core/request-context.ts"],"sourcesContent":["import type {\n CountryCode as CustomerAccountCountryCode,\n LanguageCode as CustomerAccountLanguageCode,\n} from \"../graphql/generated/customer-account-api-types\";\nimport type {\n CountryCode as StorefrontCountryCode,\n LanguageCode as StorefrontLanguageCode,\n} from \"../graphql/generated/storefront-api-types\";\nimport { STOREFRONT_API_VERSION } from \"./constants\";\nimport {\n applyPrivateResponseCacheHeaders,\n CONSENT_MANAGEMENT_HEADER,\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SERVER_TIMING_HEADER,\n SHOPIFY_STOREFRONT_ORIGIN_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n STOREFRONT_URL_HEADER,\n} from \"./headers\";\nimport { normalizePathPrefix } from \"./standard-routes/path\";\n\nconst SHOPIFY_ESSENTIAL_COOKIE = \"_shopify_essential\";\nconst SHOPIFY_TRACKING_COOKIES = [\"_shopify_analytics\", \"_shopify_marketing\"];\nconst SHOPIFY_COOKIES = new Set([SHOPIFY_ESSENTIAL_COOKIE, ...SHOPIFY_TRACKING_COOKIES]);\n\ntype StorefrontRequest = Pick<Request, \"headers\"> &\n Partial<Pick<Request, \"method\" | \"signal\" | \"url\">>;\n\nexport type ShopifyLanguageCode = Extract<StorefrontLanguageCode, CustomerAccountLanguageCode>;\nexport type ShopifyCountryCode = Extract<StorefrontCountryCode, CustomerAccountCountryCode>;\n\nexport type I18nConfig = {\n language: ShopifyLanguageCode;\n country: ShopifyCountryCode;\n /** Optional app route prefix for localized paths, for example \"/es-es\". */\n pathPrefix?: string;\n};\n\ntype NormalizedI18nConfig<I18n extends I18nConfig = I18nConfig> = Omit<I18n, \"pathPrefix\"> & {\n pathPrefix: string;\n};\n\ntype ShopifyRequestContextInputBase<I18n extends I18nConfig = I18nConfig> = {\n request: StorefrontRequest;\n i18n: I18n;\n};\n\ntype ShopifyRequestContextInput<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextInputBase<I18n> & { buyerIp?: never };\n\ntype ShopifyRequestContextWithBuyerIpInput<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextInputBase<I18n> & { buyerIp: string };\n\ntype ShopifyRequestContextBase = {\n // -- Private fields --\n /**\n * Compile-time brand so callers use createShopifyRequestContext().\n * @internal\n */\n readonly __hydrogenShopifyRequestContextBrand: never;\n /** @internal */\n cookie?: string;\n /** @internal */\n uniqueToken?: string;\n /** @internal */\n visitToken?: string;\n /** @internal */\n legacyTokens?: boolean;\n /** @internal */\n readonly buyerIp?: string;\n /** @internal */\n requestGroupId: string;\n /** @internal */\n signal?: AbortSignal;\n /** @internal */\n url?: string;\n /** @internal */\n storefrontOrigin?: string;\n /**\n * Apply request-scoped headers required by every Shopify storefront subrequest.\n * @internal\n */\n applyStorefrontRequestHeaders(headers: Headers): void;\n /**\n * Capture the first fresh storefront response headers for replay onto the final app response.\n * @internal\n */\n captureSubrequestHeaders(headers: Headers): void;\n /**\n * Consume storefront proxy response state for gated replay onto the final app response.\n * @internal\n */\n consumeStorefrontResponseHeaders(headers: Headers): void;\n /**\n * Mark the final app response as influenced by private customer state.\n * @internal\n */\n markResponseAsPersonalized(reason: string): void;\n /**\n * Mark this request as a sanctioned session-establishing endpoint (like the\n * consent flow), permitting Shopify state to return even on a cold session.\n * @internal\n */\n markResponseAsSessionEstablishing(reason: string): void;\n\n // -- Public fields --\n i18n: NormalizedI18nConfig;\n /** Return incoming request headers plus request lifecycle headers for proxy/origin handoff. */\n getForwardedRequestHeaders(): Headers;\n /** Apply important response headers for the correct functioning of Hydrogen storefronts. */\n applyResponseHeaders(headers: Headers): void;\n};\n\nexport type ShopifyRequestContext<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextBase & {\n i18n: NormalizedI18nConfig<I18n>;\n };\n\nexport type ShopifyRequestContextWithBuyerIp<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContext<I18n> & { readonly buyerIp: string };\n\ntype Context<I18n extends I18nConfig = I18nConfig> = {\n cookie?: string;\n uniqueToken?: string;\n visitToken?: string;\n legacyTokens?: boolean;\n buyerIp?: string;\n requestGroupId: string;\n signal?: AbortSignal;\n url?: string;\n storefrontOrigin?: string;\n i18n: NormalizedI18nConfig<I18n>;\n documentRequest?: boolean;\n};\n\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextWithBuyerIpInput<I18n>,\n): ShopifyRequestContextWithBuyerIp<I18n>;\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextInput<I18n>,\n): ShopifyRequestContext<I18n>;\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextInputBase<I18n> & { buyerIp?: string },\n): ShopifyRequestContext<I18n> {\n const { request } = input;\n\n if (!input.i18n?.country || !input.i18n?.language) {\n throw new Error(\"i18n with country and language is required for Shopify request contexts.\");\n }\n\n if (input.buyerIp !== undefined && !input.buyerIp) {\n throw new Error(\"buyerIp must be non-empty when provided\");\n }\n\n const requestMethod = request.method?.toUpperCase();\n const i18n = normalizeI18n(input.i18n);\n const cookieHeader = request.headers.get(\"cookie\") || undefined;\n const inboundCookies = parseCookieHeader(cookieHeader);\n const hasEssentialCookie = inboundCookies.has(SHOPIFY_ESSENTIAL_COOKIE);\n const hasTrackingCookie = SHOPIFY_TRACKING_COOKIES.some((name) => inboundCookies.has(name));\n const isConsentManagementRequest = request.headers.get(CONSENT_MANAGEMENT_HEADER) === \"1\";\n const url = request.url ?? request.headers.get(STOREFRONT_URL_HEADER) ?? undefined;\n const storefrontOrigin = getUrlOrigin(url);\n const context = {\n ...(cookieHeader && { cookie: cookieHeader }),\n i18n,\n ...(url && { url }),\n ...(storefrontOrigin && { storefrontOrigin }),\n ...(input.buyerIp && { buyerIp: input.buyerIp }),\n requestGroupId:\n request.headers.get(REQUEST_GROUP_ID_HEADER) ??\n request.headers.get(\"x-request-id\") ??\n request.headers.get(\"request-id\") ??\n crypto.randomUUID(),\n ...(isDocumentRequest(request) && { documentRequest: true }),\n ...(request.signal && { signal: request.signal }),\n } as Context<I18n>;\n\n let capturedSubrequestHeaders:\n | {\n serverTiming: string;\n setCookie: string[];\n }\n | undefined;\n let personalizedResponseReason: string | undefined;\n let sessionEstablishingReason: string | undefined;\n\n const captureSubrequestHeaders = (headers: Headers): void => {\n // Capture this the first time we get a fresh response to increase the\n // chance of returning it from the main server response. The main response\n // needs headers set at send time, while the body can stream later, so this\n // may not be used if subrequests finish after the main response is sent.\n capturedSubrequestHeaders ??= {\n serverTiming: headers.get(SERVER_TIMING_HEADER) ?? \"\",\n setCookie: headers.getSetCookie(),\n };\n };\n\n if (!hasTrackingCookie) {\n const legacyUniqueToken = inboundCookies.get(\"_shopify_y\");\n const legacyVisitToken = inboundCookies.get(\"_shopify_s\");\n const headerUniqueToken = request.headers.get(SHOPIFY_UNIQUE_TOKEN_HEADER) ?? undefined;\n const headerVisitToken = request.headers.get(SHOPIFY_VISIT_TOKEN_HEADER) ?? undefined;\n\n if (legacyUniqueToken || legacyVisitToken) context.legacyTokens = true;\n\n context.uniqueToken = legacyUniqueToken ?? headerUniqueToken;\n context.visitToken = legacyVisitToken ?? headerVisitToken;\n }\n\n return {\n ...context,\n getForwardedRequestHeaders() {\n const headers = new Headers(request.headers);\n applyStorefrontRequestHeaders(context, headers);\n if (context.url) headers.set(STOREFRONT_URL_HEADER, context.url);\n return headers;\n },\n applyStorefrontRequestHeaders(headers) {\n applyStorefrontRequestHeaders(context, headers);\n },\n captureSubrequestHeaders,\n consumeStorefrontResponseHeaders(headers) {\n if (headers.has(SERVER_TIMING_HEADER) || headers.getSetCookie().length > 0) {\n captureSubrequestHeaders(headers);\n }\n headers.delete(\"set-cookie\");\n headers.delete(SERVER_TIMING_HEADER);\n },\n markResponseAsPersonalized(reason) {\n personalizedResponseReason ??= reason;\n },\n markResponseAsSessionEstablishing(reason) {\n sessionEstablishingReason ??= reason;\n },\n applyResponseHeaders(headers) {\n headers.set(\"powered-by\", \"Shopify, Hydrogen\");\n\n // Documents may be shared or streamed, so they must not carry buyer-specific state.\n const isDocumentResponse =\n context.documentRequest || (headers.get(\"content-type\")?.startsWith(\"text/html\") ?? false);\n\n // Keep GET and HEAD responses cacheable, and fail closed when the request method is unknown.\n // Cold sessions may only be established by the explicitly marked consent request.\n const mayReturnShopifyState =\n requestMethod !== undefined &&\n requestMethod !== \"GET\" &&\n requestMethod !== \"HEAD\" &&\n !isDocumentResponse &&\n (hasEssentialCookie ||\n isConsentManagementRequest ||\n sessionEstablishingReason !== undefined);\n\n // Replay state captured from fresh SFAPI and proxy responses when allowed.\n if (capturedSubrequestHeaders && mayReturnShopifyState) {\n const existingSetCookies = new Set(headers.getSetCookie());\n for (const value of capturedSubrequestHeaders.setCookie) {\n if (existingSetCookies.has(value)) continue;\n headers.append(\"set-cookie\", value);\n existingSetCookies.add(value);\n }\n\n const capturedServerTiming = capturedSubrequestHeaders.serverTiming;\n const existingServerTiming = headers.get(SERVER_TIMING_HEADER) ?? \"\";\n const shouldAppendServerTiming =\n capturedServerTiming !== \"\" &&\n existingServerTiming !== capturedServerTiming &&\n !existingServerTiming.startsWith(`${capturedServerTiming}, `) &&\n !existingServerTiming.endsWith(`, ${capturedServerTiming}`);\n if (shouldAppendServerTiming) {\n headers.set(\n SERVER_TIMING_HEADER,\n existingServerTiming\n ? `${existingServerTiming}, ${capturedServerTiming}`\n : capturedServerTiming,\n );\n }\n }\n\n // Responses containing buyer-specific or replayed state must not enter shared caches.\n const returnsCapturedState =\n mayReturnShopifyState &&\n Boolean(\n capturedSubrequestHeaders?.serverTiming || capturedSubrequestHeaders?.setCookie.length,\n );\n\n if (\n personalizedResponseReason ||\n returnsCapturedState ||\n headers.getSetCookie().some(isShopifySetCookie)\n ) {\n applyPrivateResponseCacheHeaders(headers);\n }\n },\n } as ShopifyRequestContext<I18n>;\n}\n\nfunction normalizeI18n<I18n extends I18nConfig>(i18n: I18n): NormalizedI18nConfig<I18n> {\n return {\n ...i18n,\n pathPrefix: normalizePathPrefix(i18n.pathPrefix),\n } as NormalizedI18nConfig<I18n>;\n}\n\nfunction applyStorefrontRequestHeaders(context: Context, headers: Headers): void {\n headers.set(SDK_VARIANT_HEADER, \"hydrogen\");\n headers.set(SDK_VARIANT_SOURCE_HEADER, \"kit\");\n headers.set(SDK_VERSION_HEADER, STOREFRONT_API_VERSION);\n headers.set(HYDROGEN_VERSION_HEADER, __HYDROGEN_VERSION__);\n headers.set(REQUEST_GROUP_ID_HEADER, context.requestGroupId);\n\n if (context.cookie) headers.set(\"cookie\", context.cookie);\n else headers.delete(\"cookie\");\n if (context.storefrontOrigin) {\n headers.set(SHOPIFY_STOREFRONT_ORIGIN_HEADER, context.storefrontOrigin);\n } else headers.delete(SHOPIFY_STOREFRONT_ORIGIN_HEADER);\n\n // Some Storefront API consumers still rely on these headers instead of cookies.\n if (context.uniqueToken) headers.set(SHOPIFY_UNIQUE_TOKEN_HEADER, context.uniqueToken);\n if (context.visitToken) headers.set(SHOPIFY_VISIT_TOKEN_HEADER, context.visitToken);\n if (context.legacyTokens && context.uniqueToken) {\n headers.set(SHOPIFY_STOREFRONT_Y_HEADER, context.uniqueToken);\n }\n if (context.legacyTokens && context.visitToken) {\n headers.set(SHOPIFY_STOREFRONT_S_HEADER, context.visitToken);\n }\n}\n\nfunction getUrlOrigin(url: string | undefined): string | undefined {\n if (!url) return undefined;\n try {\n return new URL(url).origin;\n } catch {\n return undefined;\n }\n}\n\nfunction isDocumentRequest(request: StorefrontRequest): boolean {\n if (request.method && request.method !== \"GET\" && request.method !== \"HEAD\") return false;\n\n const destination = request.headers.get(\"sec-fetch-dest\");\n if (destination === \"document\") return true;\n\n return request.headers.get(\"accept\")?.includes(\"text/html\") ?? false;\n}\n\nfunction parseCookieHeader(cookieHeader: string | undefined): Map<string, string> {\n const cookies = new Map<string, string>();\n if (!cookieHeader) return cookies;\n\n for (const cookie of cookieHeader.split(\";\")) {\n const separator = cookie.indexOf(\"=\");\n if (separator < 0) continue;\n\n const name = cookie.slice(0, separator).trim();\n if (!cookies.has(name)) cookies.set(name, cookie.slice(separator + 1));\n }\n\n return cookies;\n}\n\nfunction isShopifySetCookie(value: string): boolean {\n const name = value.match(/^\\s*([^=;\\s]+)\\s*=/)?.[1];\n return name !== undefined && SHOPIFY_COOKIES.has(name);\n}\n"],"mappings":"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"}
@@ -5,6 +5,7 @@ import { handleShopifyApiProxy } from "./interceptors/api-proxy.mjs";
5
5
  import { handleCheckoutRedirect } from "./interceptors/checkout.mjs";
6
6
  import { handleMcpProxy } from "./interceptors/mcp-proxy.mjs";
7
7
  import { handleSfapiProxy } from "./interceptors/sfapi-proxy.mjs";
8
+ import { handleUcpMcpProxy } from "./interceptors/ucp-mcp-proxy.mjs";
8
9
  import { handleWellKnownProxy } from "./interceptors/well-known.mjs";
9
10
  import { handleShopifyRouteHandlers } from "./registered-routes.mjs";
10
11
 
@@ -16,6 +17,7 @@ const SHOPIFY_ROUTE_INTERCEPTORS = [
16
17
  handleShopifyRouteHandlers,
17
18
  handleCheckoutRedirect,
18
19
  handleWellKnownProxy,
20
+ handleUcpMcpProxy,
19
21
  handleMcpProxy,
20
22
  handleAjaxApi
21
23
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"handle-shopify-routes.mjs","names":[],"sources":["../../../src/core/request-routing/handle-shopify-routes.ts"],"sourcesContent":["import { handleProductVariantId } from \"../product/accept-variant-id\";\nimport { handleAjaxApi } from \"./interceptors/ajax-api\";\nimport { handleShopifyApiProxy } from \"./interceptors/api-proxy\";\nimport { handleCheckoutRedirect } from \"./interceptors/checkout\";\nimport { handleMcpProxy } from \"./interceptors/mcp-proxy\";\nimport { handleSfapiProxy } from \"./interceptors/sfapi-proxy\";\nimport { handleWellKnownProxy } from \"./interceptors/well-known\";\nimport { handleShopifyRouteHandlers } from \"./registered-routes\";\nimport type { HydrogenRouteHandler, HydrogenRouteInterceptor } from \"./route-types\";\nimport { safeApplyResponseHeaders } from \"./safe-apply-response-headers\";\n\nconst SHOPIFY_ROUTE_INTERCEPTORS = [\n handleShopifyApiProxy,\n handleSfapiProxy,\n handleProductVariantId,\n handleShopifyRouteHandlers,\n handleCheckoutRedirect,\n handleWellKnownProxy,\n handleMcpProxy,\n handleAjaxApi,\n] satisfies readonly HydrogenRouteInterceptor[];\n\n/**\n * Matches a request against Shopify standard routes and any registered handler\n * groups, returning a raw `Response` (redirect or JSON) when one matches, or\n * `null` when none do. Use it as the first step of request handling, before\n * framework routing. Matched responses already include request-context\n * response headers.\n */\nexport const handleShopifyRoutes: HydrogenRouteHandler = (options) => {\n if (options.requestContext !== options.storefrontClient.requestContext) {\n throw new Error(\n \"handleShopifyRoutes must receive the same requestContext used by storefrontClient.\",\n );\n }\n\n const url = new URL(options.request.url);\n\n for (const interceptor of SHOPIFY_ROUTE_INTERCEPTORS) {\n const responsePromise = interceptor(url, options);\n if (!responsePromise) continue;\n\n return responsePromise.then((response) =>\n safeApplyResponseHeaders(response, options.requestContext),\n );\n }\n\n return null;\n};\n"],"mappings":";;;;;;;;;;;AAWA,MAAM,6BAA6B;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;AASD,MAAa,uBAA6C,YAAY;AACpE,KAAI,QAAQ,mBAAmB,QAAQ,iBAAiB,eACtD,OAAM,IAAI,MACR,qFACD;CAGH,MAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,IAAI;AAExC,MAAK,MAAM,eAAe,4BAA4B;EACpD,MAAM,kBAAkB,YAAY,KAAK,QAAQ;AACjD,MAAI,CAAC,gBAAiB;AAEtB,SAAO,gBAAgB,MAAM,aAC3B,yBAAyB,UAAU,QAAQ,eAAe,CAC3D;;AAGH,QAAO"}
1
+ {"version":3,"file":"handle-shopify-routes.mjs","names":[],"sources":["../../../src/core/request-routing/handle-shopify-routes.ts"],"sourcesContent":["import { handleProductVariantId } from \"../product/accept-variant-id\";\nimport { handleAjaxApi } from \"./interceptors/ajax-api\";\nimport { handleShopifyApiProxy } from \"./interceptors/api-proxy\";\nimport { handleCheckoutRedirect } from \"./interceptors/checkout\";\nimport { handleMcpProxy } from \"./interceptors/mcp-proxy\";\nimport { handleSfapiProxy } from \"./interceptors/sfapi-proxy\";\nimport { handleUcpMcpProxy } from \"./interceptors/ucp-mcp-proxy\";\nimport { handleWellKnownProxy } from \"./interceptors/well-known\";\nimport { handleShopifyRouteHandlers } from \"./registered-routes\";\nimport type { HydrogenRouteHandler, HydrogenRouteInterceptor } from \"./route-types\";\nimport { safeApplyResponseHeaders } from \"./safe-apply-response-headers\";\n\nconst SHOPIFY_ROUTE_INTERCEPTORS = [\n handleShopifyApiProxy,\n handleSfapiProxy,\n handleProductVariantId,\n handleShopifyRouteHandlers,\n handleCheckoutRedirect,\n handleWellKnownProxy,\n handleUcpMcpProxy,\n handleMcpProxy,\n handleAjaxApi,\n] satisfies readonly HydrogenRouteInterceptor[];\n\n/**\n * Matches a request against Shopify standard routes and any registered handler\n * groups, returning a raw `Response` (redirect or JSON) when one matches, or\n * `null` when none do. Use it as the first step of request handling, before\n * framework routing. Matched responses already include request-context\n * response headers.\n */\nexport const handleShopifyRoutes: HydrogenRouteHandler = (options) => {\n if (options.requestContext !== options.storefrontClient.requestContext) {\n throw new Error(\n \"handleShopifyRoutes must receive the same requestContext used by storefrontClient.\",\n );\n }\n\n const url = new URL(options.request.url);\n\n for (const interceptor of SHOPIFY_ROUTE_INTERCEPTORS) {\n const responsePromise = interceptor(url, options);\n if (!responsePromise) continue;\n\n return responsePromise.then((response) =>\n safeApplyResponseHeaders(response, options.requestContext),\n );\n }\n\n return null;\n};\n"],"mappings":";;;;;;;;;;;;AAYA,MAAM,6BAA6B;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;AASD,MAAa,uBAA6C,YAAY;AACpE,KAAI,QAAQ,mBAAmB,QAAQ,iBAAiB,eACtD,OAAM,IAAI,MACR,qFACD;CAGH,MAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,IAAI;AAExC,MAAK,MAAM,eAAe,4BAA4B;EACpD,MAAM,kBAAkB,YAAY,KAAK,QAAQ;AACjD,MAAI,CAAC,gBAAiB;AAEtB,SAAO,gBAAgB,MAAM,aAC3B,yBAAyB,UAAU,QAAQ,eAAe,CAC3D;;AAGH,QAAO"}
@@ -47,7 +47,10 @@ async function getCartRedirectUrl(request, storefrontClient) {
47
47
  }
48
48
  function getCartPermalinkOrigin(storeUrl) {
49
49
  const url = new URL(storeUrl);
50
- return url.hostname === "mock.shop" ? MOCK_SHOP_CART_PERMALINK_ORIGIN : url.origin;
50
+ return isMockShopHost(url.hostname) ? MOCK_SHOP_CART_PERMALINK_ORIGIN : url.origin;
51
+ }
52
+ function isMockShopHost(hostname) {
53
+ return hostname === "mock.shop" || hostname.endsWith(".mock.shop");
51
54
  }
52
55
  function mergeSearchParams(target, source) {
53
56
  for (const [key, value] of source) if (!target.searchParams.has(key)) target.searchParams.append(key, value);
@@ -1 +1 @@
1
- {"version":3,"file":"checkout.mjs","names":[],"sources":["../../../../src/core/request-routing/interceptors/checkout.ts"],"sourcesContent":["import type { StorefrontClient } from \"../../../client\";\nimport { getCart, getCartId } from \"../../cart/get-cart\";\nimport { getLogger } from \"../../logging\";\nimport { CHECKOUT_RE, isHydrogenServerHandoffPath } from \"../../url\";\nimport type { HydrogenRouteInterceptor } from \"../route-types\";\n\nconst log = getLogger(\"checkout\");\nconst MOCK_SHOP_CART_PERMALINK_ORIGIN = \"https://demostore.mock.shop\";\n\nexport const handleCheckoutRedirect: HydrogenRouteInterceptor = (\n url,\n { request, storefrontClient },\n) => {\n if (!isHydrogenServerHandoffPath(url.pathname)) {\n return null;\n }\n\n if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n return Promise.resolve(new Response(\"Method Not Allowed\", { status: 405 }));\n }\n\n const redirectUrlPromise = CHECKOUT_RE.test(url.pathname)\n ? getCheckoutRedirectUrl(request, storefrontClient)\n : getCartRedirectUrl(request, storefrontClient);\n\n return redirectUrlPromise\n .then((redirectUrl) => {\n if (redirectUrl.pathname !== \"/\") {\n mergeSearchParams(redirectUrl, url.searchParams);\n redirectUrl.searchParams.set(\"payment\", url.searchParams.get(\"payment\") ?? \"shop_pay\");\n }\n\n return new Response(null, {\n status: 302,\n headers: { location: redirectUrl.toString() },\n });\n })\n .catch((error) => {\n log.error(\"checkout redirect request failed\", { error });\n const message = error instanceof Error ? error.message : \"Internal redirect error\";\n\n return new Response(JSON.stringify({ error: message }), {\n status: 502,\n headers: { \"content-type\": \"application/json\" },\n });\n });\n};\n\nasync function getCheckoutRedirectUrl(\n request: Request,\n storefrontClient: StorefrontClient,\n): Promise<URL> {\n const cartId = getCartId(request);\n if (!cartId) return new URL(\"/\", request.url);\n\n const result = await getCart(cartId, storefrontClient);\n return result.cart?.checkoutUrl ? new URL(result.cart.checkoutUrl) : new URL(\"/\", request.url);\n}\n\nasync function getCartRedirectUrl(\n request: Request,\n storefrontClient: StorefrontClient,\n): Promise<URL> {\n const sourceUrl = new URL(request.url);\n const redirectUrl = new URL(\n sourceUrl.pathname,\n getCartPermalinkOrigin(storefrontClient.storeUrl),\n );\n\n const cartId = getCartId(request);\n if (!cartId) return redirectUrl;\n\n try {\n const result = await getCart(cartId, storefrontClient);\n if (result.cart?.checkoutUrl) {\n mergeSearchParams(redirectUrl, new URL(result.cart.checkoutUrl).searchParams);\n }\n } catch (error) {\n log.warn(\"checkout redirect could not load cart permalink tracking params\", { error });\n }\n\n return redirectUrl;\n}\n\nfunction getCartPermalinkOrigin(storeUrl: string): string {\n const url = new URL(storeUrl);\n return url.hostname === \"mock.shop\" ? MOCK_SHOP_CART_PERMALINK_ORIGIN : url.origin;\n}\n\nfunction mergeSearchParams(target: URL, source: URLSearchParams): void {\n for (const [key, value] of source) {\n if (!target.searchParams.has(key)) target.searchParams.append(key, value);\n }\n}\n"],"mappings":";;;;;AAMA,MAAM,MAAM,UAAU,WAAW;AACjC,MAAM,kCAAkC;AAExC,MAAa,0BACX,KACA,EAAE,SAAS,uBACR;AACH,KAAI,CAAC,4BAA4B,IAAI,SAAS,CAC5C,QAAO;AAGT,KAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,OACjD,QAAO,QAAQ,QAAQ,IAAI,SAAS,sBAAsB,EAAE,QAAQ,KAAK,CAAC,CAAC;AAO7E,SAJ2B,YAAY,KAAK,IAAI,SAAS,GACrD,uBAAuB,SAAS,iBAAiB,GACjD,mBAAmB,SAAS,iBAAiB,EAG9C,MAAM,gBAAgB;AACrB,MAAI,YAAY,aAAa,KAAK;AAChC,qBAAkB,aAAa,IAAI,aAAa;AAChD,eAAY,aAAa,IAAI,WAAW,IAAI,aAAa,IAAI,UAAU,IAAI,WAAW;;AAGxF,SAAO,IAAI,SAAS,MAAM;GACxB,QAAQ;GACR,SAAS,EAAE,UAAU,YAAY,UAAU,EAAE;GAC9C,CAAC;GACF,CACD,OAAO,UAAU;AAChB,MAAI,MAAM,oCAAoC,EAAE,OAAO,CAAC;EACxD,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AAEzD,SAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,SAAS,CAAC,EAAE;GACtD,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAChD,CAAC;GACF;;AAGN,eAAe,uBACb,SACA,kBACc;CACd,MAAM,SAAS,UAAU,QAAQ;AACjC,KAAI,CAAC,OAAQ,QAAO,IAAI,IAAI,KAAK,QAAQ,IAAI;CAE7C,MAAM,SAAS,MAAM,QAAQ,QAAQ,iBAAiB;AACtD,QAAO,OAAO,MAAM,cAAc,IAAI,IAAI,OAAO,KAAK,YAAY,GAAG,IAAI,IAAI,KAAK,QAAQ,IAAI;;AAGhG,eAAe,mBACb,SACA,kBACc;CACd,MAAM,YAAY,IAAI,IAAI,QAAQ,IAAI;CACtC,MAAM,cAAc,IAAI,IACtB,UAAU,UACV,uBAAuB,iBAAiB,SAAS,CAClD;CAED,MAAM,SAAS,UAAU,QAAQ;AACjC,KAAI,CAAC,OAAQ,QAAO;AAEpB,KAAI;EACF,MAAM,SAAS,MAAM,QAAQ,QAAQ,iBAAiB;AACtD,MAAI,OAAO,MAAM,YACf,mBAAkB,aAAa,IAAI,IAAI,OAAO,KAAK,YAAY,CAAC,aAAa;UAExE,OAAO;AACd,MAAI,KAAK,mEAAmE,EAAE,OAAO,CAAC;;AAGxF,QAAO;;AAGT,SAAS,uBAAuB,UAA0B;CACxD,MAAM,MAAM,IAAI,IAAI,SAAS;AAC7B,QAAO,IAAI,aAAa,cAAc,kCAAkC,IAAI;;AAG9E,SAAS,kBAAkB,QAAa,QAA+B;AACrE,MAAK,MAAM,CAAC,KAAK,UAAU,OACzB,KAAI,CAAC,OAAO,aAAa,IAAI,IAAI,CAAE,QAAO,aAAa,OAAO,KAAK,MAAM"}
1
+ {"version":3,"file":"checkout.mjs","names":[],"sources":["../../../../src/core/request-routing/interceptors/checkout.ts"],"sourcesContent":["import type { StorefrontClient } from \"../../../client\";\nimport { getCart, getCartId } from \"../../cart/get-cart\";\nimport { getLogger } from \"../../logging\";\nimport { CHECKOUT_RE, isHydrogenServerHandoffPath } from \"../../url\";\nimport type { HydrogenRouteInterceptor } from \"../route-types\";\n\nconst log = getLogger(\"checkout\");\nconst MOCK_SHOP_CART_PERMALINK_ORIGIN = \"https://demostore.mock.shop\";\n\nexport const handleCheckoutRedirect: HydrogenRouteInterceptor = (\n url,\n { request, storefrontClient },\n) => {\n if (!isHydrogenServerHandoffPath(url.pathname)) {\n return null;\n }\n\n if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n return Promise.resolve(new Response(\"Method Not Allowed\", { status: 405 }));\n }\n\n const redirectUrlPromise = CHECKOUT_RE.test(url.pathname)\n ? getCheckoutRedirectUrl(request, storefrontClient)\n : getCartRedirectUrl(request, storefrontClient);\n\n return redirectUrlPromise\n .then((redirectUrl) => {\n if (redirectUrl.pathname !== \"/\") {\n mergeSearchParams(redirectUrl, url.searchParams);\n redirectUrl.searchParams.set(\"payment\", url.searchParams.get(\"payment\") ?? \"shop_pay\");\n }\n\n return new Response(null, {\n status: 302,\n headers: { location: redirectUrl.toString() },\n });\n })\n .catch((error) => {\n log.error(\"checkout redirect request failed\", { error });\n const message = error instanceof Error ? error.message : \"Internal redirect error\";\n\n return new Response(JSON.stringify({ error: message }), {\n status: 502,\n headers: { \"content-type\": \"application/json\" },\n });\n });\n};\n\nasync function getCheckoutRedirectUrl(\n request: Request,\n storefrontClient: StorefrontClient,\n): Promise<URL> {\n const cartId = getCartId(request);\n if (!cartId) return new URL(\"/\", request.url);\n\n const result = await getCart(cartId, storefrontClient);\n return result.cart?.checkoutUrl ? new URL(result.cart.checkoutUrl) : new URL(\"/\", request.url);\n}\n\nasync function getCartRedirectUrl(\n request: Request,\n storefrontClient: StorefrontClient,\n): Promise<URL> {\n const sourceUrl = new URL(request.url);\n const redirectUrl = new URL(\n sourceUrl.pathname,\n getCartPermalinkOrigin(storefrontClient.storeUrl),\n );\n\n const cartId = getCartId(request);\n if (!cartId) return redirectUrl;\n\n try {\n const result = await getCart(cartId, storefrontClient);\n if (result.cart?.checkoutUrl) {\n mergeSearchParams(redirectUrl, new URL(result.cart.checkoutUrl).searchParams);\n }\n } catch (error) {\n log.warn(\"checkout redirect could not load cart permalink tracking params\", { error });\n }\n\n return redirectUrl;\n}\n\nfunction getCartPermalinkOrigin(storeUrl: string): string {\n const url = new URL(storeUrl);\n return isMockShopHost(url.hostname) ? MOCK_SHOP_CART_PERMALINK_ORIGIN : url.origin;\n}\n\n// mock.shop serves many stores, each on its own host (pets.mock.shop, ...), and\n// none of them renders cart permalinks, so every mock host hands off to the demo store.\nfunction isMockShopHost(hostname: string): boolean {\n return hostname === \"mock.shop\" || hostname.endsWith(\".mock.shop\");\n}\n\nfunction mergeSearchParams(target: URL, source: URLSearchParams): void {\n for (const [key, value] of source) {\n if (!target.searchParams.has(key)) target.searchParams.append(key, value);\n }\n}\n"],"mappings":";;;;;AAMA,MAAM,MAAM,UAAU,WAAW;AACjC,MAAM,kCAAkC;AAExC,MAAa,0BACX,KACA,EAAE,SAAS,uBACR;AACH,KAAI,CAAC,4BAA4B,IAAI,SAAS,CAC5C,QAAO;AAGT,KAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,OACjD,QAAO,QAAQ,QAAQ,IAAI,SAAS,sBAAsB,EAAE,QAAQ,KAAK,CAAC,CAAC;AAO7E,SAJ2B,YAAY,KAAK,IAAI,SAAS,GACrD,uBAAuB,SAAS,iBAAiB,GACjD,mBAAmB,SAAS,iBAAiB,EAG9C,MAAM,gBAAgB;AACrB,MAAI,YAAY,aAAa,KAAK;AAChC,qBAAkB,aAAa,IAAI,aAAa;AAChD,eAAY,aAAa,IAAI,WAAW,IAAI,aAAa,IAAI,UAAU,IAAI,WAAW;;AAGxF,SAAO,IAAI,SAAS,MAAM;GACxB,QAAQ;GACR,SAAS,EAAE,UAAU,YAAY,UAAU,EAAE;GAC9C,CAAC;GACF,CACD,OAAO,UAAU;AAChB,MAAI,MAAM,oCAAoC,EAAE,OAAO,CAAC;EACxD,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AAEzD,SAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,SAAS,CAAC,EAAE;GACtD,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAChD,CAAC;GACF;;AAGN,eAAe,uBACb,SACA,kBACc;CACd,MAAM,SAAS,UAAU,QAAQ;AACjC,KAAI,CAAC,OAAQ,QAAO,IAAI,IAAI,KAAK,QAAQ,IAAI;CAE7C,MAAM,SAAS,MAAM,QAAQ,QAAQ,iBAAiB;AACtD,QAAO,OAAO,MAAM,cAAc,IAAI,IAAI,OAAO,KAAK,YAAY,GAAG,IAAI,IAAI,KAAK,QAAQ,IAAI;;AAGhG,eAAe,mBACb,SACA,kBACc;CACd,MAAM,YAAY,IAAI,IAAI,QAAQ,IAAI;CACtC,MAAM,cAAc,IAAI,IACtB,UAAU,UACV,uBAAuB,iBAAiB,SAAS,CAClD;CAED,MAAM,SAAS,UAAU,QAAQ;AACjC,KAAI,CAAC,OAAQ,QAAO;AAEpB,KAAI;EACF,MAAM,SAAS,MAAM,QAAQ,QAAQ,iBAAiB;AACtD,MAAI,OAAO,MAAM,YACf,mBAAkB,aAAa,IAAI,IAAI,OAAO,KAAK,YAAY,CAAC,aAAa;UAExE,OAAO;AACd,MAAI,KAAK,mEAAmE,EAAE,OAAO,CAAC;;AAGxF,QAAO;;AAGT,SAAS,uBAAuB,UAA0B;CACxD,MAAM,MAAM,IAAI,IAAI,SAAS;AAC7B,QAAO,eAAe,IAAI,SAAS,GAAG,kCAAkC,IAAI;;AAK9E,SAAS,eAAe,UAA2B;AACjD,QAAO,aAAa,eAAe,SAAS,SAAS,aAAa;;AAGpE,SAAS,kBAAkB,QAAa,QAA+B;AACrE,MAAK,MAAM,CAAC,KAAK,UAAU,OACzB,KAAI,CAAC,OAAO,aAAa,IAAI,IAAI,CAAE,QAAO,aAAa,OAAO,KAAK,MAAM"}
@@ -3,25 +3,30 @@ import { extractHeaders } from "../../headers.mjs";
3
3
 
4
4
  //#region src/core/request-routing/interceptors/proxy.ts
5
5
  const PROXY_TIMEOUT_MS = 3e4;
6
+ const PROXY_ERROR_CACHE_CONTROL = "no-store";
6
7
  function createProxyInterceptor(descriptor) {
7
8
  const log = getLogger(descriptor.scope);
8
9
  const formatError = descriptor.formatError ?? defaultFormatError;
9
10
  return (url, options) => {
10
11
  const { request, storefrontClient } = options;
11
12
  if (!descriptor.match.test(url.pathname)) return null;
12
- if (descriptor.methods && !descriptor.methods.includes(request.method)) return null;
13
+ if (descriptor.methods && !descriptor.methods.includes(request.method)) return Promise.resolve(new Response(JSON.stringify(formatError("Method Not Allowed")), {
14
+ status: 405,
15
+ headers: {
16
+ allow: descriptor.methods.join(", "),
17
+ "content-type": "application/json",
18
+ "cache-control": PROXY_ERROR_CACHE_CONTROL
19
+ }
20
+ }));
13
21
  let upstreamUrl;
14
22
  let init;
15
23
  try {
16
24
  const upstreamPathname = descriptor.rewritePathname?.(url.pathname) ?? url.pathname;
17
25
  upstreamUrl = new URL(upstreamPathname + url.search, storefrontClient.storeUrl);
18
- const forwardedHeaders = createProxyRequestHeaders(descriptor, request);
19
- options.requestContext.applyStorefrontRequestHeaders(forwardedHeaders);
20
- descriptor.requestHeaders.prepare?.(forwardedHeaders, options, url);
21
26
  init = {
22
27
  method: request.method,
23
28
  body: request.body,
24
- headers: forwardedHeaders,
29
+ headers: createProxyRequestHeaders(descriptor, options, url),
25
30
  signal: AbortSignal.timeout(descriptor.timeoutMs ?? PROXY_TIMEOUT_MS),
26
31
  redirect: descriptor.redirect ?? "manual"
27
32
  };
@@ -52,13 +57,19 @@ function createProxyErrorResponse(error, status, formatError) {
52
57
  const message = error instanceof Error ? error.message : "Internal proxy error";
53
58
  return new Response(JSON.stringify(formatError(message)), {
54
59
  status,
55
- headers: { "content-type": "application/json" }
60
+ headers: {
61
+ "content-type": "application/json",
62
+ "cache-control": PROXY_ERROR_CACHE_CONTROL
63
+ }
56
64
  });
57
65
  }
58
- function createProxyRequestHeaders(descriptor, request) {
59
- const { allow, deny } = descriptor.requestHeaders;
66
+ function createProxyRequestHeaders(descriptor, options, url) {
67
+ const { request, requestContext } = options;
68
+ const { allow, deny, prepare } = descriptor.requestHeaders;
60
69
  const headers = allow ? new Headers(extractHeaders((key) => request.headers.get(key), allow)) : new Headers(request.headers);
70
+ requestContext.applyStorefrontRequestHeaders(headers);
61
71
  for (const header of deny ?? []) headers.delete(header);
72
+ prepare?.(headers, options, url);
62
73
  return headers;
63
74
  }
64
75
  function createProxyResponseHeaders(upstreamHeaders) {
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.mjs","names":[],"sources":["../../../../src/core/request-routing/interceptors/proxy.ts"],"sourcesContent":["import { extractHeaders } from \"../../headers\";\nimport { getLogger } from \"../../logging\";\nimport type { HydrogenRouteInterceptor, HydrogenRoutesOptions } from \"../route-types\";\n\nconst PROXY_TIMEOUT_MS = 30_000;\n\ntype PrepareHeaders = (headers: Headers, options: HydrogenRoutesOptions, url: URL) => void;\n\ntype ProxyRequestHeaderOptions = (\n | { allow: readonly string[]; deny?: never }\n | { allow?: never; deny: readonly string[] }\n) & {\n prepare?: PrepareHeaders;\n};\n\ntype ProxyResponseHeaderOptions = {\n prepare?: PrepareHeaders;\n};\n\ntype ProxyDescriptor = {\n match: RegExp;\n methods?: readonly string[];\n formatError?: (message: string) => unknown;\n redirect?: RequestRedirect;\n scope: string;\n timeoutMs?: number;\n rewritePathname?: (pathname: string) => string;\n requestHeaders: ProxyRequestHeaderOptions;\n responseHeaders?: ProxyResponseHeaderOptions;\n};\n\nexport function createProxyInterceptor(descriptor: ProxyDescriptor): HydrogenRouteInterceptor {\n const log = getLogger(descriptor.scope);\n const formatError = descriptor.formatError ?? defaultFormatError;\n\n return (url, options) => {\n const { request, storefrontClient } = options;\n if (!descriptor.match.test(url.pathname)) return null;\n if (descriptor.methods && !descriptor.methods.includes(request.method)) return null;\n\n let upstreamUrl: URL;\n let init: RequestInit & { duplex?: \"half\" };\n try {\n const upstreamPathname = descriptor.rewritePathname?.(url.pathname) ?? url.pathname;\n upstreamUrl = new URL(upstreamPathname + url.search, storefrontClient.storeUrl);\n const forwardedHeaders = createProxyRequestHeaders(descriptor, request);\n options.requestContext.applyStorefrontRequestHeaders(forwardedHeaders);\n descriptor.requestHeaders.prepare?.(forwardedHeaders, options, url);\n\n init = {\n method: request.method,\n body: request.body,\n headers: forwardedHeaders,\n signal: AbortSignal.timeout(descriptor.timeoutMs ?? PROXY_TIMEOUT_MS),\n redirect: descriptor.redirect ?? \"manual\",\n };\n\n // Node's fetch requires this when forwarding a streaming request body.\n if (request.body) init.duplex = \"half\";\n } catch (error) {\n log.error(\"request failed\", { error });\n return Promise.resolve(createProxyErrorResponse(error, 500, formatError));\n }\n\n return fetch(upstreamUrl, init)\n .then((upstreamResponse) => {\n const headers = createProxyResponseHeaders(upstreamResponse.headers);\n descriptor.responseHeaders?.prepare?.(headers, options, url);\n options.requestContext.consumeStorefrontResponseHeaders(headers);\n\n return new Response(upstreamResponse.body, {\n status: upstreamResponse.status,\n statusText: upstreamResponse.statusText,\n headers,\n });\n })\n .catch((error) => {\n log.error(\"request failed\", { error });\n return createProxyErrorResponse(error, 502, formatError);\n });\n };\n}\n\nfunction defaultFormatError(message: string): { error: string } {\n return { error: message };\n}\n\nfunction createProxyErrorResponse(\n error: unknown,\n status: number,\n formatError: (message: string) => unknown,\n): Response {\n const message = error instanceof Error ? error.message : \"Internal proxy error\";\n return new Response(JSON.stringify(formatError(message)), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\nfunction createProxyRequestHeaders(descriptor: ProxyDescriptor, request: Request): Headers {\n const { allow, deny } = descriptor.requestHeaders;\n const headers = allow\n ? new Headers(extractHeaders((key) => request.headers.get(key), allow))\n : new Headers(request.headers);\n for (const header of deny ?? []) headers.delete(header);\n return headers;\n}\n\nexport function createProxyResponseHeaders(upstreamHeaders: Headers): Headers {\n const headers = new Headers(upstreamHeaders);\n headers.delete(\"content-encoding\");\n headers.delete(\"content-length\");\n return headers;\n}\n"],"mappings":";;;;AAIA,MAAM,mBAAmB;AA2BzB,SAAgB,uBAAuB,YAAuD;CAC5F,MAAM,MAAM,UAAU,WAAW,MAAM;CACvC,MAAM,cAAc,WAAW,eAAe;AAE9C,SAAQ,KAAK,YAAY;EACvB,MAAM,EAAE,SAAS,qBAAqB;AACtC,MAAI,CAAC,WAAW,MAAM,KAAK,IAAI,SAAS,CAAE,QAAO;AACjD,MAAI,WAAW,WAAW,CAAC,WAAW,QAAQ,SAAS,QAAQ,OAAO,CAAE,QAAO;EAE/E,IAAI;EACJ,IAAI;AACJ,MAAI;GACF,MAAM,mBAAmB,WAAW,kBAAkB,IAAI,SAAS,IAAI,IAAI;AAC3E,iBAAc,IAAI,IAAI,mBAAmB,IAAI,QAAQ,iBAAiB,SAAS;GAC/E,MAAM,mBAAmB,0BAA0B,YAAY,QAAQ;AACvE,WAAQ,eAAe,8BAA8B,iBAAiB;AACtE,cAAW,eAAe,UAAU,kBAAkB,SAAS,IAAI;AAEnE,UAAO;IACL,QAAQ,QAAQ;IAChB,MAAM,QAAQ;IACd,SAAS;IACT,QAAQ,YAAY,QAAQ,WAAW,aAAa,iBAAiB;IACrE,UAAU,WAAW,YAAY;IAClC;AAGD,OAAI,QAAQ,KAAM,MAAK,SAAS;WACzB,OAAO;AACd,OAAI,MAAM,kBAAkB,EAAE,OAAO,CAAC;AACtC,UAAO,QAAQ,QAAQ,yBAAyB,OAAO,KAAK,YAAY,CAAC;;AAG3E,SAAO,MAAM,aAAa,KAAK,CAC5B,MAAM,qBAAqB;GAC1B,MAAM,UAAU,2BAA2B,iBAAiB,QAAQ;AACpE,cAAW,iBAAiB,UAAU,SAAS,SAAS,IAAI;AAC5D,WAAQ,eAAe,iCAAiC,QAAQ;AAEhE,UAAO,IAAI,SAAS,iBAAiB,MAAM;IACzC,QAAQ,iBAAiB;IACzB,YAAY,iBAAiB;IAC7B;IACD,CAAC;IACF,CACD,OAAO,UAAU;AAChB,OAAI,MAAM,kBAAkB,EAAE,OAAO,CAAC;AACtC,UAAO,yBAAyB,OAAO,KAAK,YAAY;IACxD;;;AAIR,SAAS,mBAAmB,SAAoC;AAC9D,QAAO,EAAE,OAAO,SAAS;;AAG3B,SAAS,yBACP,OACA,QACA,aACU;CACV,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,QAAO,IAAI,SAAS,KAAK,UAAU,YAAY,QAAQ,CAAC,EAAE;EACxD;EACA,SAAS,EAAE,gBAAgB,oBAAoB;EAChD,CAAC;;AAGJ,SAAS,0BAA0B,YAA6B,SAA2B;CACzF,MAAM,EAAE,OAAO,SAAS,WAAW;CACnC,MAAM,UAAU,QACZ,IAAI,QAAQ,gBAAgB,QAAQ,QAAQ,QAAQ,IAAI,IAAI,EAAE,MAAM,CAAC,GACrE,IAAI,QAAQ,QAAQ,QAAQ;AAChC,MAAK,MAAM,UAAU,QAAQ,EAAE,CAAE,SAAQ,OAAO,OAAO;AACvD,QAAO;;AAGT,SAAgB,2BAA2B,iBAAmC;CAC5E,MAAM,UAAU,IAAI,QAAQ,gBAAgB;AAC5C,SAAQ,OAAO,mBAAmB;AAClC,SAAQ,OAAO,iBAAiB;AAChC,QAAO"}
1
+ {"version":3,"file":"proxy.mjs","names":[],"sources":["../../../../src/core/request-routing/interceptors/proxy.ts"],"sourcesContent":["import { extractHeaders } from \"../../headers\";\nimport { getLogger } from \"../../logging\";\nimport type { HydrogenRouteInterceptor, HydrogenRoutesOptions } from \"../route-types\";\n\nconst PROXY_TIMEOUT_MS = 30_000;\n\n// Proxy errors are transient and may be buyer-specific: never cache them.\nconst PROXY_ERROR_CACHE_CONTROL = \"no-store\";\n\ntype PrepareHeaders = (headers: Headers, options: HydrogenRoutesOptions, url: URL) => void;\n\ntype ProxyRequestHeaderOptions = (\n | { allow: readonly string[]; deny?: never }\n | { allow?: never; deny: readonly string[] }\n) & {\n prepare?: PrepareHeaders;\n};\n\ntype ProxyResponseHeaderOptions = {\n prepare?: PrepareHeaders;\n};\n\ntype ProxyDescriptor = {\n match: RegExp;\n methods?: readonly string[];\n formatError?: (message: string) => unknown;\n redirect?: RequestRedirect;\n scope: string;\n timeoutMs?: number;\n rewritePathname?: (pathname: string) => string;\n requestHeaders: ProxyRequestHeaderOptions;\n responseHeaders?: ProxyResponseHeaderOptions;\n};\n\nexport function createProxyInterceptor(descriptor: ProxyDescriptor): HydrogenRouteInterceptor {\n const log = getLogger(descriptor.scope);\n const formatError = descriptor.formatError ?? defaultFormatError;\n\n return (url, options) => {\n const { request, storefrontClient } = options;\n if (!descriptor.match.test(url.pathname)) return null;\n if (descriptor.methods && !descriptor.methods.includes(request.method)) {\n // Method not allowed. Shape the body via formatError, like other errors.\n return Promise.resolve(\n new Response(JSON.stringify(formatError(\"Method Not Allowed\")), {\n status: 405,\n headers: {\n allow: descriptor.methods.join(\", \"),\n \"content-type\": \"application/json\",\n \"cache-control\": PROXY_ERROR_CACHE_CONTROL,\n },\n }),\n );\n }\n\n let upstreamUrl: URL;\n let init: RequestInit & { duplex?: \"half\" };\n try {\n const upstreamPathname = descriptor.rewritePathname?.(url.pathname) ?? url.pathname;\n upstreamUrl = new URL(upstreamPathname + url.search, storefrontClient.storeUrl);\n\n init = {\n method: request.method,\n body: request.body,\n headers: createProxyRequestHeaders(descriptor, options, url),\n signal: AbortSignal.timeout(descriptor.timeoutMs ?? PROXY_TIMEOUT_MS),\n redirect: descriptor.redirect ?? \"manual\",\n };\n\n // Node's fetch requires this when forwarding a streaming request body.\n if (request.body) init.duplex = \"half\";\n } catch (error) {\n log.error(\"request failed\", { error });\n return Promise.resolve(createProxyErrorResponse(error, 500, formatError));\n }\n\n return fetch(upstreamUrl, init)\n .then((upstreamResponse) => {\n const headers = createProxyResponseHeaders(upstreamResponse.headers);\n descriptor.responseHeaders?.prepare?.(headers, options, url);\n options.requestContext.consumeStorefrontResponseHeaders(headers);\n\n return new Response(upstreamResponse.body, {\n status: upstreamResponse.status,\n statusText: upstreamResponse.statusText,\n headers,\n });\n })\n .catch((error) => {\n log.error(\"request failed\", { error });\n return createProxyErrorResponse(error, 502, formatError);\n });\n };\n}\n\nfunction defaultFormatError(message: string): { error: string } {\n return { error: message };\n}\n\nfunction createProxyErrorResponse(\n error: unknown,\n status: number,\n formatError: (message: string) => unknown,\n): Response {\n const message = error instanceof Error ? error.message : \"Internal proxy error\";\n return new Response(JSON.stringify(formatError(message)), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": PROXY_ERROR_CACHE_CONTROL,\n },\n });\n}\n\nfunction createProxyRequestHeaders(\n descriptor: ProxyDescriptor,\n options: HydrogenRoutesOptions,\n url: URL,\n): Headers {\n const { request, requestContext } = options;\n const { allow, deny, prepare } = descriptor.requestHeaders;\n const headers = allow\n ? new Headers(extractHeaders((key) => request.headers.get(key), allow))\n : new Headers(request.headers);\n\n requestContext.applyStorefrontRequestHeaders(headers);\n\n for (const header of deny ?? []) headers.delete(header);\n prepare?.(headers, options, url);\n\n return headers;\n}\n\nexport function createProxyResponseHeaders(upstreamHeaders: Headers): Headers {\n const headers = new Headers(upstreamHeaders);\n headers.delete(\"content-encoding\");\n headers.delete(\"content-length\");\n return headers;\n}\n"],"mappings":";;;;AAIA,MAAM,mBAAmB;AAGzB,MAAM,4BAA4B;AA2BlC,SAAgB,uBAAuB,YAAuD;CAC5F,MAAM,MAAM,UAAU,WAAW,MAAM;CACvC,MAAM,cAAc,WAAW,eAAe;AAE9C,SAAQ,KAAK,YAAY;EACvB,MAAM,EAAE,SAAS,qBAAqB;AACtC,MAAI,CAAC,WAAW,MAAM,KAAK,IAAI,SAAS,CAAE,QAAO;AACjD,MAAI,WAAW,WAAW,CAAC,WAAW,QAAQ,SAAS,QAAQ,OAAO,CAEpE,QAAO,QAAQ,QACb,IAAI,SAAS,KAAK,UAAU,YAAY,qBAAqB,CAAC,EAAE;GAC9D,QAAQ;GACR,SAAS;IACP,OAAO,WAAW,QAAQ,KAAK,KAAK;IACpC,gBAAgB;IAChB,iBAAiB;IAClB;GACF,CAAC,CACH;EAGH,IAAI;EACJ,IAAI;AACJ,MAAI;GACF,MAAM,mBAAmB,WAAW,kBAAkB,IAAI,SAAS,IAAI,IAAI;AAC3E,iBAAc,IAAI,IAAI,mBAAmB,IAAI,QAAQ,iBAAiB,SAAS;AAE/E,UAAO;IACL,QAAQ,QAAQ;IAChB,MAAM,QAAQ;IACd,SAAS,0BAA0B,YAAY,SAAS,IAAI;IAC5D,QAAQ,YAAY,QAAQ,WAAW,aAAa,iBAAiB;IACrE,UAAU,WAAW,YAAY;IAClC;AAGD,OAAI,QAAQ,KAAM,MAAK,SAAS;WACzB,OAAO;AACd,OAAI,MAAM,kBAAkB,EAAE,OAAO,CAAC;AACtC,UAAO,QAAQ,QAAQ,yBAAyB,OAAO,KAAK,YAAY,CAAC;;AAG3E,SAAO,MAAM,aAAa,KAAK,CAC5B,MAAM,qBAAqB;GAC1B,MAAM,UAAU,2BAA2B,iBAAiB,QAAQ;AACpE,cAAW,iBAAiB,UAAU,SAAS,SAAS,IAAI;AAC5D,WAAQ,eAAe,iCAAiC,QAAQ;AAEhE,UAAO,IAAI,SAAS,iBAAiB,MAAM;IACzC,QAAQ,iBAAiB;IACzB,YAAY,iBAAiB;IAC7B;IACD,CAAC;IACF,CACD,OAAO,UAAU;AAChB,OAAI,MAAM,kBAAkB,EAAE,OAAO,CAAC;AACtC,UAAO,yBAAyB,OAAO,KAAK,YAAY;IACxD;;;AAIR,SAAS,mBAAmB,SAAoC;AAC9D,QAAO,EAAE,OAAO,SAAS;;AAG3B,SAAS,yBACP,OACA,QACA,aACU;CACV,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,QAAO,IAAI,SAAS,KAAK,UAAU,YAAY,QAAQ,CAAC,EAAE;EACxD;EACA,SAAS;GACP,gBAAgB;GAChB,iBAAiB;GAClB;EACF,CAAC;;AAGJ,SAAS,0BACP,YACA,SACA,KACS;CACT,MAAM,EAAE,SAAS,mBAAmB;CACpC,MAAM,EAAE,OAAO,MAAM,YAAY,WAAW;CAC5C,MAAM,UAAU,QACZ,IAAI,QAAQ,gBAAgB,QAAQ,QAAQ,QAAQ,IAAI,IAAI,EAAE,MAAM,CAAC,GACrE,IAAI,QAAQ,QAAQ,QAAQ;AAEhC,gBAAe,8BAA8B,QAAQ;AAErD,MAAK,MAAM,UAAU,QAAQ,EAAE,CAAE,SAAQ,OAAO,OAAO;AACvD,WAAU,SAAS,SAAS,IAAI;AAEhC,QAAO;;AAGT,SAAgB,2BAA2B,iBAAmC;CAC5E,MAAM,UAAU,IAAI,QAAQ,gBAAgB;AAC5C,SAAQ,OAAO,mBAAmB;AAClC,SAAQ,OAAO,iBAAiB;AAChC,QAAO"}
@@ -0,0 +1,35 @@
1
+ import { PROXY_REQUEST_HEADER_DENYLIST } from "../../headers.mjs";
2
+ import { UCP_MCP_RE } from "../../url.mjs";
3
+ import { createProxyInterceptor } from "./proxy.mjs";
4
+
5
+ //#region src/core/request-routing/interceptors/ucp-mcp-proxy.ts
6
+ const JSON_RPC_INTERNAL_ERROR = -32603;
7
+ const RESPONSE_STATE_REASON = "ucp request";
8
+ /**
9
+ * Caller-authenticated UCP pass-through. The caller owns both the
10
+ * Authorization header and the UCP agent profile in its request body.
11
+ */
12
+ const proxyUcpMcpRequest = createProxyInterceptor({
13
+ match: UCP_MCP_RE,
14
+ methods: ["POST"],
15
+ requestHeaders: { deny: PROXY_REQUEST_HEADER_DENYLIST },
16
+ formatError: (message) => ({
17
+ jsonrpc: "2.0",
18
+ error: {
19
+ code: JSON_RPC_INTERNAL_ERROR,
20
+ message
21
+ },
22
+ id: null
23
+ }),
24
+ scope: "ucp-mcp-proxy"
25
+ });
26
+ const handleUcpMcpProxy = (url, options) => {
27
+ if (!UCP_MCP_RE.test(url.pathname)) return null;
28
+ options.requestContext.markResponseAsPersonalized(RESPONSE_STATE_REASON);
29
+ options.requestContext.markResponseAsSessionEstablishing(RESPONSE_STATE_REASON);
30
+ return proxyUcpMcpRequest(url, options);
31
+ };
32
+
33
+ //#endregion
34
+ export { handleUcpMcpProxy };
35
+ //# sourceMappingURL=ucp-mcp-proxy.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ucp-mcp-proxy.mjs","names":[],"sources":["../../../../src/core/request-routing/interceptors/ucp-mcp-proxy.ts"],"sourcesContent":["import { PROXY_REQUEST_HEADER_DENYLIST } from \"../../headers\";\nimport { UCP_MCP_RE } from \"../../url\";\nimport type { HydrogenRouteInterceptor } from \"../route-types\";\nimport { createProxyInterceptor } from \"./proxy\";\n\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\n// Traceability reason recorded when marking UCP MCP response state.\nconst RESPONSE_STATE_REASON = \"ucp request\";\n\n/**\n * Caller-authenticated UCP pass-through. The caller owns both the\n * Authorization header and the UCP agent profile in its request body.\n */\nconst proxyUcpMcpRequest = createProxyInterceptor({\n match: UCP_MCP_RE,\n methods: [\"POST\"],\n requestHeaders: { deny: PROXY_REQUEST_HEADER_DENYLIST },\n formatError: (message) => ({\n jsonrpc: \"2.0\",\n error: { code: JSON_RPC_INTERNAL_ERROR, message },\n id: null,\n }),\n scope: \"ucp-mcp-proxy\",\n});\n\nexport const handleUcpMcpProxy: HydrogenRouteInterceptor = (url, options) => {\n if (!UCP_MCP_RE.test(url.pathname)) return null;\n\n options.requestContext.markResponseAsPersonalized(RESPONSE_STATE_REASON);\n // UCP MCP is a sanctioned session-establishing endpoint: let the store's\n // session cookie return on a cold agent request, staying within the response\n // state gating rather than bypassing it.\n options.requestContext.markResponseAsSessionEstablishing(RESPONSE_STATE_REASON);\n return proxyUcpMcpRequest(url, options);\n};\n"],"mappings":";;;;;AAKA,MAAM,0BAA0B;AAGhC,MAAM,wBAAwB;;;;;AAM9B,MAAM,qBAAqB,uBAAuB;CAChD,OAAO;CACP,SAAS,CAAC,OAAO;CACjB,gBAAgB,EAAE,MAAM,+BAA+B;CACvD,cAAc,aAAa;EACzB,SAAS;EACT,OAAO;GAAE,MAAM;GAAyB;GAAS;EACjD,IAAI;EACL;CACD,OAAO;CACR,CAAC;AAEF,MAAa,qBAA+C,KAAK,YAAY;AAC3E,KAAI,CAAC,WAAW,KAAK,IAAI,SAAS,CAAE,QAAO;AAE3C,SAAQ,eAAe,2BAA2B,sBAAsB;AAIxE,SAAQ,eAAe,kCAAkC,sBAAsB;AAC/E,QAAO,mBAAmB,KAAK,QAAQ"}
@@ -1,7 +1,7 @@
1
1
  import { asInlineScript } from "./utils/inline-script.mjs";
2
2
 
3
3
  //#region src/core/shopify-scripts/analytics.ts
4
- const initializeShopifyAnalyticsBus = (scriptData) => decodeURIComponent("(function()%7Blet%20e%3D%7Btrace%3A%60debug%60%2Cdebug%3A%60debug%60%2Cinfo%3A%60info%60%2Cwarn%3A%60warn%60%2Cerror%3A%60error%60%2Cfatal%3A%60error%60%7D%3Bfunction%20t(e%2Ct)%7Breturn%60%5Bhydrogen%3A%24%7Be%7D%3A%24%7Bt%7D%5D%60%7Dfunction%20n(n%2Cr%2Ci)%7Blet%7Bscope%3Aa%2Cerror%3Ao%2C...s%7D%3Di%3F%3F%7B%7D%2Cc%3D%5Ba%3F%60%24%7Bt(n%2Ca)%7D%20%24%7Br%7D%60%3Ar%5D%3Bo!%3D%3Dvoid%200%26%26c.push(o)%2CObject.keys(s).length%3E0%26%26c.push(s)%2Cconsole%5Be%5Bn%5D%5D(...c)%7Dlet%20r%3D%7Btrace%3A(e%2Ct)%3D%3En(%60trace%60%2Ce%2Ct)%2Cdebug%3A(e%2Ct)%3D%3En(%60debug%60%2Ce%2Ct)%2Cinfo%3A(e%2Ct)%3D%3En(%60info%60%2Ce%2Ct)%2Cwarn%3A(e%2Ct)%3D%3En(%60warn%60%2Ce%2Ct)%2Cerror%3A(e%2Ct)%3D%3En(%60error%60%2Ce%2Ct)%2Cfatal%3A(e%2Ct)%3D%3En(%60fatal%60%2Ce%2Ct)%7D%2Ci%3D%60https%3A%2F%2Fcdn.shopify.com%60%3B%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%3Blet%20a%3D%60visitorConsentCollected%60%3Bfunction%20o()%7Bif(typeof%20window%3E%60u%60)return%3Blet%20e%3Dwindow%3Breturn%20e.Shopify%3F%3F%3D%7B%7D%7Dfunction%20s(e)%7Breturn%20typeof%20e%3D%3D%60object%60%26%26!!e%26%26!Array.isArray(e)%7Dfunction%20c(e%2Ct)%7Blet%20n%3De.subscriptions.get(t.event)%3Bif(!n%3F.size)%7Be.nextReplaySequence%3DMath.max(e.nextReplaySequence%2Ct.sequence%2B1)%3Breturn%7Dfor(let%20i%20of%20n)try%7Bi(t.payload)%7Dcatch(t)%7Br.error(%60error%20in%20analytics%20destination%20%22%24%7Be.name%7D%22%60%2C%7Bscope%3A%60analytics%60%2Cerror%3At%7D)%7De.nextReplaySequence%3DMath.max(e.nextReplaySequence%2Ct.sequence%2B1)%7Dfunction%20l(e)%7Blet%20t%3D0%2Cn%3D!0%2Ci%3D%5B%5D%2Ca%3Dnew%20Set%2Co%3Dnew%20Set%3Bfunction%20s(t%3D!1)%7Bif(!e.canTrack())%7Bt%26%26(i.length%3D0%2Cn%3D!1)%3Breturn%7Dn%3D!0%3Bfor(let%20e%20of%20a)for(let%20t%20of%20i)t.sequence%3Ce.nextReplaySequence%7C%7Cc(e%2Ct)%7Dfunction%20l(t)%7Bif(o.has(t.name))return%20r.warn(%60analytics%20destination%20%22%24%7Bt.name%7D%22%20is%20already%20registered%60%2C%7Bscope%3A%60analytics%60%7D)%2C()%3D%3E%7B%7D%3Bo.add(t.name)%3Blet%20n%3D%7Bname%3At.name%2Csubscriptions%3Anew%20Map%2CnextReplaySequence%3A0%7D%2Ci%3D!1%2Cc%3D(t%2Cr)%3D%3E%7Bif(i)return()%3D%3E%7B%7D%3Bif(e.isSupportedEvent%26%26!e.isSupportedEvent(t))return%20e.warnUnsupportedEvent%3F.(t)%2C()%3D%3E%7B%7D%3Blet%20a%3Dn.subscriptions.get(t)%3Breturn%20a%7C%7C(a%3Dnew%20Set%2Cn.subscriptions.set(t%2Ca))%2Ca.add(r)%2C()%3D%3E%7Blet%20e%3Dn.subscriptions.get(t)%3Be%3F.delete(r)%2Ce%3F.size%3D%3D%3D0%26%26n.subscriptions.delete(t)%7D%7D%2Cl%3D()%3D%3E%7Bi%7C%7C(i%3D!0%2Ca.delete(n)%2Co.delete(t.name)%2Cn.subscriptions.clear()%2Cn.cleanup%3F.())%7D%2Cu%3De%3D%3E%7Bif(i)%7Be%3F.()%3Breturn%7Dtypeof%20e%3D%3D%60function%60%26%26(n.cleanup%3De)%2Ca.add(n)%2Cs()%7D%3Btry%7Blet%20n%3Dt.setup(%7Bsubscribe%3Ac%2CgetConfig%3Ae.getConfig%7D)%3Bn%26%26typeof%20n.then%3D%3D%60function%60%3Fn.then(u).catch(e%3D%3E%7Br.error(%60error%20setting%20up%20analytics%20destination%20%22%24%7Bt.name%7D%22%60%2C%7Bscope%3A%60analytics%60%2Cerror%3Ae%7D)%2Cl()%7D)%3Au(n)%7Dcatch(e)%7Br.error(%60error%20setting%20up%20analytics%20destination%20%22%24%7Bt.name%7D%22%60%2C%7Bscope%3A%60analytics%60%2Cerror%3Ae%7D)%2Cl()%7Dreturn%20l%7Dfunction%20u(r%2Co)%7Blet%20s%3D%7Bsequence%3At%2B%2B%2Cevent%3Ar%2Cpayload%3Ao%7D%3Bif(n%26%26(i.push(s)%2Ci.length%3E500%26%26i.shift())%2Ce.canTrack())for(let%20e%20of%20a)c(e%2Cs)%7Dfunction%20d()%7Bi.length%3D0%3Bfor(let%20e%20of%20a)e.subscriptions.clear()%2Ce.cleanup%3F.()%3Ba.clear()%2Co.clear()%7Dreturn%7BaddDestination%3Al%2ConPublish%3Au%2Creplay%3As%2Cdestroy%3Ad%7D%7Dlet%20u%3D%7BPAGE_VIEWED%3A%60page_viewed%60%2CPRODUCT_VIEWED%3A%60product_viewed%60%2CCOLLECTION_VIEWED%3A%60collection_viewed%60%2CCART_VIEWED%3A%60cart_viewed%60%2CSEARCH_VIEWED%3A%60search_viewed%60%2CCART_UPDATED%3A%60cart_updated%60%2CPRODUCT_ADD_TO_CART%3A%60product_added_to_cart%60%2CPRODUCT_REMOVED_FROM_CART%3A%60product_removed_from_cart%60%7D%2Cd%3D%60gid%3A%2F%2Fshopify%2FShop%2F%60%3Bfunction%20f(e)%7Breturn%20e.startsWith(d)%3Fe%3A%60%24%7Bd%7D%24%7Be%7D%60%7Dfunction%20p(e)%7Breturn%20e%3F%7B...e%2CshopId%3Af(e.shopId)%7D%3Anull%7Dlet%20m%3Dnew%20Set(%5Bu.PAGE_VIEWED%2Cu.PRODUCT_VIEWED%2Cu.COLLECTION_VIEWED%2Cu.CART_VIEWED%2Cu.SEARCH_VIEWED%5D)%2Ch%3Dnew%20Set(Object.values(u))%3Bfunction%20g()%7Bif(!(typeof%20window%3E%60u%60))return%20window.location.href%7Dfunction%20_(e%2Ct)%7Breturn%20Object.prototype.hasOwnProperty.call(e%2Ct)%7Dfunction%20v(e%2Ct)%7Breturn%20s(e)%3F_(e%2C%60shop%60)%3F%7B...e%2Cshop%3Ap(e.shop)%7D%3A%7B...e%2Cshop%3At%7D%3Ae%7Dfunction%20y(e%2Ct)%7Bif(!m.has(e)%7C%7C!s(t)%7C%7Ctypeof%20t.url%3D%3D%60string%60%26%26t.url.length%3E0)return%20t%3Blet%20n%3Dg()%3Breturn%20n%3F%7B...t%2Curl%3An%7D%3At%7Dfunction%20b()%7Btry%7Blet%20e%3Dwindow.Shopify%3F.customerPrivacy%3Bif(e%3F.consentStatus!%3D%3D%60loaded%60)return!1%3Blet%20t%3De.currentVisitorConsent%3F.()%3Breturn%20s(t)%26%26t.analytics%3D%3D%3D%60no%60%3F!1%3Ae%3F.analyticsProcessingAllowed%3F.()%3F%3F!1%7Dcatch%7Breturn!1%7D%7Dfunction%20x(e)%7Breturn%20typeof%20e%3D%3D%60string%60%26%26h.has(e)%7Dfunction%20S(e)%7Br.warn(%60unsupported%20analytics%20event%20%22%24%7BString(e)%7D%22%60%2C%7Bscope%3A%60analytics%60%7D)%7Dfunction%20C(e)%7Breturn!s(e)%7C%7Ce.analytics%3D%3D%3D%60%60%26%26e.marketing%3D%3D%3D%60%60%26%26e.preferences%3D%3D%3D%60%60%7Dfunction%20w()%7Btry%7Bif(!s(window.privacyBanner))return!1%3Blet%20e%3Dwindow.Shopify%3F.customerPrivacy%2Ct%3De%3F.shouldShowBanner%3Breturn%20typeof%20t%3D%3D%60function%60%3Ft()%26%26C(e%3F.currentVisitorConsent%3F.())%3A!0%7Dcatch%7Breturn!0%7D%7Dfunction%20T(e)%7Breturn%20e%3D%3D%3Dvoid%200%3F%7B%7D%3Ae%7Dfunction%20E(e)%7Bif(typeof%20window%3C%60u%60%26%26window.Shopify%3F.analytics)throw%20Error(%60Analytics%20bus%20already%20initialized.%20Only%20one%20setupStorefrontAnalytics()%20instance%20is%20allowed.%20Call%20destroy()%20first%20to%20re-initialize.%60)%3Blet%7Bconsent%3At%2CcustomData%3An%7D%3De%2Ci%3Dp(e.shop)%2Cs%3D!1%2Cc%3D!1%2Cu%3Dnew%20Map%2Cd%3D0%3Bfunction%20f()%7Breturn%7Bshop%3Ai%2Cconsent%3At%2CcustomData%3An%7D%7Dlet%20m%3Dl(%7BcanTrack%3A()%3D%3E!c%26%26b()%2CgetConfig%3Af%2CisSupportedEvent%3Ax%2CwarnUnsupportedEvent%3AS%7D)%3Bfunction%20h(e%2C...t)%7Bif(s)return%3Bif(!x(e))%7BS(e)%3Breturn%7Dlet%20n%3Dy(e%2Cv(T(t%5B0%5D)%2Ci))%3B(u.get(e)%3F%3Fnew%20Map).forEach((e%2Ct)%3D%3E%7Btry%7Be(n)%7Dcatch(e)%7Br.error(%60analytics%20publish%20error%60%2C%7Bscope%3A%60analytics%60%2Cerror%3Ae%2CsubscriberId%3At%7D)%7D%7D)%2Cm.onPublish(e%2Cn)%7Dfunction%20g(e%2Ct)%7Bif(!x(e))return%20S(e)%2C()%3D%3E%7B%7D%3Blet%20n%3Du.get(e)%3Bn%7C%7C(n%3Dnew%20Map%2Cu.set(e%2Cn))%3Blet%20r%3DString(d%2B%2B)%3Breturn%20n.set(r%2Ct)%2C()%3D%3E%7Bu.get(e)%3F.delete(r)%7D%7Dlet%20_%3Bfunction%20C()%7Bif(typeof%20document%3E%60u%60)return%3Blet%20e%3De%3D%3E%7Bif(s)return%3Blet%20t%3De.detail%3F.source%2Cn%3Dt%3D%3D%3D%60initial%60%26%26w()%3Bif(c%3Dn%2Cn)return%3Blet%20r%3Dt!%3D%3D%60initial%60%3Bm.replay(r)%7D%3Bdocument.addEventListener(a%2Ce)%2C_%3D()%3D%3E%7Bdocument.removeEventListener(a%2Ce)%7D%7Dfunction%20E()%7Bif(typeof%20window%3E%60u%60)return%3Blet%20e%3DO%2Ct%3Do()%3Bt%26%26(t.analytics%3De)%7Dfunction%20D()%7Bs%3D!0%2Cu.clear()%2Cm.destroy()%2C_%3F.()%2Ctypeof%20window%3C%60u%60%26%26window.Shopify%3F.analytics%3D%3D%3DO%26%26delete%20window.Shopify.analytics%7Dlet%20O%3D%7Bpublish%3Ah%2Csubscribe%3Ag%2CaddDestination%3Am.addDestination%2Cdestroy%3AD%2CgetConfig%3Af%7D%3Breturn%20i%3F.shopId%26%26String(i.shopId).endsWith(%60%2F68817551382%60)%26%26r.warn(%60mock%20shop%20detected%3B%20analytics%20will%20not%20work%20properly%60%2C%7Bscope%3A%60analytics%60%7D)%2CC()%2CE()%2CO%7Dfunction%20D(e)%7Be%26%26E(e)%7DD(__SHOPIFY_SCRIPT_DATA__)%7D)()%3B").replace("__SHOPIFY_SCRIPT_DATA__", () => (JSON.stringify(scriptData) ?? "undefined").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"));
4
+ const initializeShopifyAnalyticsBus = (scriptData) => decodeURIComponent("(function()%7Blet%20e%3D%7Btrace%3A%60debug%60%2Cdebug%3A%60debug%60%2Cinfo%3A%60info%60%2Cwarn%3A%60warn%60%2Cerror%3A%60error%60%2Cfatal%3A%60error%60%7D%3Bfunction%20t(e%2Ct)%7Breturn%60%5Bhydrogen%3A%24%7Be%7D%3A%24%7Bt%7D%5D%60%7Dfunction%20n(n%2Cr%2Ci)%7Blet%7Bscope%3Aa%2Cerror%3Ao%2C...s%7D%3Di%3F%3F%7B%7D%2Cc%3D%5Ba%3F%60%24%7Bt(n%2Ca)%7D%20%24%7Br%7D%60%3Ar%5D%3Bo!%3D%3Dvoid%200%26%26c.push(o)%2CObject.keys(s).length%3E0%26%26c.push(s)%2Cconsole%5Be%5Bn%5D%5D(...c)%7Dlet%20r%3D%7Btrace%3A(e%2Ct)%3D%3En(%60trace%60%2Ce%2Ct)%2Cdebug%3A(e%2Ct)%3D%3En(%60debug%60%2Ce%2Ct)%2Cinfo%3A(e%2Ct)%3D%3En(%60info%60%2Ce%2Ct)%2Cwarn%3A(e%2Ct)%3D%3En(%60warn%60%2Ce%2Ct)%2Cerror%3A(e%2Ct)%3D%3En(%60error%60%2Ce%2Ct)%2Cfatal%3A(e%2Ct)%3D%3En(%60fatal%60%2Ce%2Ct)%7D%2Ci%3D%60https%3A%2F%2Fcdn.shopify.com%60%3B%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%2C%60%24%7Bi%7D%60%3Blet%20a%3D%60consentTrackingApiLoaded%60%2Co%3D%60visitorConsentCollected%60%3Bfunction%20s()%7Bif(typeof%20window%3E%60u%60)return%3Blet%20e%3Dwindow%3Breturn%20e.Shopify%3F%3F%3D%7B%7D%7Dfunction%20c(e)%7Breturn%20typeof%20e%3D%3D%60object%60%26%26!!e%26%26!Array.isArray(e)%7Dfunction%20l(e%2Ct)%7Blet%20n%3De.subscriptions.get(t.event)%3Bif(!n%3F.size)%7Be.nextReplaySequence%3DMath.max(e.nextReplaySequence%2Ct.sequence%2B1)%3Breturn%7Dfor(let%20i%20of%20n)try%7Bi(t.payload)%7Dcatch(t)%7Br.error(%60error%20in%20analytics%20destination%20%22%24%7Be.name%7D%22%60%2C%7Bscope%3A%60analytics%60%2Cerror%3At%7D)%7De.nextReplaySequence%3DMath.max(e.nextReplaySequence%2Ct.sequence%2B1)%7Dfunction%20u(e)%7Blet%20t%3D0%2Cn%3D!0%2Ci%3D%5B%5D%2Ca%3Dnew%20Set%2Co%3Dnew%20Set%3Bfunction%20s(t%3D!1)%7Bif(!e.canTrack())%7Bt%26%26(i.length%3D0%2Cn%3D!1)%3Breturn%7Dn%3D!0%3Bfor(let%20e%20of%20a)for(let%20t%20of%20i)t.sequence%3Ce.nextReplaySequence%7C%7Cl(e%2Ct)%7Dfunction%20c(t)%7Bif(o.has(t.name))return%20r.warn(%60analytics%20destination%20%22%24%7Bt.name%7D%22%20is%20already%20registered%60%2C%7Bscope%3A%60analytics%60%7D)%2C()%3D%3E%7B%7D%3Bo.add(t.name)%3Blet%20n%3D%7Bname%3At.name%2Csubscriptions%3Anew%20Map%2CnextReplaySequence%3A0%7D%2Ci%3D!1%2Cc%3D(t%2Cr)%3D%3E%7Bif(i)return()%3D%3E%7B%7D%3Bif(e.isSupportedEvent%26%26!e.isSupportedEvent(t))return%20e.warnUnsupportedEvent%3F.(t)%2C()%3D%3E%7B%7D%3Blet%20a%3Dn.subscriptions.get(t)%3Breturn%20a%7C%7C(a%3Dnew%20Set%2Cn.subscriptions.set(t%2Ca))%2Ca.add(r)%2C()%3D%3E%7Blet%20e%3Dn.subscriptions.get(t)%3Be%3F.delete(r)%2Ce%3F.size%3D%3D%3D0%26%26n.subscriptions.delete(t)%7D%7D%2Cl%3D()%3D%3E%7Bi%7C%7C(i%3D!0%2Ca.delete(n)%2Co.delete(t.name)%2Cn.subscriptions.clear()%2Cn.cleanup%3F.())%7D%2Cu%3De%3D%3E%7Bif(i)%7Be%3F.()%3Breturn%7Dtypeof%20e%3D%3D%60function%60%26%26(n.cleanup%3De)%2Ca.add(n)%2Cs()%7D%3Btry%7Blet%20n%3Dt.setup(%7Bsubscribe%3Ac%2CgetConfig%3Ae.getConfig%7D)%3Bn%26%26typeof%20n.then%3D%3D%60function%60%3Fn.then(u).catch(e%3D%3E%7Br.error(%60error%20setting%20up%20analytics%20destination%20%22%24%7Bt.name%7D%22%60%2C%7Bscope%3A%60analytics%60%2Cerror%3Ae%7D)%2Cl()%7D)%3Au(n)%7Dcatch(e)%7Br.error(%60error%20setting%20up%20analytics%20destination%20%22%24%7Bt.name%7D%22%60%2C%7Bscope%3A%60analytics%60%2Cerror%3Ae%7D)%2Cl()%7Dreturn%20l%7Dfunction%20u(r%2Co)%7Blet%20s%3D%7Bsequence%3At%2B%2B%2Cevent%3Ar%2Cpayload%3Ao%7D%3Bif(n%26%26(i.push(s)%2Ci.length%3E500%26%26i.shift())%2Ce.canTrack())for(let%20e%20of%20a)l(e%2Cs)%7Dfunction%20d()%7Bi.length%3D0%3Bfor(let%20e%20of%20a)e.subscriptions.clear()%2Ce.cleanup%3F.()%3Ba.clear()%2Co.clear()%7Dreturn%7BaddDestination%3Ac%2ConPublish%3Au%2Creplay%3As%2Cdestroy%3Ad%7D%7Dlet%20d%3D%7BPAGE_VIEWED%3A%60page_viewed%60%2CPRODUCT_VIEWED%3A%60product_viewed%60%2CCOLLECTION_VIEWED%3A%60collection_viewed%60%2CCART_VIEWED%3A%60cart_viewed%60%2CSEARCH_VIEWED%3A%60search_viewed%60%2CCART_UPDATED%3A%60cart_updated%60%2CPRODUCT_ADD_TO_CART%3A%60product_added_to_cart%60%2CPRODUCT_REMOVED_FROM_CART%3A%60product_removed_from_cart%60%7D%2Cf%3D%60gid%3A%2F%2Fshopify%2FShop%2F%60%3Bfunction%20p(e)%7Breturn%20e.startsWith(f)%3Fe%3A%60%24%7Bf%7D%24%7Be%7D%60%7Dfunction%20m(e)%7Breturn%20e%3F%7B...e%2CshopId%3Ap(e.shopId)%7D%3Anull%7Dlet%20h%3Dnew%20Set(%5Bd.PAGE_VIEWED%2Cd.PRODUCT_VIEWED%2Cd.COLLECTION_VIEWED%2Cd.CART_VIEWED%2Cd.SEARCH_VIEWED%5D)%2Cg%3Dnew%20Set(Object.values(d))%3Bfunction%20_()%7Bif(!(typeof%20window%3E%60u%60))return%20window.location.href%7Dfunction%20v(e%2Ct)%7Breturn%20Object.prototype.hasOwnProperty.call(e%2Ct)%7Dfunction%20y(e%2Ct)%7Breturn%20c(e)%3Fv(e%2C%60shop%60)%3F%7B...e%2Cshop%3Am(e.shop)%7D%3A%7B...e%2Cshop%3At%7D%3Ae%7Dfunction%20b(e%2Ct)%7Bif(!h.has(e)%7C%7C!c(t)%7C%7Ctypeof%20t.url%3D%3D%60string%60%26%26t.url.length%3E0)return%20t%3Blet%20n%3D_()%3Breturn%20n%3F%7B...t%2Curl%3An%7D%3At%7Dfunction%20x()%7Btry%7Blet%20e%3Dwindow.Shopify%3F.customerPrivacy%3Bif(e%3F.consentStatus!%3D%3D%60loaded%60)return!1%3Blet%20t%3De.currentVisitorConsent%3F.()%3Breturn%20c(t)%26%26t.analytics%3D%3D%3D%60no%60%3F!1%3Ae%3F.analyticsProcessingAllowed%3F.()%3F%3F!1%7Dcatch%7Breturn!1%7D%7Dfunction%20S(e)%7Breturn%20typeof%20e%3D%3D%60string%60%26%26g.has(e)%7Dfunction%20C(e)%7Br.warn(%60unsupported%20analytics%20event%20%22%24%7BString(e)%7D%22%60%2C%7Bscope%3A%60analytics%60%7D)%7Dfunction%20w(e)%7Btry%7Bif(!e%26%26!c(window.privacyBanner))return!1%3Blet%20t%3Dwindow.Shopify%3F.customerPrivacy%3Breturn%20typeof%20t%3F.shouldShowGDPRBanner%3D%3D%60function%60%3Ft.shouldShowGDPRBanner()%3A!0%7Dcatch%7Breturn!0%7D%7Dfunction%20T(e)%7Breturn%20e%3D%3D%3Dvoid%200%3F%7B%7D%3Ae%7Dfunction%20E(e)%7Bif(typeof%20window%3C%60u%60%26%26window.Shopify%3F.analytics)throw%20Error(%60Analytics%20bus%20already%20initialized.%20Only%20one%20setupStorefrontAnalytics()%20instance%20is%20allowed.%20Call%20destroy()%20first%20to%20re-initialize.%60)%3Blet%7Bconsent%3At%2CcustomData%3An%7D%3De%2Ci%3Dt%3F.mode%3D%3D%3D%60default-banner%60%2Cc%3Dm(e.shop)%2Cl%3D!1%2Cd%3D!1%2Cf%3Dnew%20Map%2Cp%3D0%3Bfunction%20h()%7Breturn%7Bshop%3Ac%2Cconsent%3At%2CcustomData%3An%7D%7Dlet%20g%3Du(%7BcanTrack%3A()%3D%3E!d%26%26x()%2CgetConfig%3Ah%2CisSupportedEvent%3AS%2CwarnUnsupportedEvent%3AC%7D)%3Bfunction%20_(e%2C...t)%7Bif(l)return%3Bif(!S(e))%7BC(e)%3Breturn%7Dlet%20n%3Db(e%2Cy(T(t%5B0%5D)%2Cc))%3B(f.get(e)%3F%3Fnew%20Map).forEach((e%2Ct)%3D%3E%7Btry%7Be(n)%7Dcatch(e)%7Br.error(%60analytics%20publish%20error%60%2C%7Bscope%3A%60analytics%60%2Cerror%3Ae%2CsubscriberId%3At%7D)%7D%7D)%2Cg.onPublish(e%2Cn)%7Dfunction%20v(e%2Ct)%7Bif(!S(e))return%20C(e)%2C()%3D%3E%7B%7D%3Blet%20n%3Df.get(e)%3Bn%7C%7C(n%3Dnew%20Map%2Cf.set(e%2Cn))%3Blet%20r%3DString(p%2B%2B)%3Breturn%20n.set(r%2Ct)%2C()%3D%3E%7Bf.get(e)%3F.delete(r)%7D%7Dlet%20E%3Bfunction%20D()%7Bif(typeof%20document%3E%60u%60)return%3Blet%20e%3D()%3D%3E%7Bif(l)return%3Blet%20e%3Dw(i)%3Bd%3De%2C!e%26%26g.replay()%7D%2Ct%3D()%3D%3E%7Bl%7C%7C(d%3D!1%2Cg.replay(!0))%7D%3Bdocument.addEventListener(a%2Ce)%2Cdocument.addEventListener(o%2Ct)%2Cwindow.Shopify%3F.customerPrivacy%3F.consentStatus%3D%3D%3D%60loaded%60%26%26e()%2CE%3D()%3D%3E%7Bdocument.removeEventListener(a%2Ce)%2Cdocument.removeEventListener(o%2Ct)%7D%7Dfunction%20O()%7Bif(typeof%20window%3E%60u%60)return%3Blet%20e%3DA%2Ct%3Ds()%3Bt%26%26(t.analytics%3De)%7Dfunction%20k()%7Bl%3D!0%2Cf.clear()%2Cg.destroy()%2CE%3F.()%2Ctypeof%20window%3C%60u%60%26%26window.Shopify%3F.analytics%3D%3D%3DA%26%26delete%20window.Shopify.analytics%7Dlet%20A%3D%7Bpublish%3A_%2Csubscribe%3Av%2CaddDestination%3Ag.addDestination%2Cdestroy%3Ak%2CgetConfig%3Ah%7D%3Breturn%20c%3F.shopId%26%26String(c.shopId).endsWith(%60%2F68817551382%60)%26%26r.warn(%60mock%20shop%20detected%3B%20analytics%20will%20not%20work%20properly%60%2C%7Bscope%3A%60analytics%60%7D)%2CD()%2CO()%2CA%7Dfunction%20D(e)%7Be%26%26E(e)%7DD(__SHOPIFY_SCRIPT_DATA__)%7D)()%3B").replace("__SHOPIFY_SCRIPT_DATA__", () => (JSON.stringify(scriptData) ?? "undefined").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"));
5
5
  function getShopifyAnalyticsConfig({ analytics, consent, shop }) {
6
6
  const channel = analytics?.channel ?? "hydrogen";
7
7
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.mjs","names":[],"sources":["../../../src/core/shopify-scripts/analytics.ts"],"sourcesContent":["import initializeShopifyAnalyticsBus from \"../analytics/cdn/bootstrap\" with { type: \"script\" };\nimport type { StorefrontAnalyticsConfig } from \"../analytics/types\";\nimport type { ShopifyScriptTagsOptions } from \"./types\";\nimport { asInlineScript } from \"./utils/inline-script\";\n\nexport function getShopifyAnalyticsConfig({\n analytics,\n consent,\n shop,\n}: Pick<ShopifyScriptTagsOptions, \"analytics\" | \"consent\" | \"shop\">): StorefrontAnalyticsConfig {\n const channel = analytics?.channel ?? \"hydrogen\";\n\n return {\n shop:\n channel === \"headless\"\n ? { shopId: shop.shopId, channel }\n : { shopId: shop.shopId, storefrontId: shop.storefrontId, channel },\n consent: consent ?? {},\n customData: analytics?.customData,\n };\n}\n\nexport function getShopifyAnalyticsBusScript(analytics: StorefrontAnalyticsConfig): string {\n return asInlineScript(initializeShopifyAnalyticsBus)(analytics);\n}\n"],"mappings":";;;AAAA,MAAM,iCAA+B,eAAkB,mBAAgB,mpPAAuB,CAAA,QAAA,kCAAA,KAAA,UAAA,WAAA,IAAA,aAAA,QAAA,MAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA;AAK9F,SAAgB,0BAA0B,EACxC,WACA,SACA,QAC8F;CAC9F,MAAM,UAAU,WAAW,WAAW;AAEtC,QAAO;EACL,MACE,YAAY,aACR;GAAE,QAAQ,KAAK;GAAQ;GAAQ,GAC/B;GAAE,QAAQ,KAAK;GAAQ,cAAc,KAAK;GAAc;GAAS;EACvE,SAAS,WAAW,EAAE;EACtB,YAAY,WAAW;EACxB;;AAGH,SAAgB,6BAA6B,WAA8C;AACzF,QAAO,eAAe,8BAA8B,CAAC,UAAU"}
1
+ {"version":3,"file":"analytics.mjs","names":[],"sources":["../../../src/core/shopify-scripts/analytics.ts"],"sourcesContent":["import initializeShopifyAnalyticsBus from \"../analytics/cdn/bootstrap\" with { type: \"script\" };\nimport type { StorefrontAnalyticsConfig } from \"../analytics/types\";\nimport type { ShopifyScriptTagsOptions } from \"./types\";\nimport { asInlineScript } from \"./utils/inline-script\";\n\nexport function getShopifyAnalyticsConfig({\n analytics,\n consent,\n shop,\n}: Pick<ShopifyScriptTagsOptions, \"analytics\" | \"consent\" | \"shop\">): StorefrontAnalyticsConfig {\n const channel = analytics?.channel ?? \"hydrogen\";\n\n return {\n shop:\n channel === \"headless\"\n ? { shopId: shop.shopId, channel }\n : { shopId: shop.shopId, storefrontId: shop.storefrontId, channel },\n consent: consent ?? {},\n customData: analytics?.customData,\n };\n}\n\nexport function getShopifyAnalyticsBusScript(analytics: StorefrontAnalyticsConfig): string {\n return asInlineScript(initializeShopifyAnalyticsBus)(analytics);\n}\n"],"mappings":";;;AAAA,MAAM,iCAA+B,eAAkB,mBAAgB,ytPAAuB,CAAA,QAAA,kCAAA,KAAA,UAAA,WAAA,IAAA,aAAA,QAAA,MAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA;AAK9F,SAAgB,0BAA0B,EACxC,WACA,SACA,QAC8F;CAC9F,MAAM,UAAU,WAAW,WAAW;AAEtC,QAAO;EACL,MACE,YAAY,aACR;GAAE,QAAQ,KAAK;GAAQ;GAAQ,GAC/B;GAAE,QAAQ,KAAK;GAAQ,cAAc,KAAK;GAAc;GAAS;EACvE,SAAS,WAAW,EAAE;EACtB,YAAY,WAAW;EACxB;;AAGH,SAAgB,6BAA6B,WAA8C;AACzF,QAAO,eAAe,8BAA8B,CAAC,UAAU"}
@@ -5,6 +5,7 @@ const SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storef
5
5
  const SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/standard-events.js`;
6
6
  const SHOPIFY_STOREFRONT_STANDARD_EVENTS_INSPECTOR_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/standard-events-inspector.js`;
7
7
  const SHOPIFY_STOREFRONT_WEBMCP_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/webmcp.js`;
8
+ const SHOPIFY_ACCOUNT_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/web-components/account.js`;
8
9
  const SHOPIFY_INBOX_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/web-components/agent.js`;
9
10
  const SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/analytics/shopify.js`;
10
11
  const SHOPIFY_PERF_KIT_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/perf-kit/shopify-perf-kit-spa.min.js?v2`;
@@ -12,8 +13,9 @@ const SHOPIFY_CONSENT_API_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/consent-t
12
13
  const SHOPIFY_PRIVACY_BANNER_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/privacy-banner/storefront-banner.js`;
13
14
  const SHOPIFY_CONSENT_SCRIPT_ID = "shopify-consent";
14
15
  const SHOPIFY_PERF_KIT_SCRIPT_ID = "shopify-perfkit";
16
+ const CONSENT_TRACKING_API_LOADED_EVENT = "consentTrackingApiLoaded";
15
17
  const VISITOR_CONSENT_COLLECTED_EVENT = "visitorConsentCollected";
16
18
 
17
19
  //#endregion
18
- export { SHOPIFY_CDN_ORIGIN, SHOPIFY_CONSENT_API_SCRIPT, SHOPIFY_CONSENT_SCRIPT_ID, SHOPIFY_INBOX_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT_ID, SHOPIFY_PRIVACY_BANNER_SCRIPT, SHOPIFY_SHOP_APP_ORIGIN, SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT, SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT, SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT, SHOPIFY_STOREFRONT_WEBMCP_SCRIPT, VISITOR_CONSENT_COLLECTED_EVENT };
20
+ export { CONSENT_TRACKING_API_LOADED_EVENT, SHOPIFY_ACCOUNT_SCRIPT, SHOPIFY_CDN_ORIGIN, SHOPIFY_CONSENT_API_SCRIPT, SHOPIFY_CONSENT_SCRIPT_ID, SHOPIFY_INBOX_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT_ID, SHOPIFY_PRIVACY_BANNER_SCRIPT, SHOPIFY_SHOP_APP_ORIGIN, SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT, SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT, SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT, SHOPIFY_STOREFRONT_WEBMCP_SCRIPT, VISITOR_CONSENT_COLLECTED_EVENT };
19
21
  //# sourceMappingURL=constants.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.mjs","names":[],"sources":["../../../src/core/shopify-scripts/constants.ts"],"sourcesContent":["export const SHOPIFY_CDN_ORIGIN = \"https://cdn.shopify.com\";\nexport const SHOPIFY_SHOP_APP_ORIGIN = \"https://shop.app\";\nexport const SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/standard-actions.js`;\nexport const SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/standard-events.js`;\nexport const SHOPIFY_STOREFRONT_STANDARD_EVENTS_INSPECTOR_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/standard-events-inspector.js`;\nexport const SHOPIFY_STOREFRONT_WEBMCP_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/webmcp.js`;\nexport const SHOPIFY_INBOX_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/web-components/agent.js`;\n\nexport const SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/analytics/shopify.js`;\n\nexport const SHOPIFY_PERF_KIT_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/perf-kit/shopify-perf-kit-spa.min.js?v2`;\nexport const SHOPIFY_CONSENT_API_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js`;\nexport const SHOPIFY_PRIVACY_BANNER_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/privacy-banner/storefront-banner.js`;\n\nexport const SHOPIFY_CONSENT_SCRIPT_ID = \"shopify-consent\";\nexport const SHOPIFY_PERF_KIT_SCRIPT_ID = \"shopify-perfkit\";\n\nexport const VISITOR_CONSENT_COLLECTED_EVENT = \"visitorConsentCollected\";\n"],"mappings":";AAAA,MAAa,qBAAqB;AAClC,MAAa,0BAA0B;AACvC,MAAa,6CAA6C,GAAG,mBAAmB;AAChF,MAAa,4CAA4C,GAAG,mBAAmB;AAC/E,MAAa,sDAAsD,GAAG,mBAAmB;AACzF,MAAa,mCAAmC,GAAG,mBAAmB;AACtE,MAAa,uBAAuB,GAAG,mBAAmB;AAE1D,MAAa,sCAAsC,GAAG,mBAAmB;AAEzE,MAAa,0BAA0B,GAAG,mBAAmB;AAC7D,MAAa,6BAA6B,GAAG,mBAAmB;AAChE,MAAa,gCAAgC,GAAG,mBAAmB;AAEnE,MAAa,4BAA4B;AACzC,MAAa,6BAA6B;AAE1C,MAAa,kCAAkC"}
1
+ {"version":3,"file":"constants.mjs","names":[],"sources":["../../../src/core/shopify-scripts/constants.ts"],"sourcesContent":["export const SHOPIFY_CDN_ORIGIN = \"https://cdn.shopify.com\";\nexport const SHOPIFY_SHOP_APP_ORIGIN = \"https://shop.app\";\nexport const SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/standard-actions.js`;\nexport const SHOPIFY_STOREFRONT_STANDARD_EVENTS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/standard-events.js`;\nexport const SHOPIFY_STOREFRONT_STANDARD_EVENTS_INSPECTOR_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/standard-events-inspector.js`;\nexport const SHOPIFY_STOREFRONT_WEBMCP_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/webmcp.js`;\nexport const SHOPIFY_ACCOUNT_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/web-components/account.js`;\nexport const SHOPIFY_INBOX_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/web-components/agent.js`;\n\nexport const SHOPIFY_STOREFRONT_ANALYTICS_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/storefront/analytics/shopify.js`;\n\nexport const SHOPIFY_PERF_KIT_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/perf-kit/shopify-perf-kit-spa.min.js?v2`;\nexport const SHOPIFY_CONSENT_API_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js`;\nexport const SHOPIFY_PRIVACY_BANNER_SCRIPT = `${SHOPIFY_CDN_ORIGIN}/shopifycloud/privacy-banner/storefront-banner.js`;\n\nexport const SHOPIFY_CONSENT_SCRIPT_ID = \"shopify-consent\";\nexport const SHOPIFY_PERF_KIT_SCRIPT_ID = \"shopify-perfkit\";\n\nexport const CONSENT_TRACKING_API_LOADED_EVENT = \"consentTrackingApiLoaded\";\nexport const VISITOR_CONSENT_COLLECTED_EVENT = \"visitorConsentCollected\";\n"],"mappings":";AAAA,MAAa,qBAAqB;AAClC,MAAa,0BAA0B;AACvC,MAAa,6CAA6C,GAAG,mBAAmB;AAChF,MAAa,4CAA4C,GAAG,mBAAmB;AAC/E,MAAa,sDAAsD,GAAG,mBAAmB;AACzF,MAAa,mCAAmC,GAAG,mBAAmB;AACtE,MAAa,yBAAyB,GAAG,mBAAmB;AAC5D,MAAa,uBAAuB,GAAG,mBAAmB;AAE1D,MAAa,sCAAsC,GAAG,mBAAmB;AAEzE,MAAa,0BAA0B,GAAG,mBAAmB;AAC7D,MAAa,6BAA6B,GAAG,mBAAmB;AAChE,MAAa,gCAAgC,GAAG,mBAAmB;AAEnE,MAAa,4BAA4B;AACzC,MAAa,6BAA6B;AAE1C,MAAa,oCAAoC;AACjD,MAAa,kCAAkC"}
@@ -1,4 +1,4 @@
1
- import { VISITOR_CONSENT_COLLECTED_EVENT } from "./constants.mjs";
1
+ import { CONSENT_TRACKING_API_LOADED_EVENT, VISITOR_CONSENT_COLLECTED_EVENT } from "./constants.mjs";
2
2
  import { findWritableCookieDomain } from "./utils/cookie-domain.mjs";
3
3
  import { getTrackingValues } from "./utils/tracking-values.mjs";
4
4
  import { buildUUID } from "./utils/uuid.mjs";
@@ -77,6 +77,7 @@ function initializeDeprecatedCookies() {
77
77
  const syncConsent = () => {
78
78
  updateCookies(hasAnalyticsConsent(), domain);
79
79
  };
80
+ document.addEventListener(CONSENT_TRACKING_API_LOADED_EVENT, syncConsent);
80
81
  document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, syncConsent);
81
82
  if (isConsentLoaded()) syncConsent();
82
83
  const syncPageView = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"deprecated-cookies.mjs","names":[],"sources":["../../../src/core/shopify-scripts/deprecated-cookies.ts"],"sourcesContent":["/**\n * @deprecated This module manages the legacy `_shopify_y` and `_shopify_s`\n * JavaScript-visible cookies. Modern Shopify storefronts use http-only cookies\n * set by the Storefront API via the SFAPI proxy. This module exists only for\n * backward compatibility with downstream systems that may still read these\n * JS cookies directly.\n *\n * To remove: delete this file and remove the ShopifyScripts startup call.\n */\n\nimport { VISITOR_CONSENT_COLLECTED_EVENT } from \"./constants\";\nimport { findWritableCookieDomain } from \"./utils/cookie-domain\";\nimport { getTrackingValues } from \"./utils/tracking-values\";\nimport { buildUUID } from \"./utils/uuid\";\n\nconst SHOPIFY_Y = \"_shopify_y\";\nconst SHOPIFY_S = \"_shopify_s\";\n\nconst LONG_TERM_EXPIRY_IN_SECONDS = 60 * 60 * 24 * 360;\nconst SHORT_TERM_EXPIRY_IN_SECONDS = 60 * 30;\n\n/**\n * Computes the cookie domain as the broadest wildcard domain the browser will\n * accept for the consent domain. Returns empty string for localhost or when\n * probing fails (cookies use current domain).\n *\n * @deprecated Part of legacy cookie management.\n */\nexport function computeCookieDomain(consentDomain?: string): string {\n if (typeof window === \"undefined\") return \"\";\n\n return findWritableCookieDomain(consentDomain || window.location.host);\n}\n\nfunction setCookie(name: string, value: string, maxAgeInSeconds: number, domain: string): void {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n parts.push(`max-age=${maxAgeInSeconds}`);\n parts.push(\"path=/\");\n parts.push(\"SameSite=Lax\");\n if (domain) parts.push(`domain=${domain}`);\n document.cookie = parts.join(\"; \");\n}\n\n/**\n * @deprecated Sets or removes the legacy `_shopify_y` and `_shopify_s` cookies\n * based on the current consent state and tracking values.\n */\nfunction updateCookies(hasConsent: boolean, domain: string): void {\n if (hasConsent) {\n const trackingValues = getTrackingValues();\n const tokenValue = trackingValues.uniqueToken || trackingValues.visitToken || \"\";\n if (tokenValue.startsWith(\"00000000-\")) return;\n\n setCookie(\n SHOPIFY_Y,\n trackingValues.uniqueToken || buildUUID(),\n LONG_TERM_EXPIRY_IN_SECONDS,\n domain,\n );\n setCookie(\n SHOPIFY_S,\n trackingValues.visitToken || buildUUID(),\n SHORT_TERM_EXPIRY_IN_SECONDS,\n domain,\n );\n } else {\n setCookie(SHOPIFY_Y, \"\", 0, domain);\n setCookie(SHOPIFY_S, \"\", 0, domain);\n }\n}\n\nfunction hasAnalyticsConsent(): boolean {\n try {\n const privacy = window.Shopify?.customerPrivacy;\n return privacy?.analyticsProcessingAllowed?.() ?? false;\n } catch {\n return false;\n }\n}\n\nfunction isConsentLoaded(): boolean {\n return window.Shopify?.customerPrivacy?.consentStatus === \"loaded\";\n}\n\nfunction getConsentDomain(): string | undefined {\n return window.Shopify?.customerPrivacy?.config?.consentDomain;\n}\n\nlet didInitializeDeprecatedCookies = false;\nconst noOpCleanup = () => {};\n\n/**\n * @deprecated Starts page-lifetime legacy cookie management from ShopifyScripts.\n */\nexport function initializeDeprecatedCookies(): () => void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return noOpCleanup;\n if (didInitializeDeprecatedCookies) return noOpCleanup;\n\n didInitializeDeprecatedCookies = true;\n const domain = computeCookieDomain(getConsentDomain());\n\n const syncConsent = () => {\n updateCookies(hasAnalyticsConsent(), domain);\n };\n\n document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, syncConsent);\n if (isConsentLoaded()) syncConsent();\n\n const syncPageView = () => {\n if (hasAnalyticsConsent()) {\n updateCookies(true, domain);\n }\n };\n\n const subscribeToAnalytics = () => {\n const subscribe = window.Shopify?.analytics?.subscribe;\n if (typeof subscribe !== \"function\") return false;\n\n subscribe(\"page_viewed\", syncPageView);\n return true;\n };\n\n if (!subscribeToAnalytics() && document.readyState === \"loading\") {\n // The bus is inline today, but if it moves to a module/defer script it will\n // still be available before DOMContentLoaded.\n document.addEventListener(\"DOMContentLoaded\", subscribeToAnalytics, { once: true });\n }\n\n return noOpCleanup;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAM,YAAY;AAClB,MAAM,YAAY;AAElB,MAAM,8BAA8B,OAAU,KAAK;AACnD,MAAM,+BAA+B;;;;;;;;AASrC,SAAgB,oBAAoB,eAAgC;AAClE,KAAI,OAAO,WAAW,YAAa,QAAO;AAE1C,QAAO,yBAAyB,iBAAiB,OAAO,SAAS,KAAK;;AAGxE,SAAS,UAAU,MAAc,OAAe,iBAAyB,QAAsB;CAC7F,MAAM,QAAQ,CAAC,GAAG,KAAK,GAAG,mBAAmB,MAAM,GAAG;AACtD,OAAM,KAAK,WAAW,kBAAkB;AACxC,OAAM,KAAK,SAAS;AACpB,OAAM,KAAK,eAAe;AAC1B,KAAI,OAAQ,OAAM,KAAK,UAAU,SAAS;AAC1C,UAAS,SAAS,MAAM,KAAK,KAAK;;;;;;AAOpC,SAAS,cAAc,YAAqB,QAAsB;AAChE,KAAI,YAAY;EACd,MAAM,iBAAiB,mBAAmB;AAE1C,OADmB,eAAe,eAAe,eAAe,cAAc,IAC/D,WAAW,YAAY,CAAE;AAExC,YACE,WACA,eAAe,eAAe,WAAW,EACzC,6BACA,OACD;AACD,YACE,WACA,eAAe,cAAc,WAAW,EACxC,8BACA,OACD;QACI;AACL,YAAU,WAAW,IAAI,GAAG,OAAO;AACnC,YAAU,WAAW,IAAI,GAAG,OAAO;;;AAIvC,SAAS,sBAA+B;AACtC,KAAI;AAEF,UADgB,OAAO,SAAS,kBAChB,8BAA8B,IAAI;SAC5C;AACN,SAAO;;;AAIX,SAAS,kBAA2B;AAClC,QAAO,OAAO,SAAS,iBAAiB,kBAAkB;;AAG5D,SAAS,mBAAuC;AAC9C,QAAO,OAAO,SAAS,iBAAiB,QAAQ;;AAGlD,IAAI,iCAAiC;AACrC,MAAM,oBAAoB;;;;AAK1B,SAAgB,8BAA0C;AACxD,KAAI,OAAO,WAAW,eAAe,OAAO,aAAa,YAAa,QAAO;AAC7E,KAAI,+BAAgC,QAAO;AAE3C,kCAAiC;CACjC,MAAM,SAAS,oBAAoB,kBAAkB,CAAC;CAEtD,MAAM,oBAAoB;AACxB,gBAAc,qBAAqB,EAAE,OAAO;;AAG9C,UAAS,iBAAiB,iCAAiC,YAAY;AACvE,KAAI,iBAAiB,CAAE,cAAa;CAEpC,MAAM,qBAAqB;AACzB,MAAI,qBAAqB,CACvB,eAAc,MAAM,OAAO;;CAI/B,MAAM,6BAA6B;EACjC,MAAM,YAAY,OAAO,SAAS,WAAW;AAC7C,MAAI,OAAO,cAAc,WAAY,QAAO;AAE5C,YAAU,eAAe,aAAa;AACtC,SAAO;;AAGT,KAAI,CAAC,sBAAsB,IAAI,SAAS,eAAe,UAGrD,UAAS,iBAAiB,oBAAoB,sBAAsB,EAAE,MAAM,MAAM,CAAC;AAGrF,QAAO"}
1
+ {"version":3,"file":"deprecated-cookies.mjs","names":[],"sources":["../../../src/core/shopify-scripts/deprecated-cookies.ts"],"sourcesContent":["/**\n * @deprecated This module manages the legacy `_shopify_y` and `_shopify_s`\n * JavaScript-visible cookies. Modern Shopify storefronts use http-only cookies\n * set by the Storefront API via the SFAPI proxy. This module exists only for\n * backward compatibility with downstream systems that may still read these\n * JS cookies directly.\n *\n * To remove: delete this file and remove the ShopifyScripts startup call.\n */\n\nimport { CONSENT_TRACKING_API_LOADED_EVENT, VISITOR_CONSENT_COLLECTED_EVENT } from \"./constants\";\nimport { findWritableCookieDomain } from \"./utils/cookie-domain\";\nimport { getTrackingValues } from \"./utils/tracking-values\";\nimport { buildUUID } from \"./utils/uuid\";\n\nconst SHOPIFY_Y = \"_shopify_y\";\nconst SHOPIFY_S = \"_shopify_s\";\n\nconst LONG_TERM_EXPIRY_IN_SECONDS = 60 * 60 * 24 * 360;\nconst SHORT_TERM_EXPIRY_IN_SECONDS = 60 * 30;\n\n/**\n * Computes the cookie domain as the broadest wildcard domain the browser will\n * accept for the consent domain. Returns empty string for localhost or when\n * probing fails (cookies use current domain).\n *\n * @deprecated Part of legacy cookie management.\n */\nexport function computeCookieDomain(consentDomain?: string): string {\n if (typeof window === \"undefined\") return \"\";\n\n return findWritableCookieDomain(consentDomain || window.location.host);\n}\n\nfunction setCookie(name: string, value: string, maxAgeInSeconds: number, domain: string): void {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n parts.push(`max-age=${maxAgeInSeconds}`);\n parts.push(\"path=/\");\n parts.push(\"SameSite=Lax\");\n if (domain) parts.push(`domain=${domain}`);\n document.cookie = parts.join(\"; \");\n}\n\n/**\n * @deprecated Sets or removes the legacy `_shopify_y` and `_shopify_s` cookies\n * based on the current consent state and tracking values.\n */\nfunction updateCookies(hasConsent: boolean, domain: string): void {\n if (hasConsent) {\n const trackingValues = getTrackingValues();\n const tokenValue = trackingValues.uniqueToken || trackingValues.visitToken || \"\";\n if (tokenValue.startsWith(\"00000000-\")) return;\n\n setCookie(\n SHOPIFY_Y,\n trackingValues.uniqueToken || buildUUID(),\n LONG_TERM_EXPIRY_IN_SECONDS,\n domain,\n );\n setCookie(\n SHOPIFY_S,\n trackingValues.visitToken || buildUUID(),\n SHORT_TERM_EXPIRY_IN_SECONDS,\n domain,\n );\n } else {\n setCookie(SHOPIFY_Y, \"\", 0, domain);\n setCookie(SHOPIFY_S, \"\", 0, domain);\n }\n}\n\nfunction hasAnalyticsConsent(): boolean {\n try {\n const privacy = window.Shopify?.customerPrivacy;\n return privacy?.analyticsProcessingAllowed?.() ?? false;\n } catch {\n return false;\n }\n}\n\nfunction isConsentLoaded(): boolean {\n return window.Shopify?.customerPrivacy?.consentStatus === \"loaded\";\n}\n\nfunction getConsentDomain(): string | undefined {\n return window.Shopify?.customerPrivacy?.config?.consentDomain;\n}\n\nlet didInitializeDeprecatedCookies = false;\nconst noOpCleanup = () => {};\n\n/**\n * @deprecated Starts page-lifetime legacy cookie management from ShopifyScripts.\n */\nexport function initializeDeprecatedCookies(): () => void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return noOpCleanup;\n if (didInitializeDeprecatedCookies) return noOpCleanup;\n\n didInitializeDeprecatedCookies = true;\n const domain = computeCookieDomain(getConsentDomain());\n\n const syncConsent = () => {\n updateCookies(hasAnalyticsConsent(), domain);\n };\n\n document.addEventListener(CONSENT_TRACKING_API_LOADED_EVENT, syncConsent);\n document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, syncConsent);\n if (isConsentLoaded()) syncConsent();\n\n const syncPageView = () => {\n if (hasAnalyticsConsent()) {\n updateCookies(true, domain);\n }\n };\n\n const subscribeToAnalytics = () => {\n const subscribe = window.Shopify?.analytics?.subscribe;\n if (typeof subscribe !== \"function\") return false;\n\n subscribe(\"page_viewed\", syncPageView);\n return true;\n };\n\n if (!subscribeToAnalytics() && document.readyState === \"loading\") {\n // The bus is inline today, but if it moves to a module/defer script it will\n // still be available before DOMContentLoaded.\n document.addEventListener(\"DOMContentLoaded\", subscribeToAnalytics, { once: true });\n }\n\n return noOpCleanup;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAM,YAAY;AAClB,MAAM,YAAY;AAElB,MAAM,8BAA8B,OAAU,KAAK;AACnD,MAAM,+BAA+B;;;;;;;;AASrC,SAAgB,oBAAoB,eAAgC;AAClE,KAAI,OAAO,WAAW,YAAa,QAAO;AAE1C,QAAO,yBAAyB,iBAAiB,OAAO,SAAS,KAAK;;AAGxE,SAAS,UAAU,MAAc,OAAe,iBAAyB,QAAsB;CAC7F,MAAM,QAAQ,CAAC,GAAG,KAAK,GAAG,mBAAmB,MAAM,GAAG;AACtD,OAAM,KAAK,WAAW,kBAAkB;AACxC,OAAM,KAAK,SAAS;AACpB,OAAM,KAAK,eAAe;AAC1B,KAAI,OAAQ,OAAM,KAAK,UAAU,SAAS;AAC1C,UAAS,SAAS,MAAM,KAAK,KAAK;;;;;;AAOpC,SAAS,cAAc,YAAqB,QAAsB;AAChE,KAAI,YAAY;EACd,MAAM,iBAAiB,mBAAmB;AAE1C,OADmB,eAAe,eAAe,eAAe,cAAc,IAC/D,WAAW,YAAY,CAAE;AAExC,YACE,WACA,eAAe,eAAe,WAAW,EACzC,6BACA,OACD;AACD,YACE,WACA,eAAe,cAAc,WAAW,EACxC,8BACA,OACD;QACI;AACL,YAAU,WAAW,IAAI,GAAG,OAAO;AACnC,YAAU,WAAW,IAAI,GAAG,OAAO;;;AAIvC,SAAS,sBAA+B;AACtC,KAAI;AAEF,UADgB,OAAO,SAAS,kBAChB,8BAA8B,IAAI;SAC5C;AACN,SAAO;;;AAIX,SAAS,kBAA2B;AAClC,QAAO,OAAO,SAAS,iBAAiB,kBAAkB;;AAG5D,SAAS,mBAAuC;AAC9C,QAAO,OAAO,SAAS,iBAAiB,QAAQ;;AAGlD,IAAI,iCAAiC;AACrC,MAAM,oBAAoB;;;;AAK1B,SAAgB,8BAA0C;AACxD,KAAI,OAAO,WAAW,eAAe,OAAO,aAAa,YAAa,QAAO;AAC7E,KAAI,+BAAgC,QAAO;AAE3C,kCAAiC;CACjC,MAAM,SAAS,oBAAoB,kBAAkB,CAAC;CAEtD,MAAM,oBAAoB;AACxB,gBAAc,qBAAqB,EAAE,OAAO;;AAG9C,UAAS,iBAAiB,mCAAmC,YAAY;AACzE,UAAS,iBAAiB,iCAAiC,YAAY;AACvE,KAAI,iBAAiB,CAAE,cAAa;CAEpC,MAAM,qBAAqB;AACzB,MAAI,qBAAqB,CACvB,eAAc,MAAM,OAAO;;CAI/B,MAAM,6BAA6B;EACjC,MAAM,YAAY,OAAO,SAAS,WAAW;AAC7C,MAAI,OAAO,cAAc,WAAY,QAAO;AAE5C,YAAU,eAAe,aAAa;AACtC,SAAO;;AAGT,KAAI,CAAC,sBAAsB,IAAI,SAAS,eAAe,UAGrD,UAAS,iBAAiB,oBAAoB,sBAAsB,EAAE,MAAM,MAAM,CAAC;AAGrF,QAAO"}