@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.
@@ -443,7 +443,8 @@ function useCustomerPrivacy(props) {
443
443
  react.useEffect(() => {
444
444
  if (observing.current.customerPrivacy) return;
445
445
  observing.current.customerPrivacy = true;
446
- let customCustomerPrivacy = null;
446
+ let backendConsentStub = null;
447
+ let fullCustomerPrivacy = null;
447
448
  let customShopify = window.Shopify || void 0;
448
449
  Object.defineProperty(window, "Shopify", {
449
450
  configurable: true,
@@ -453,15 +454,16 @@ function useCustomerPrivacy(props) {
453
454
  set(value) {
454
455
  if (typeof value === "object" && value !== null && Object.keys(value).length === 0) {
455
456
  customShopify = value;
457
+ backendConsentStub = { backendConsentEnabled: true };
456
458
  Object.defineProperty(window.Shopify, "customerPrivacy", {
457
459
  configurable: true,
458
460
  get() {
459
- return customCustomerPrivacy;
461
+ return fullCustomerPrivacy ?? backendConsentStub;
460
462
  },
461
463
  set(value2) {
462
464
  if (typeof value2 === "object" && value2 !== null && "setTrackingConsent" in value2) {
463
465
  const customerPrivacy = value2;
464
- customCustomerPrivacy = {
466
+ fullCustomerPrivacy = {
465
467
  ...customerPrivacy,
466
468
  // Note: this method is not used by the privacy-banner,
467
469
  // it bundles its own setTrackingConsent.
@@ -471,7 +473,7 @@ function useCustomerPrivacy(props) {
471
473
  };
472
474
  customShopify = {
473
475
  ...customShopify,
474
- customerPrivacy: customCustomerPrivacy
476
+ customerPrivacy: fullCustomerPrivacy
475
477
  };
476
478
  setLoaded.customerPrivacy();
477
479
  }
@@ -596,7 +598,8 @@ function overridePrivacyBannerMethods({
596
598
  }
597
599
  function getCustomerPrivacy() {
598
600
  try {
599
- return window.Shopify && window.Shopify.customerPrivacy ? window.Shopify?.customerPrivacy : null;
601
+ const cp = window.Shopify?.customerPrivacy;
602
+ return cp && "setTrackingConsent" in cp ? cp : null;
600
603
  } catch (e) {
601
604
  return null;
602
605
  }
@@ -610,7 +613,7 @@ function getPrivacyBanner() {
610
613
  }
611
614
 
612
615
  // package.json
613
- var version = "2026.1.3";
616
+ var version = "2026.4.0";
614
617
 
615
618
  // src/analytics-manager/ShopifyAnalytics.tsx
616
619
  function getCustomerPrivacyRequired() {
@@ -1133,7 +1136,7 @@ function register(key) {
1133
1136
  }
1134
1137
  function shopifyCanTrack() {
1135
1138
  try {
1136
- return window.Shopify.customerPrivacy.analyticsProcessingAllowed();
1139
+ return window.Shopify.customerPrivacy?.analyticsProcessingAllowed?.() ?? false;
1137
1140
  } catch (e) {
1138
1141
  }
1139
1142
  return false;
@@ -1326,6 +1329,7 @@ function getStorefrontHeaders(request) {
1326
1329
  };
1327
1330
  }
1328
1331
  var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
1332
+ var MCP_RE = /^\/api\/mcp$/;
1329
1333
  var getSafePathname = (url) => {
1330
1334
  try {
1331
1335
  return new URL(url, "http://e.c").pathname;
@@ -2005,7 +2009,7 @@ function generateUUID() {
2005
2009
  }
2006
2010
 
2007
2011
  // src/version.ts
2008
- var LIB_VERSION = "2026.1.3";
2012
+ var LIB_VERSION = "2026.4.0";
2009
2013
 
2010
2014
  // src/utils/graphql.ts
2011
2015
  function minifyQuery(string) {
@@ -2430,6 +2434,70 @@ function createStorefrontClient(options) {
2430
2434
  );
2431
2435
  return new Response(sfapiResponse.body, sfapiResponse);
2432
2436
  },
2437
+ /**
2438
+ * Checks if the request is targeting the Storefront MCP endpoint.
2439
+ */
2440
+ isMcpUrl(request) {
2441
+ return MCP_RE.test(getSafePathname(request.url ?? ""));
2442
+ },
2443
+ /**
2444
+ * Forwards the request to the Storefront MCP endpoint.
2445
+ * CORS headers are intentionally omitted — the Storefront MCP
2446
+ * server is server-to-server only (OPTIONS preflight returns 404).
2447
+ */
2448
+ async forwardMcp(request) {
2449
+ const forwardedHeaders = new Headers([
2450
+ ...extractHeaders(
2451
+ (key) => request.headers.get(key),
2452
+ [
2453
+ "accept",
2454
+ "accept-encoding",
2455
+ "accept-language",
2456
+ "content-type",
2457
+ "cookie",
2458
+ "origin",
2459
+ "referer",
2460
+ "user-agent"
2461
+ ]
2462
+ ),
2463
+ ...extractHeaders(
2464
+ (key) => defaultHeaders[key],
2465
+ [
2466
+ SHOPIFY_CLIENT_IP_HEADER,
2467
+ SHOPIFY_CLIENT_IP_SIG_HEADER,
2468
+ STOREFRONT_ACCESS_TOKEN_HEADER,
2469
+ STOREFRONT_REQUEST_GROUP_ID_HEADER,
2470
+ hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER
2471
+ ]
2472
+ )
2473
+ ]);
2474
+ if (storefrontHeaders?.buyerIp) {
2475
+ forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
2476
+ }
2477
+ const mcpUrl = `${getShopifyDomain()}/api/mcp`;
2478
+ try {
2479
+ const mcpResponse = await fetch(mcpUrl, {
2480
+ method: request.method,
2481
+ body: request.body,
2482
+ headers: forwardedHeaders
2483
+ });
2484
+ return new Response(mcpResponse.body, mcpResponse);
2485
+ } catch (error) {
2486
+ const JSON_RPC_INTERNAL_ERROR = -32603;
2487
+ const message = error instanceof Error ? error.message : "Internal proxy error";
2488
+ return new Response(
2489
+ JSON.stringify({
2490
+ jsonrpc: "2.0",
2491
+ error: { code: JSON_RPC_INTERNAL_ERROR, message },
2492
+ id: null
2493
+ }),
2494
+ {
2495
+ status: 502,
2496
+ headers: { "content-type": "application/json" }
2497
+ }
2498
+ );
2499
+ }
2500
+ },
2433
2501
  setCollectedSubrequestHeaders: (response) => {
2434
2502
  if (collectedSubrequestHeaders) {
2435
2503
  for (const value of collectedSubrequestHeaders.setCookie) {
@@ -3596,7 +3664,7 @@ var hydrogenContext = {
3596
3664
  };
3597
3665
 
3598
3666
  // src/customer/constants.ts
3599
- var DEFAULT_CUSTOMER_API_VERSION = "2026-01";
3667
+ var DEFAULT_CUSTOMER_API_VERSION = "2026-04";
3600
3668
  var USER_AGENT = `Shopify Hydrogen ${LIB_VERSION}`;
3601
3669
  var CUSTOMER_API_CLIENT_ID = "30243aa5-17c1-465a-8493-944bcc4e88aa";
3602
3670
  var CUSTOMER_ACCOUNT_SESSION_KEY = "customerAccount";
@@ -3923,29 +3991,32 @@ function createCustomerAccountHelper(customerApiVersion, shopId) {
3923
3991
 
3924
3992
  // src/customer/customer.ts
3925
3993
  var HYDROGEN_TUNNEL_DOMAIN_SUFFIX = ".tryhydrogen.dev";
3926
- function throwIfNotTunnelled(hostname) {
3927
- {
3928
- if (!hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) {
3929
- throw new Response(
3930
- [
3931
- "Customer Account API OAuth requires a Hydrogen tunnel in local development.",
3932
- "Run the development server with the `--customer-account-push` flag,",
3933
- `then open the tunnel URL shown in your terminal (\`https://*${HYDROGEN_TUNNEL_DOMAIN_SUFFIX}\`) instead of localhost.`
3934
- ].join("\n\n"),
3935
- {
3936
- status: 400,
3937
- headers: {
3938
- "Content-Type": "text/plain; charset=utf-8"
3939
- }
3940
- }
3941
- );
3942
- }
3994
+ function checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri) {
3995
+ if (hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) return;
3996
+ if (useCustomAuthDomain) {
3997
+ const redirectHint = redirectUri ? ` (${redirectUri})` : "";
3998
+ warnOnce(
3999
+ `[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.`
4000
+ );
4001
+ return;
3943
4002
  }
4003
+ throw new Response(
4004
+ [
4005
+ "Customer Account API OAuth requires a Hydrogen tunnel in local development.",
4006
+ "Run the development server with the `--customer-account-push` flag,",
4007
+ `then open the tunnel URL shown in your terminal (\`https://*${HYDROGEN_TUNNEL_DOMAIN_SUFFIX}\`) instead of localhost.`
4008
+ ].join("\n\n"),
4009
+ {
4010
+ status: 400,
4011
+ headers: {
4012
+ "Content-Type": "text/plain; charset=utf-8"
4013
+ }
4014
+ }
4015
+ );
3944
4016
  }
3945
4017
  function defaultAuthStatusHandler(request, defaultLoginUrl) {
3946
4018
  if (!request.url) return defaultLoginUrl;
3947
- const { hostname, pathname } = new URL(request.url);
3948
- throwIfNotTunnelled(hostname);
4019
+ const { pathname } = new URL(request.url);
3949
4020
  const cleanedPathname = pathname.replace(/\.data$/, "").replace(/\/_root$/, "/").replace(/(.+)\/$/, "$1");
3950
4021
  const redirectTo = defaultLoginUrl + `?${new URLSearchParams({ return_to: cleanedPathname }).toString()}`;
3951
4022
  return redirect(redirectTo);
@@ -3963,7 +4034,8 @@ function createCustomerAccountClient({
3963
4034
  loginPath = "/account/login",
3964
4035
  authorizePath = "/account/authorize",
3965
4036
  defaultRedirectPath = "/account",
3966
- language
4037
+ language,
4038
+ useCustomAuthDomain
3967
4039
  }) {
3968
4040
  if (customerApiVersion !== DEFAULT_CUSTOMER_API_VERSION) {
3969
4041
  console.warn(
@@ -3980,7 +4052,6 @@ function createCustomerAccountClient({
3980
4052
  "[h2:error:createCustomerAccountClient] The request object does not contain a URL."
3981
4053
  );
3982
4054
  }
3983
- const authStatusHandler = customAuthStatusHandler ? customAuthStatusHandler : () => defaultAuthStatusHandler(request, loginPath);
3984
4055
  const requestUrl = new URL(request.url);
3985
4056
  const httpsOrigin = requestUrl.protocol === "http:" ? requestUrl.origin.replace("http", "https") : requestUrl.origin;
3986
4057
  const redirectUri = ensureLocalRedirectUrl({
@@ -3988,6 +4059,11 @@ function createCustomerAccountClient({
3988
4059
  defaultUrl: authorizePath,
3989
4060
  redirectUrl: authUrl
3990
4061
  });
4062
+ const ensureTunnel = (hostname) => checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri);
4063
+ const authStatusHandler = customAuthStatusHandler ? customAuthStatusHandler : () => {
4064
+ ensureTunnel(requestUrl.hostname);
4065
+ return defaultAuthStatusHandler(request, loginPath);
4066
+ };
3991
4067
  const getCustomerAccountUrl = createCustomerAccountHelper(
3992
4068
  customerApiVersion,
3993
4069
  shopId
@@ -4109,7 +4185,7 @@ function createCustomerAccountClient({
4109
4185
  return session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.accessToken;
4110
4186
  }
4111
4187
  async function mutate(mutation, options) {
4112
- throwIfNotTunnelled(requestUrl.hostname);
4188
+ ensureTunnel(requestUrl.hostname);
4113
4189
  ifInvalidCredentialThrowError();
4114
4190
  mutation = minifyQuery(mutation);
4115
4191
  assertMutation(mutation, "customer.mutate");
@@ -4119,7 +4195,7 @@ function createCustomerAccountClient({
4119
4195
  );
4120
4196
  }
4121
4197
  async function query(query2, options) {
4122
- throwIfNotTunnelled(requestUrl.hostname);
4198
+ ensureTunnel(requestUrl.hostname);
4123
4199
  ifInvalidCredentialThrowError();
4124
4200
  query2 = minifyQuery(query2);
4125
4201
  assertQuery(query2, "customer.query");
@@ -4143,7 +4219,7 @@ function createCustomerAccountClient({
4143
4219
  return {
4144
4220
  i18n: { language: language ?? "EN" },
4145
4221
  login: async (options) => {
4146
- throwIfNotTunnelled(requestUrl.hostname);
4222
+ ensureTunnel(requestUrl.hostname);
4147
4223
  ifInvalidCredentialThrowError();
4148
4224
  const loginUrl = new URL(getCustomerAccountUrl("AUTH" /* AUTH */));
4149
4225
  const state = generateState();
@@ -4195,7 +4271,7 @@ function createCustomerAccountClient({
4195
4271
  return redirect(loginUrl.toString());
4196
4272
  },
4197
4273
  logout: async (options) => {
4198
- throwIfNotTunnelled(requestUrl.hostname);
4274
+ ensureTunnel(requestUrl.hostname);
4199
4275
  ifInvalidCredentialThrowError();
4200
4276
  const idToken = session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.idToken;
4201
4277
  const postLogoutRedirectUri = ensureLocalRedirectUrl({
@@ -4230,7 +4306,7 @@ function createCustomerAccountClient({
4230
4306
  mutate,
4231
4307
  query,
4232
4308
  authorize: async () => {
4233
- throwIfNotTunnelled(requestUrl.hostname);
4309
+ ensureTunnel(requestUrl.hostname);
4234
4310
  ifInvalidCredentialThrowError();
4235
4311
  const code = requestUrl.searchParams.get("code");
4236
4312
  const state = requestUrl.searchParams.get("state");
@@ -4436,6 +4512,7 @@ function createHydrogenContext(options, additionalContext) {
4436
4512
  customerApiVersion: customerAccountOptions?.apiVersion,
4437
4513
  authUrl: customerAccountOptions?.authUrl,
4438
4514
  customAuthStatusHandler: customerAccountOptions?.customAuthStatusHandler,
4515
+ useCustomAuthDomain: customerAccountOptions?.useCustomAuthDomain,
4439
4516
  // locale - i18n.language is a union of StorefrontLanguageCode | CustomerLanguageCode
4440
4517
  // We cast here because createCustomerAccountClient expects CustomerLanguageCode specifically,
4441
4518
  // but the union type is compatible since most language codes overlap between the two APIs
@@ -4514,8 +4591,7 @@ function createRequestHandler({
4514
4591
  mode,
4515
4592
  poweredByHeader = true,
4516
4593
  getLoadContext,
4517
- collectTrackingInformation = true,
4518
- proxyStandardRoutes = true
4594
+ collectTrackingInformation = true
4519
4595
  }) {
4520
4596
  const handleRequest = reactRouter.createRequestHandler(build, mode);
4521
4597
  const appendPoweredByHeader = poweredByHeader ? (response) => response.headers.append("powered-by", "Shopify, Hydrogen") : void 0;
@@ -4537,27 +4613,28 @@ function createRequestHandler({
4537
4613
  }
4538
4614
  const context = await getLoadContext?.(request);
4539
4615
  const storefront = context?.storefront || context?.get?.(storefrontContext);
4540
- if (proxyStandardRoutes) {
4541
- if (!storefront) {
4542
- warnOnce(
4543
- "[h2:createRequestHandler] Storefront instance is required to proxy standard routes."
4544
- );
4545
- }
4546
- if (storefront?.isStorefrontApiUrl(request)) {
4547
- const response2 = await storefront.forward(request);
4548
- appendPoweredByHeader?.(response2);
4549
- return response2;
4550
- }
4616
+ if (!storefront) {
4617
+ throw new Error(
4618
+ "[h2:createRequestHandler] Storefront instance is required in the load context. Make sure to use createHydrogenContext() or provide a storefront instance via getLoadContext."
4619
+ );
4620
+ }
4621
+ if (storefront.isStorefrontApiUrl(request)) {
4622
+ const response2 = await storefront.forward(request);
4623
+ appendPoweredByHeader?.(response2);
4624
+ return response2;
4625
+ }
4626
+ if (storefront.isMcpUrl(request)) {
4627
+ const response2 = await storefront.forwardMcp(request);
4628
+ appendPoweredByHeader?.(response2);
4629
+ return response2;
4551
4630
  }
4552
4631
  const response = await handleRequest(request, context);
4553
- if (storefront && proxyStandardRoutes) {
4554
- if (collectTrackingInformation) {
4555
- storefront.setCollectedSubrequestHeaders(response);
4556
- }
4557
- const fetchDest = request.headers.get("sec-fetch-dest");
4558
- if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
4559
- appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
4560
- }
4632
+ if (collectTrackingInformation) {
4633
+ storefront.setCollectedSubrequestHeaders(response);
4634
+ }
4635
+ const fetchDest = request.headers.get("sec-fetch-dest");
4636
+ if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
4637
+ appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
4561
4638
  }
4562
4639
  appendPoweredByHeader?.(response);
4563
4640
  return response;
@@ -6597,14 +6674,14 @@ var QUERIES = {
6597
6674
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartNoteUpdate
6598
6675
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartSelectedDeliveryOptionsUpdate
6599
6676
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartMetafieldsSet
6600
- //! @see https://shopify.dev/docs/api/storefront/2026-01/mutations/cartMetafieldDelete
6677
+ //! @see https://shopify.dev/docs/api/storefront/2026-04/mutations/cartMetafieldDelete
6601
6678
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartGiftCardCodesUpdate
6602
6679
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartGiftCardCodesAdd
6603
6680
  //! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartGiftCardCodesRemove
6604
6681
  //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesAdd
6605
6682
  //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesRemove
6606
6683
  //! @see: https://shopify.dev/docs/api/storefront/latest/mutations/cartDeliveryAddressesUpdate
6607
- //! @see: https://shopify.dev/docs/api/storefront/2026-01/mutations/cartDeliveryAddressesReplace
6684
+ //! @see: https://shopify.dev/docs/api/storefront/2026-04/mutations/cartDeliveryAddressesReplace
6608
6685
 
6609
6686
  Object.defineProperty(exports, "AnalyticsEventName", {
6610
6687
  enumerable: true,