@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.
@@ -610,7 +610,7 @@ function getPrivacyBanner() {
610
610
  }
611
611
 
612
612
  // package.json
613
- var version = "2026.1.2";
613
+ var version = "2026.1.4";
614
614
 
615
615
  // src/analytics-manager/ShopifyAnalytics.tsx
616
616
  function getCustomerPrivacyRequired() {
@@ -1326,6 +1326,7 @@ function getStorefrontHeaders(request) {
1326
1326
  };
1327
1327
  }
1328
1328
  var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
1329
+ var MCP_RE = /^\/api\/mcp$/;
1329
1330
  var getSafePathname = (url) => {
1330
1331
  try {
1331
1332
  return new URL(url, "http://e.c").pathname;
@@ -2005,7 +2006,7 @@ function generateUUID() {
2005
2006
  }
2006
2007
 
2007
2008
  // src/version.ts
2008
- var LIB_VERSION = "2026.1.2";
2009
+ var LIB_VERSION = "2026.1.4";
2009
2010
 
2010
2011
  // src/utils/graphql.ts
2011
2012
  function minifyQuery(string) {
@@ -2430,6 +2431,70 @@ function createStorefrontClient(options) {
2430
2431
  );
2431
2432
  return new Response(sfapiResponse.body, sfapiResponse);
2432
2433
  },
2434
+ /**
2435
+ * Checks if the request is targeting the Storefront MCP endpoint.
2436
+ */
2437
+ isMcpUrl(request) {
2438
+ return MCP_RE.test(getSafePathname(request.url ?? ""));
2439
+ },
2440
+ /**
2441
+ * Forwards the request to the Storefront MCP endpoint.
2442
+ * CORS headers are intentionally omitted — the Storefront MCP
2443
+ * server is server-to-server only (OPTIONS preflight returns 404).
2444
+ */
2445
+ async forwardMcp(request) {
2446
+ const forwardedHeaders = new Headers([
2447
+ ...extractHeaders(
2448
+ (key) => request.headers.get(key),
2449
+ [
2450
+ "accept",
2451
+ "accept-encoding",
2452
+ "accept-language",
2453
+ "content-type",
2454
+ "cookie",
2455
+ "origin",
2456
+ "referer",
2457
+ "user-agent"
2458
+ ]
2459
+ ),
2460
+ ...extractHeaders(
2461
+ (key) => defaultHeaders[key],
2462
+ [
2463
+ SHOPIFY_CLIENT_IP_HEADER,
2464
+ SHOPIFY_CLIENT_IP_SIG_HEADER,
2465
+ STOREFRONT_ACCESS_TOKEN_HEADER,
2466
+ STOREFRONT_REQUEST_GROUP_ID_HEADER,
2467
+ hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER
2468
+ ]
2469
+ )
2470
+ ]);
2471
+ if (storefrontHeaders?.buyerIp) {
2472
+ forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
2473
+ }
2474
+ const mcpUrl = `${getShopifyDomain()}/api/mcp`;
2475
+ try {
2476
+ const mcpResponse = await fetch(mcpUrl, {
2477
+ method: request.method,
2478
+ body: request.body,
2479
+ headers: forwardedHeaders
2480
+ });
2481
+ return new Response(mcpResponse.body, mcpResponse);
2482
+ } catch (error) {
2483
+ const JSON_RPC_INTERNAL_ERROR = -32603;
2484
+ const message = error instanceof Error ? error.message : "Internal proxy error";
2485
+ return new Response(
2486
+ JSON.stringify({
2487
+ jsonrpc: "2.0",
2488
+ error: { code: JSON_RPC_INTERNAL_ERROR, message },
2489
+ id: null
2490
+ }),
2491
+ {
2492
+ status: 502,
2493
+ headers: { "content-type": "application/json" }
2494
+ }
2495
+ );
2496
+ }
2497
+ },
2433
2498
  setCollectedSubrequestHeaders: (response) => {
2434
2499
  if (collectedSubrequestHeaders) {
2435
2500
  for (const value of collectedSubrequestHeaders.setCookie) {
@@ -3923,29 +3988,32 @@ function createCustomerAccountHelper(customerApiVersion, shopId) {
3923
3988
 
3924
3989
  // src/customer/customer.ts
3925
3990
  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
- }
3991
+ function checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri) {
3992
+ if (hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) return;
3993
+ if (useCustomAuthDomain) {
3994
+ const redirectHint = redirectUri ? ` (${redirectUri})` : "";
3995
+ warnOnce(
3996
+ `[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.`
3997
+ );
3998
+ return;
3943
3999
  }
4000
+ throw new Response(
4001
+ [
4002
+ "Customer Account API OAuth requires a Hydrogen tunnel in local development.",
4003
+ "Run the development server with the `--customer-account-push` flag,",
4004
+ `then open the tunnel URL shown in your terminal (\`https://*${HYDROGEN_TUNNEL_DOMAIN_SUFFIX}\`) instead of localhost.`
4005
+ ].join("\n\n"),
4006
+ {
4007
+ status: 400,
4008
+ headers: {
4009
+ "Content-Type": "text/plain; charset=utf-8"
4010
+ }
4011
+ }
4012
+ );
3944
4013
  }
3945
4014
  function defaultAuthStatusHandler(request, defaultLoginUrl) {
3946
4015
  if (!request.url) return defaultLoginUrl;
3947
- const { hostname, pathname } = new URL(request.url);
3948
- throwIfNotTunnelled(hostname);
4016
+ const { pathname } = new URL(request.url);
3949
4017
  const cleanedPathname = pathname.replace(/\.data$/, "").replace(/\/_root$/, "/").replace(/(.+)\/$/, "$1");
3950
4018
  const redirectTo = defaultLoginUrl + `?${new URLSearchParams({ return_to: cleanedPathname }).toString()}`;
3951
4019
  return redirect(redirectTo);
@@ -3963,7 +4031,8 @@ function createCustomerAccountClient({
3963
4031
  loginPath = "/account/login",
3964
4032
  authorizePath = "/account/authorize",
3965
4033
  defaultRedirectPath = "/account",
3966
- language
4034
+ language,
4035
+ useCustomAuthDomain
3967
4036
  }) {
3968
4037
  if (customerApiVersion !== DEFAULT_CUSTOMER_API_VERSION) {
3969
4038
  console.warn(
@@ -3980,7 +4049,6 @@ function createCustomerAccountClient({
3980
4049
  "[h2:error:createCustomerAccountClient] The request object does not contain a URL."
3981
4050
  );
3982
4051
  }
3983
- const authStatusHandler = customAuthStatusHandler ? customAuthStatusHandler : () => defaultAuthStatusHandler(request, loginPath);
3984
4052
  const requestUrl = new URL(request.url);
3985
4053
  const httpsOrigin = requestUrl.protocol === "http:" ? requestUrl.origin.replace("http", "https") : requestUrl.origin;
3986
4054
  const redirectUri = ensureLocalRedirectUrl({
@@ -3988,6 +4056,11 @@ function createCustomerAccountClient({
3988
4056
  defaultUrl: authorizePath,
3989
4057
  redirectUrl: authUrl
3990
4058
  });
4059
+ const ensureTunnel = (hostname) => checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri);
4060
+ const authStatusHandler = customAuthStatusHandler ? customAuthStatusHandler : () => {
4061
+ ensureTunnel(requestUrl.hostname);
4062
+ return defaultAuthStatusHandler(request, loginPath);
4063
+ };
3991
4064
  const getCustomerAccountUrl = createCustomerAccountHelper(
3992
4065
  customerApiVersion,
3993
4066
  shopId
@@ -4109,7 +4182,7 @@ function createCustomerAccountClient({
4109
4182
  return session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.accessToken;
4110
4183
  }
4111
4184
  async function mutate(mutation, options) {
4112
- throwIfNotTunnelled(requestUrl.hostname);
4185
+ ensureTunnel(requestUrl.hostname);
4113
4186
  ifInvalidCredentialThrowError();
4114
4187
  mutation = minifyQuery(mutation);
4115
4188
  assertMutation(mutation, "customer.mutate");
@@ -4119,7 +4192,7 @@ function createCustomerAccountClient({
4119
4192
  );
4120
4193
  }
4121
4194
  async function query(query2, options) {
4122
- throwIfNotTunnelled(requestUrl.hostname);
4195
+ ensureTunnel(requestUrl.hostname);
4123
4196
  ifInvalidCredentialThrowError();
4124
4197
  query2 = minifyQuery(query2);
4125
4198
  assertQuery(query2, "customer.query");
@@ -4143,7 +4216,7 @@ function createCustomerAccountClient({
4143
4216
  return {
4144
4217
  i18n: { language: language ?? "EN" },
4145
4218
  login: async (options) => {
4146
- throwIfNotTunnelled(requestUrl.hostname);
4219
+ ensureTunnel(requestUrl.hostname);
4147
4220
  ifInvalidCredentialThrowError();
4148
4221
  const loginUrl = new URL(getCustomerAccountUrl("AUTH" /* AUTH */));
4149
4222
  const state = generateState();
@@ -4195,7 +4268,7 @@ function createCustomerAccountClient({
4195
4268
  return redirect(loginUrl.toString());
4196
4269
  },
4197
4270
  logout: async (options) => {
4198
- throwIfNotTunnelled(requestUrl.hostname);
4271
+ ensureTunnel(requestUrl.hostname);
4199
4272
  ifInvalidCredentialThrowError();
4200
4273
  const idToken = session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.idToken;
4201
4274
  const postLogoutRedirectUri = ensureLocalRedirectUrl({
@@ -4230,7 +4303,7 @@ function createCustomerAccountClient({
4230
4303
  mutate,
4231
4304
  query,
4232
4305
  authorize: async () => {
4233
- throwIfNotTunnelled(requestUrl.hostname);
4306
+ ensureTunnel(requestUrl.hostname);
4234
4307
  ifInvalidCredentialThrowError();
4235
4308
  const code = requestUrl.searchParams.get("code");
4236
4309
  const state = requestUrl.searchParams.get("state");
@@ -4436,6 +4509,7 @@ function createHydrogenContext(options, additionalContext) {
4436
4509
  customerApiVersion: customerAccountOptions?.apiVersion,
4437
4510
  authUrl: customerAccountOptions?.authUrl,
4438
4511
  customAuthStatusHandler: customerAccountOptions?.customAuthStatusHandler,
4512
+ useCustomAuthDomain: customerAccountOptions?.useCustomAuthDomain,
4439
4513
  // locale - i18n.language is a union of StorefrontLanguageCode | CustomerLanguageCode
4440
4514
  // We cast here because createCustomerAccountClient expects CustomerLanguageCode specifically,
4441
4515
  // but the union type is compatible since most language codes overlap between the two APIs
@@ -4548,6 +4622,11 @@ function createRequestHandler({
4548
4622
  appendPoweredByHeader?.(response2);
4549
4623
  return response2;
4550
4624
  }
4625
+ if (storefront?.isMcpUrl(request)) {
4626
+ const response2 = await storefront.forwardMcp(request);
4627
+ appendPoweredByHeader?.(response2);
4628
+ return response2;
4629
+ }
4551
4630
  }
4552
4631
  const response = await handleRequest(request, context);
4553
4632
  if (storefront && proxyStandardRoutes) {