@shopify/hydrogen 2026.1.3 → 2026.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -267,7 +267,8 @@ function useCustomerPrivacy(props) {
267
267
  useEffect(() => {
268
268
  if (observing.current.customerPrivacy) return;
269
269
  observing.current.customerPrivacy = true;
270
- let customCustomerPrivacy = null;
270
+ let backendConsentStub = null;
271
+ let fullCustomerPrivacy = null;
271
272
  let customShopify = window.Shopify || void 0;
272
273
  Object.defineProperty(window, "Shopify", {
273
274
  configurable: true,
@@ -277,15 +278,16 @@ function useCustomerPrivacy(props) {
277
278
  set(value) {
278
279
  if (typeof value === "object" && value !== null && Object.keys(value).length === 0) {
279
280
  customShopify = value;
281
+ backendConsentStub = { backendConsentEnabled: true };
280
282
  Object.defineProperty(window.Shopify, "customerPrivacy", {
281
283
  configurable: true,
282
284
  get() {
283
- return customCustomerPrivacy;
285
+ return fullCustomerPrivacy ?? backendConsentStub;
284
286
  },
285
287
  set(value2) {
286
288
  if (typeof value2 === "object" && value2 !== null && "setTrackingConsent" in value2) {
287
289
  const customerPrivacy = value2;
288
- customCustomerPrivacy = {
290
+ fullCustomerPrivacy = {
289
291
  ...customerPrivacy,
290
292
  // Note: this method is not used by the privacy-banner,
291
293
  // it bundles its own setTrackingConsent.
@@ -295,7 +297,7 @@ function useCustomerPrivacy(props) {
295
297
  };
296
298
  customShopify = {
297
299
  ...customShopify,
298
- customerPrivacy: customCustomerPrivacy
300
+ customerPrivacy: fullCustomerPrivacy
299
301
  };
300
302
  setLoaded.customerPrivacy();
301
303
  }
@@ -420,7 +422,8 @@ function overridePrivacyBannerMethods({
420
422
  }
421
423
  function getCustomerPrivacy() {
422
424
  try {
423
- return window.Shopify && window.Shopify.customerPrivacy ? window.Shopify?.customerPrivacy : null;
425
+ const cp = window.Shopify?.customerPrivacy;
426
+ return cp && "setTrackingConsent" in cp ? cp : null;
424
427
  } catch (e) {
425
428
  return null;
426
429
  }
@@ -434,7 +437,7 @@ function getPrivacyBanner() {
434
437
  }
435
438
 
436
439
  // package.json
437
- var version = "2026.1.3";
440
+ var version = "2026.4.0";
438
441
 
439
442
  // src/analytics-manager/ShopifyAnalytics.tsx
440
443
  function getCustomerPrivacyRequired() {
@@ -957,7 +960,7 @@ function register(key) {
957
960
  }
958
961
  function shopifyCanTrack() {
959
962
  try {
960
- return window.Shopify.customerPrivacy.analyticsProcessingAllowed();
963
+ return window.Shopify.customerPrivacy?.analyticsProcessingAllowed?.() ?? false;
961
964
  } catch (e) {
962
965
  }
963
966
  return false;
@@ -1150,6 +1153,7 @@ function getStorefrontHeaders(request) {
1150
1153
  };
1151
1154
  }
1152
1155
  var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
1156
+ var MCP_RE = /^\/api\/mcp$/;
1153
1157
  var getSafePathname = (url) => {
1154
1158
  try {
1155
1159
  return new URL(url, "http://e.c").pathname;
@@ -1829,7 +1833,7 @@ function generateUUID() {
1829
1833
  }
1830
1834
 
1831
1835
  // src/version.ts
1832
- var LIB_VERSION = "2026.1.3";
1836
+ var LIB_VERSION = "2026.4.0";
1833
1837
 
1834
1838
  // src/utils/graphql.ts
1835
1839
  function minifyQuery(string) {
@@ -2254,6 +2258,70 @@ function createStorefrontClient(options) {
2254
2258
  );
2255
2259
  return new Response(sfapiResponse.body, sfapiResponse);
2256
2260
  },
2261
+ /**
2262
+ * Checks if the request is targeting the Storefront MCP endpoint.
2263
+ */
2264
+ isMcpUrl(request) {
2265
+ return MCP_RE.test(getSafePathname(request.url ?? ""));
2266
+ },
2267
+ /**
2268
+ * Forwards the request to the Storefront MCP endpoint.
2269
+ * CORS headers are intentionally omitted — the Storefront MCP
2270
+ * server is server-to-server only (OPTIONS preflight returns 404).
2271
+ */
2272
+ async forwardMcp(request) {
2273
+ const forwardedHeaders = new Headers([
2274
+ ...extractHeaders(
2275
+ (key) => request.headers.get(key),
2276
+ [
2277
+ "accept",
2278
+ "accept-encoding",
2279
+ "accept-language",
2280
+ "content-type",
2281
+ "cookie",
2282
+ "origin",
2283
+ "referer",
2284
+ "user-agent"
2285
+ ]
2286
+ ),
2287
+ ...extractHeaders(
2288
+ (key) => defaultHeaders[key],
2289
+ [
2290
+ SHOPIFY_CLIENT_IP_HEADER,
2291
+ SHOPIFY_CLIENT_IP_SIG_HEADER,
2292
+ STOREFRONT_ACCESS_TOKEN_HEADER,
2293
+ STOREFRONT_REQUEST_GROUP_ID_HEADER,
2294
+ SHOPIFY_STOREFRONT_ID_HEADER
2295
+ ]
2296
+ )
2297
+ ]);
2298
+ if (storefrontHeaders?.buyerIp) {
2299
+ forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
2300
+ }
2301
+ const mcpUrl = `${getShopifyDomain()}/api/mcp`;
2302
+ try {
2303
+ const mcpResponse = await fetch(mcpUrl, {
2304
+ method: request.method,
2305
+ body: request.body,
2306
+ headers: forwardedHeaders
2307
+ });
2308
+ return new Response(mcpResponse.body, mcpResponse);
2309
+ } catch (error) {
2310
+ const JSON_RPC_INTERNAL_ERROR = -32603;
2311
+ const message = error instanceof Error ? error.message : "Internal proxy error";
2312
+ return new Response(
2313
+ JSON.stringify({
2314
+ jsonrpc: "2.0",
2315
+ error: { code: JSON_RPC_INTERNAL_ERROR, message },
2316
+ id: null
2317
+ }),
2318
+ {
2319
+ status: 502,
2320
+ headers: { "content-type": "application/json" }
2321
+ }
2322
+ );
2323
+ }
2324
+ },
2257
2325
  setCollectedSubrequestHeaders: (response) => {
2258
2326
  if (collectedSubrequestHeaders) {
2259
2327
  for (const value of collectedSubrequestHeaders.setCookie) {
@@ -3420,7 +3488,7 @@ var hydrogenContext = {
3420
3488
  };
3421
3489
 
3422
3490
  // src/customer/constants.ts
3423
- var DEFAULT_CUSTOMER_API_VERSION = "2026-01";
3491
+ var DEFAULT_CUSTOMER_API_VERSION = "2026-04";
3424
3492
  var USER_AGENT = `Shopify Hydrogen ${LIB_VERSION}`;
3425
3493
  var CUSTOMER_API_CLIENT_ID = "30243aa5-17c1-465a-8493-944bcc4e88aa";
3426
3494
  var CUSTOMER_ACCOUNT_SESSION_KEY = "customerAccount";
@@ -3747,29 +3815,32 @@ function createCustomerAccountHelper(customerApiVersion, shopId) {
3747
3815
 
3748
3816
  // src/customer/customer.ts
3749
3817
  var HYDROGEN_TUNNEL_DOMAIN_SUFFIX = ".tryhydrogen.dev";
3750
- function throwIfNotTunnelled(hostname) {
3751
- {
3752
- if (!hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) {
3753
- throw new Response(
3754
- [
3755
- "Customer Account API OAuth requires a Hydrogen tunnel in local development.",
3756
- "Run the development server with the `--customer-account-push` flag,",
3757
- `then open the tunnel URL shown in your terminal (\`https://*${HYDROGEN_TUNNEL_DOMAIN_SUFFIX}\`) instead of localhost.`
3758
- ].join("\n\n"),
3759
- {
3760
- status: 400,
3761
- headers: {
3762
- "Content-Type": "text/plain; charset=utf-8"
3763
- }
3764
- }
3765
- );
3766
- }
3818
+ function checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri) {
3819
+ if (hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) return;
3820
+ if (useCustomAuthDomain) {
3821
+ const redirectHint = redirectUri ? ` (${redirectUri})` : "";
3822
+ warnOnce(
3823
+ `[h2:warn:customerAccount] You are using a custom domain (${hostname}) instead of a Hydrogen dev tunnel. Make sure you have manually registered your redirect_uri${redirectHint} in your Customer Account API settings in the Shopify admin. See https://shopify.dev/docs/api/customer for details.`
3824
+ );
3825
+ return;
3767
3826
  }
3827
+ throw new Response(
3828
+ [
3829
+ "Customer Account API OAuth requires a Hydrogen tunnel in local development.",
3830
+ "Run the development server with the `--customer-account-push` flag,",
3831
+ `then open the tunnel URL shown in your terminal (\`https://*${HYDROGEN_TUNNEL_DOMAIN_SUFFIX}\`) instead of localhost.`
3832
+ ].join("\n\n"),
3833
+ {
3834
+ status: 400,
3835
+ headers: {
3836
+ "Content-Type": "text/plain; charset=utf-8"
3837
+ }
3838
+ }
3839
+ );
3768
3840
  }
3769
3841
  function defaultAuthStatusHandler(request, defaultLoginUrl) {
3770
3842
  if (!request.url) return defaultLoginUrl;
3771
- const { hostname, pathname } = new URL(request.url);
3772
- throwIfNotTunnelled(hostname);
3843
+ const { pathname } = new URL(request.url);
3773
3844
  const cleanedPathname = pathname.replace(/\.data$/, "").replace(/\/_root$/, "/").replace(/(.+)\/$/, "$1");
3774
3845
  const redirectTo = defaultLoginUrl + `?${new URLSearchParams({ return_to: cleanedPathname }).toString()}`;
3775
3846
  return redirect(redirectTo);
@@ -3787,7 +3858,8 @@ function createCustomerAccountClient({
3787
3858
  loginPath = "/account/login",
3788
3859
  authorizePath = "/account/authorize",
3789
3860
  defaultRedirectPath = "/account",
3790
- language
3861
+ language,
3862
+ useCustomAuthDomain
3791
3863
  }) {
3792
3864
  if (customerApiVersion !== DEFAULT_CUSTOMER_API_VERSION) {
3793
3865
  console.warn(
@@ -3804,7 +3876,6 @@ function createCustomerAccountClient({
3804
3876
  "[h2:error:createCustomerAccountClient] The request object does not contain a URL."
3805
3877
  );
3806
3878
  }
3807
- const authStatusHandler = customAuthStatusHandler ? customAuthStatusHandler : () => defaultAuthStatusHandler(request, loginPath);
3808
3879
  const requestUrl = new URL(request.url);
3809
3880
  const httpsOrigin = requestUrl.protocol === "http:" ? requestUrl.origin.replace("http", "https") : requestUrl.origin;
3810
3881
  const redirectUri = ensureLocalRedirectUrl({
@@ -3812,6 +3883,11 @@ function createCustomerAccountClient({
3812
3883
  defaultUrl: authorizePath,
3813
3884
  redirectUrl: authUrl
3814
3885
  });
3886
+ const ensureTunnel = (hostname) => checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri);
3887
+ const authStatusHandler = customAuthStatusHandler ? customAuthStatusHandler : () => {
3888
+ ensureTunnel(requestUrl.hostname);
3889
+ return defaultAuthStatusHandler(request, loginPath);
3890
+ };
3815
3891
  const getCustomerAccountUrl = createCustomerAccountHelper(
3816
3892
  customerApiVersion,
3817
3893
  shopId
@@ -3933,7 +4009,7 @@ function createCustomerAccountClient({
3933
4009
  return session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.accessToken;
3934
4010
  }
3935
4011
  async function mutate(mutation, options) {
3936
- throwIfNotTunnelled(requestUrl.hostname);
4012
+ ensureTunnel(requestUrl.hostname);
3937
4013
  ifInvalidCredentialThrowError();
3938
4014
  mutation = minifyQuery(mutation);
3939
4015
  assertMutation(mutation, "customer.mutate");
@@ -3943,7 +4019,7 @@ function createCustomerAccountClient({
3943
4019
  );
3944
4020
  }
3945
4021
  async function query(query2, options) {
3946
- throwIfNotTunnelled(requestUrl.hostname);
4022
+ ensureTunnel(requestUrl.hostname);
3947
4023
  ifInvalidCredentialThrowError();
3948
4024
  query2 = minifyQuery(query2);
3949
4025
  assertQuery(query2, "customer.query");
@@ -3967,7 +4043,7 @@ function createCustomerAccountClient({
3967
4043
  return {
3968
4044
  i18n: { language: language ?? "EN" },
3969
4045
  login: async (options) => {
3970
- throwIfNotTunnelled(requestUrl.hostname);
4046
+ ensureTunnel(requestUrl.hostname);
3971
4047
  ifInvalidCredentialThrowError();
3972
4048
  const loginUrl = new URL(getCustomerAccountUrl("AUTH" /* AUTH */));
3973
4049
  const state = generateState();
@@ -4019,7 +4095,7 @@ function createCustomerAccountClient({
4019
4095
  return redirect(loginUrl.toString());
4020
4096
  },
4021
4097
  logout: async (options) => {
4022
- throwIfNotTunnelled(requestUrl.hostname);
4098
+ ensureTunnel(requestUrl.hostname);
4023
4099
  ifInvalidCredentialThrowError();
4024
4100
  const idToken = session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.idToken;
4025
4101
  const postLogoutRedirectUri = ensureLocalRedirectUrl({
@@ -4054,7 +4130,7 @@ function createCustomerAccountClient({
4054
4130
  mutate,
4055
4131
  query,
4056
4132
  authorize: async () => {
4057
- throwIfNotTunnelled(requestUrl.hostname);
4133
+ ensureTunnel(requestUrl.hostname);
4058
4134
  ifInvalidCredentialThrowError();
4059
4135
  const code = requestUrl.searchParams.get("code");
4060
4136
  const state = requestUrl.searchParams.get("state");
@@ -4260,6 +4336,7 @@ function createHydrogenContext(options, additionalContext) {
4260
4336
  customerApiVersion: customerAccountOptions?.apiVersion,
4261
4337
  authUrl: customerAccountOptions?.authUrl,
4262
4338
  customAuthStatusHandler: customerAccountOptions?.customAuthStatusHandler,
4339
+ useCustomAuthDomain: customerAccountOptions?.useCustomAuthDomain,
4263
4340
  // locale - i18n.language is a union of StorefrontLanguageCode | CustomerLanguageCode
4264
4341
  // We cast here because createCustomerAccountClient expects CustomerLanguageCode specifically,
4265
4342
  // but the union type is compatible since most language codes overlap between the two APIs
@@ -4338,8 +4415,7 @@ function createRequestHandler({
4338
4415
  mode,
4339
4416
  poweredByHeader = true,
4340
4417
  getLoadContext,
4341
- collectTrackingInformation = true,
4342
- proxyStandardRoutes = true
4418
+ collectTrackingInformation = true
4343
4419
  }) {
4344
4420
  const handleRequest = createRequestHandler$1(build, mode);
4345
4421
  const appendPoweredByHeader = poweredByHeader ? (response) => response.headers.append("powered-by", "Shopify, Hydrogen") : void 0;
@@ -4361,27 +4437,28 @@ function createRequestHandler({
4361
4437
  }
4362
4438
  const context = await getLoadContext?.(request);
4363
4439
  const storefront = context?.storefront || context?.get?.(storefrontContext);
4364
- if (proxyStandardRoutes) {
4365
- if (!storefront) {
4366
- warnOnce(
4367
- "[h2:createRequestHandler] Storefront instance is required to proxy standard routes."
4368
- );
4369
- }
4370
- if (storefront?.isStorefrontApiUrl(request)) {
4371
- const response2 = await storefront.forward(request);
4372
- appendPoweredByHeader?.(response2);
4373
- return response2;
4374
- }
4440
+ if (!storefront) {
4441
+ throw new Error(
4442
+ "[h2:createRequestHandler] Storefront instance is required in the load context. Make sure to use createHydrogenContext() or provide a storefront instance via getLoadContext."
4443
+ );
4444
+ }
4445
+ if (storefront.isStorefrontApiUrl(request)) {
4446
+ const response2 = await storefront.forward(request);
4447
+ appendPoweredByHeader?.(response2);
4448
+ return response2;
4449
+ }
4450
+ if (storefront.isMcpUrl(request)) {
4451
+ const response2 = await storefront.forwardMcp(request);
4452
+ appendPoweredByHeader?.(response2);
4453
+ return response2;
4375
4454
  }
4376
4455
  const response = await handleRequest(request, context);
4377
- if (storefront && proxyStandardRoutes) {
4378
- if (collectTrackingInformation) {
4379
- storefront.setCollectedSubrequestHeaders(response);
4380
- }
4381
- const fetchDest = request.headers.get("sec-fetch-dest");
4382
- if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
4383
- appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
4384
- }
4456
+ if (collectTrackingInformation) {
4457
+ storefront.setCollectedSubrequestHeaders(response);
4458
+ }
4459
+ const fetchDest = request.headers.get("sec-fetch-dest");
4460
+ if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
4461
+ appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
4385
4462
  }
4386
4463
  appendPoweredByHeader?.(response);
4387
4464
  return response;
@@ -6421,14 +6498,14 @@ var QUERIES = {
6421
6498
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartNoteUpdate
6422
6499
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartSelectedDeliveryOptionsUpdate
6423
6500
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartMetafieldsSet
6424
- //! @see https://shopify.dev/docs/api/storefront/2026-01/mutations/cartMetafieldDelete
6501
+ //! @see https://shopify.dev/docs/api/storefront/2026-04/mutations/cartMetafieldDelete
6425
6502
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartGiftCardCodesUpdate
6426
6503
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartGiftCardCodesAdd
6427
6504
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartGiftCardCodesRemove
6428
6505
  //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesAdd
6429
6506
  //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesRemove
6430
6507
  //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesUpdate
6431
- //! @see: https://shopify.dev/docs/api/storefront/2026-01/mutations/cartDeliveryAddressesReplace
6508
+ //! @see: https://shopify.dev/docs/api/storefront/2026-04/mutations/cartDeliveryAddressesReplace
6432
6509
 
6433
6510
  export { Analytics, AnalyticsEvent, CacheCustom, CacheLong, CacheNone, CacheShort, CartForm, InMemoryCache, NonceProvider, OptimisticInput, Pagination, RichText, Script, Seo, ShopPayButton, VariantSelector, cartAttributesUpdateDefault, cartBuyerIdentityUpdateDefault, cartCreateDefault, cartDiscountCodesUpdateDefault, cartGetDefault, cartGetIdDefault, cartGiftCardCodesAddDefault, cartGiftCardCodesRemoveDefault, cartGiftCardCodesUpdateDefault, cartLinesAddDefault, cartLinesRemoveDefault, cartLinesUpdateDefault, cartMetafieldDeleteDefault, cartMetafieldsSetDefault, cartNoteUpdateDefault, cartSelectedDeliveryOptionsUpdateDefault, cartSetIdDefault, changelogHandler, createCartHandler, createContentSecurityPolicy, createCustomerAccountClient, createHydrogenContext, createRequestHandler, createStorefrontClient, createWithCache, formatAPIResult, generateCacheControlHeader, getPaginationVariables, getSelectedProductOptions, getSeoMeta, getShopAnalytics, getSitemap, getSitemapIndex, graphiqlLoader, hydrogenContext, hydrogenPreset, hydrogenRoutes, storefrontRedirect, useAnalytics, useCustomerPrivacy, useNonce, useOptimisticCart, useOptimisticData, useOptimisticVariant };
6434
6511
  //# sourceMappingURL=index.js.map