@shopify/hydrogen 2026.1.2 → 2026.1.4

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.
@@ -434,7 +434,7 @@ function getPrivacyBanner() {
434
434
  }
435
435
 
436
436
  // package.json
437
- var version = "2026.1.2";
437
+ var version = "2026.1.4";
438
438
 
439
439
  // src/analytics-manager/ShopifyAnalytics.tsx
440
440
  function getCustomerPrivacyRequired() {
@@ -1150,6 +1150,7 @@ function getStorefrontHeaders(request) {
1150
1150
  };
1151
1151
  }
1152
1152
  var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
1153
+ var MCP_RE = /^\/api\/mcp$/;
1153
1154
  var getSafePathname = (url) => {
1154
1155
  try {
1155
1156
  return new URL(url, "http://e.c").pathname;
@@ -1829,7 +1830,7 @@ function generateUUID() {
1829
1830
  }
1830
1831
 
1831
1832
  // src/version.ts
1832
- var LIB_VERSION = "2026.1.2";
1833
+ var LIB_VERSION = "2026.1.4";
1833
1834
 
1834
1835
  // src/utils/graphql.ts
1835
1836
  function minifyQuery(string) {
@@ -2254,6 +2255,70 @@ function createStorefrontClient(options) {
2254
2255
  );
2255
2256
  return new Response(sfapiResponse.body, sfapiResponse);
2256
2257
  },
2258
+ /**
2259
+ * Checks if the request is targeting the Storefront MCP endpoint.
2260
+ */
2261
+ isMcpUrl(request) {
2262
+ return MCP_RE.test(getSafePathname(request.url ?? ""));
2263
+ },
2264
+ /**
2265
+ * Forwards the request to the Storefront MCP endpoint.
2266
+ * CORS headers are intentionally omitted — the Storefront MCP
2267
+ * server is server-to-server only (OPTIONS preflight returns 404).
2268
+ */
2269
+ async forwardMcp(request) {
2270
+ const forwardedHeaders = new Headers([
2271
+ ...extractHeaders(
2272
+ (key) => request.headers.get(key),
2273
+ [
2274
+ "accept",
2275
+ "accept-encoding",
2276
+ "accept-language",
2277
+ "content-type",
2278
+ "cookie",
2279
+ "origin",
2280
+ "referer",
2281
+ "user-agent"
2282
+ ]
2283
+ ),
2284
+ ...extractHeaders(
2285
+ (key) => defaultHeaders[key],
2286
+ [
2287
+ SHOPIFY_CLIENT_IP_HEADER,
2288
+ SHOPIFY_CLIENT_IP_SIG_HEADER,
2289
+ STOREFRONT_ACCESS_TOKEN_HEADER,
2290
+ STOREFRONT_REQUEST_GROUP_ID_HEADER,
2291
+ SHOPIFY_STOREFRONT_ID_HEADER
2292
+ ]
2293
+ )
2294
+ ]);
2295
+ if (storefrontHeaders?.buyerIp) {
2296
+ forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
2297
+ }
2298
+ const mcpUrl = `${getShopifyDomain()}/api/mcp`;
2299
+ try {
2300
+ const mcpResponse = await fetch(mcpUrl, {
2301
+ method: request.method,
2302
+ body: request.body,
2303
+ headers: forwardedHeaders
2304
+ });
2305
+ return new Response(mcpResponse.body, mcpResponse);
2306
+ } catch (error) {
2307
+ const JSON_RPC_INTERNAL_ERROR = -32603;
2308
+ const message = error instanceof Error ? error.message : "Internal proxy error";
2309
+ return new Response(
2310
+ JSON.stringify({
2311
+ jsonrpc: "2.0",
2312
+ error: { code: JSON_RPC_INTERNAL_ERROR, message },
2313
+ id: null
2314
+ }),
2315
+ {
2316
+ status: 502,
2317
+ headers: { "content-type": "application/json" }
2318
+ }
2319
+ );
2320
+ }
2321
+ },
2257
2322
  setCollectedSubrequestHeaders: (response) => {
2258
2323
  if (collectedSubrequestHeaders) {
2259
2324
  for (const value of collectedSubrequestHeaders.setCookie) {
@@ -3747,29 +3812,32 @@ function createCustomerAccountHelper(customerApiVersion, shopId) {
3747
3812
 
3748
3813
  // src/customer/customer.ts
3749
3814
  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
- }
3815
+ function checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri) {
3816
+ if (hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) return;
3817
+ if (useCustomAuthDomain) {
3818
+ const redirectHint = redirectUri ? ` (${redirectUri})` : "";
3819
+ warnOnce(
3820
+ `[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.`
3821
+ );
3822
+ return;
3767
3823
  }
3824
+ throw new Response(
3825
+ [
3826
+ "Customer Account API OAuth requires a Hydrogen tunnel in local development.",
3827
+ "Run the development server with the `--customer-account-push` flag,",
3828
+ `then open the tunnel URL shown in your terminal (\`https://*${HYDROGEN_TUNNEL_DOMAIN_SUFFIX}\`) instead of localhost.`
3829
+ ].join("\n\n"),
3830
+ {
3831
+ status: 400,
3832
+ headers: {
3833
+ "Content-Type": "text/plain; charset=utf-8"
3834
+ }
3835
+ }
3836
+ );
3768
3837
  }
3769
3838
  function defaultAuthStatusHandler(request, defaultLoginUrl) {
3770
3839
  if (!request.url) return defaultLoginUrl;
3771
- const { hostname, pathname } = new URL(request.url);
3772
- throwIfNotTunnelled(hostname);
3840
+ const { pathname } = new URL(request.url);
3773
3841
  const cleanedPathname = pathname.replace(/\.data$/, "").replace(/\/_root$/, "/").replace(/(.+)\/$/, "$1");
3774
3842
  const redirectTo = defaultLoginUrl + `?${new URLSearchParams({ return_to: cleanedPathname }).toString()}`;
3775
3843
  return redirect(redirectTo);
@@ -3787,7 +3855,8 @@ function createCustomerAccountClient({
3787
3855
  loginPath = "/account/login",
3788
3856
  authorizePath = "/account/authorize",
3789
3857
  defaultRedirectPath = "/account",
3790
- language
3858
+ language,
3859
+ useCustomAuthDomain
3791
3860
  }) {
3792
3861
  if (customerApiVersion !== DEFAULT_CUSTOMER_API_VERSION) {
3793
3862
  console.warn(
@@ -3804,7 +3873,6 @@ function createCustomerAccountClient({
3804
3873
  "[h2:error:createCustomerAccountClient] The request object does not contain a URL."
3805
3874
  );
3806
3875
  }
3807
- const authStatusHandler = customAuthStatusHandler ? customAuthStatusHandler : () => defaultAuthStatusHandler(request, loginPath);
3808
3876
  const requestUrl = new URL(request.url);
3809
3877
  const httpsOrigin = requestUrl.protocol === "http:" ? requestUrl.origin.replace("http", "https") : requestUrl.origin;
3810
3878
  const redirectUri = ensureLocalRedirectUrl({
@@ -3812,6 +3880,11 @@ function createCustomerAccountClient({
3812
3880
  defaultUrl: authorizePath,
3813
3881
  redirectUrl: authUrl
3814
3882
  });
3883
+ const ensureTunnel = (hostname) => checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri);
3884
+ const authStatusHandler = customAuthStatusHandler ? customAuthStatusHandler : () => {
3885
+ ensureTunnel(requestUrl.hostname);
3886
+ return defaultAuthStatusHandler(request, loginPath);
3887
+ };
3815
3888
  const getCustomerAccountUrl = createCustomerAccountHelper(
3816
3889
  customerApiVersion,
3817
3890
  shopId
@@ -3933,7 +4006,7 @@ function createCustomerAccountClient({
3933
4006
  return session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.accessToken;
3934
4007
  }
3935
4008
  async function mutate(mutation, options) {
3936
- throwIfNotTunnelled(requestUrl.hostname);
4009
+ ensureTunnel(requestUrl.hostname);
3937
4010
  ifInvalidCredentialThrowError();
3938
4011
  mutation = minifyQuery(mutation);
3939
4012
  assertMutation(mutation, "customer.mutate");
@@ -3943,7 +4016,7 @@ function createCustomerAccountClient({
3943
4016
  );
3944
4017
  }
3945
4018
  async function query(query2, options) {
3946
- throwIfNotTunnelled(requestUrl.hostname);
4019
+ ensureTunnel(requestUrl.hostname);
3947
4020
  ifInvalidCredentialThrowError();
3948
4021
  query2 = minifyQuery(query2);
3949
4022
  assertQuery(query2, "customer.query");
@@ -3967,7 +4040,7 @@ function createCustomerAccountClient({
3967
4040
  return {
3968
4041
  i18n: { language: language ?? "EN" },
3969
4042
  login: async (options) => {
3970
- throwIfNotTunnelled(requestUrl.hostname);
4043
+ ensureTunnel(requestUrl.hostname);
3971
4044
  ifInvalidCredentialThrowError();
3972
4045
  const loginUrl = new URL(getCustomerAccountUrl("AUTH" /* AUTH */));
3973
4046
  const state = generateState();
@@ -4019,7 +4092,7 @@ function createCustomerAccountClient({
4019
4092
  return redirect(loginUrl.toString());
4020
4093
  },
4021
4094
  logout: async (options) => {
4022
- throwIfNotTunnelled(requestUrl.hostname);
4095
+ ensureTunnel(requestUrl.hostname);
4023
4096
  ifInvalidCredentialThrowError();
4024
4097
  const idToken = session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.idToken;
4025
4098
  const postLogoutRedirectUri = ensureLocalRedirectUrl({
@@ -4054,7 +4127,7 @@ function createCustomerAccountClient({
4054
4127
  mutate,
4055
4128
  query,
4056
4129
  authorize: async () => {
4057
- throwIfNotTunnelled(requestUrl.hostname);
4130
+ ensureTunnel(requestUrl.hostname);
4058
4131
  ifInvalidCredentialThrowError();
4059
4132
  const code = requestUrl.searchParams.get("code");
4060
4133
  const state = requestUrl.searchParams.get("state");
@@ -4260,6 +4333,7 @@ function createHydrogenContext(options, additionalContext) {
4260
4333
  customerApiVersion: customerAccountOptions?.apiVersion,
4261
4334
  authUrl: customerAccountOptions?.authUrl,
4262
4335
  customAuthStatusHandler: customerAccountOptions?.customAuthStatusHandler,
4336
+ useCustomAuthDomain: customerAccountOptions?.useCustomAuthDomain,
4263
4337
  // locale - i18n.language is a union of StorefrontLanguageCode | CustomerLanguageCode
4264
4338
  // We cast here because createCustomerAccountClient expects CustomerLanguageCode specifically,
4265
4339
  // but the union type is compatible since most language codes overlap between the two APIs
@@ -4372,6 +4446,11 @@ function createRequestHandler({
4372
4446
  appendPoweredByHeader?.(response2);
4373
4447
  return response2;
4374
4448
  }
4449
+ if (storefront?.isMcpUrl(request)) {
4450
+ const response2 = await storefront.forwardMcp(request);
4451
+ appendPoweredByHeader?.(response2);
4452
+ return response2;
4453
+ }
4375
4454
  }
4376
4455
  const response = await handleRequest(request, context);
4377
4456
  if (storefront && proxyStandardRoutes) {