@shopify/hydrogen 2026.4.4 → 2026.4.5

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.
@@ -613,7 +613,7 @@ function getPrivacyBanner() {
613
613
  }
614
614
 
615
615
  // package.json
616
- var version = "2026.4.4";
616
+ var version = "2026.4.5";
617
617
 
618
618
  // src/analytics-manager/ShopifyAnalytics.tsx
619
619
  function getCustomerPrivacyRequired() {
@@ -2009,7 +2009,7 @@ function generateUUID() {
2009
2009
  }
2010
2010
 
2011
2011
  // src/version.ts
2012
- var LIB_VERSION = "2026.4.4";
2012
+ var LIB_VERSION = "2026.4.5";
2013
2013
 
2014
2014
  // src/utils/graphql.ts
2015
2015
  function minifyQuery(string) {
@@ -3780,11 +3780,13 @@ async function refreshToken({
3780
3780
  const customerAccount = session.get(CUSTOMER_ACCOUNT_SESSION_KEY);
3781
3781
  const refreshToken2 = customerAccount?.refreshToken;
3782
3782
  const idToken = customerAccount?.idToken;
3783
- if (!refreshToken2)
3783
+ if (!refreshToken2) {
3784
+ clearSession(session);
3784
3785
  throw new BadRequest(
3785
3786
  "Unauthorized",
3786
3787
  "No refreshToken found in the session. Make sure your session is configured correctly and passed to `createCustomerAccountClient`."
3787
3788
  );
3789
+ }
3788
3790
  newBody.append("grant_type", "refresh_token");
3789
3791
  newBody.append("refresh_token", refreshToken2);
3790
3792
  newBody.append("client_id", customerAccountId);
@@ -3809,6 +3811,9 @@ async function refreshToken({
3809
3811
  });
3810
3812
  if (!response.ok) {
3811
3813
  const text = await response.text();
3814
+ if (getOAuthError(text) === "invalid_grant") {
3815
+ clearSession(session);
3816
+ }
3812
3817
  throw new Response(text, {
3813
3818
  status: response.status,
3814
3819
  headers: {
@@ -3819,9 +3824,13 @@ async function refreshToken({
3819
3824
  const {
3820
3825
  access_token,
3821
3826
  expires_in,
3822
- refresh_token
3827
+ refresh_token,
3828
+ error
3823
3829
  } = await response.json();
3824
3830
  if (!access_token || access_token.length === 0) {
3831
+ if (error === "invalid_grant") {
3832
+ clearSession(session);
3833
+ }
3825
3834
  throw new BadRequest("Unauthorized", "Invalid access token received.");
3826
3835
  }
3827
3836
  session.set(CUSTOMER_ACCOUNT_SESSION_KEY, {
@@ -3858,7 +3867,6 @@ async function checkExpires({
3858
3867
  await locks.refresh;
3859
3868
  delete locks.refresh;
3860
3869
  } catch (error) {
3861
- clearSession(session);
3862
3870
  if (error && error.status !== 401) {
3863
3871
  throw error;
3864
3872
  } else {
@@ -3870,6 +3878,14 @@ async function checkExpires({
3870
3878
  }
3871
3879
  }
3872
3880
  }
3881
+ function getOAuthError(body) {
3882
+ try {
3883
+ const data = JSON.parse(body);
3884
+ return typeof data?.error === "string" ? data.error : void 0;
3885
+ } catch {
3886
+ return void 0;
3887
+ }
3888
+ }
3873
3889
  function generateCodeVerifier() {
3874
3890
  const rando = generateRandomCode();
3875
3891
  return base64UrlEncode(rando);
@@ -3897,9 +3913,7 @@ function convertBufferToString(hash) {
3897
3913
  return String.fromCharCode(...numberArray);
3898
3914
  }
3899
3915
  function generateState() {
3900
- const timestamp = Date.now().toString();
3901
- const randomString = Math.random().toString(36).substring(2);
3902
- return timestamp + randomString;
3916
+ return base64UrlEncode(generateRandomCode());
3903
3917
  }
3904
3918
  async function exchangeAccessToken(authAccessToken, customerAccountId, customerAccountTokenExchangeUrl, httpsOrigin, debugInfo) {
3905
3919
  const clientId = customerAccountId;
@@ -4053,6 +4067,8 @@ function createCustomerAccountHelper(customerApiVersion, shopId) {
4053
4067
 
4054
4068
  // src/customer/customer.ts
4055
4069
  var HYDROGEN_TUNNEL_DOMAIN_SUFFIX = ".tryhydrogen.dev";
4070
+ var OAUTH_RECOVERY_PARAM = "oauth_recovery";
4071
+ var OAUTH_RECOVERY_STATE_PREFIX = "oauth-recovery.";
4056
4072
  function checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri) {
4057
4073
  if (hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) return;
4058
4074
  if (useCustomAuthDomain) {
@@ -4284,7 +4300,8 @@ function createCustomerAccountClient({
4284
4300
  ensureTunnel(requestUrl.hostname);
4285
4301
  ifInvalidCredentialThrowError();
4286
4302
  const loginUrl = new URL(getCustomerAccountUrl("AUTH" /* AUTH */));
4287
- const state = generateState();
4303
+ const isOAuthRecovery = requestUrl.searchParams.get(OAUTH_RECOVERY_PARAM) === "true";
4304
+ const state = `${isOAuthRecovery ? OAUTH_RECOVERY_STATE_PREFIX : ""}${generateState()}`;
4288
4305
  const nonce = generateNonce();
4289
4306
  loginUrl.searchParams.set("client_id", customerAccountId);
4290
4307
  loginUrl.searchParams.set("scope", "openid email");
@@ -4373,17 +4390,30 @@ function createCustomerAccountClient({
4373
4390
  const code = requestUrl.searchParams.get("code");
4374
4391
  const state = requestUrl.searchParams.get("state");
4375
4392
  if (!code || !state) {
4376
- clearSession(session);
4377
4393
  throw new BadRequest(
4378
4394
  "Unauthorized",
4379
4395
  "No code or state parameter found in the redirect URL."
4380
4396
  );
4381
4397
  }
4382
- if (session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.state !== state) {
4383
- clearSession(session);
4384
- throw new BadRequest(
4385
- "Unauthorized",
4386
- "The session state does not match the state parameter. Make sure that the session is configured correctly and passed to `createCustomerAccountClient`."
4398
+ const customerAccountSession = session.get(CUSTOMER_ACCOUNT_SESSION_KEY);
4399
+ if (customerAccountSession?.state !== state) {
4400
+ if (state.startsWith(OAUTH_RECOVERY_STATE_PREFIX)) {
4401
+ throw new BadRequest(
4402
+ "Unable to complete sign in. Please try again.",
4403
+ "The session state does not match the state parameter. Make sure that the session is configured correctly and passed to `createCustomerAccountClient`."
4404
+ );
4405
+ }
4406
+ warnOnce(
4407
+ "[h2:warn:customerAccount] OAuth callback state mismatch. Restarting customer login once."
4408
+ );
4409
+ const recoveryLoginUrl = new URL(loginPath, request.url);
4410
+ recoveryLoginUrl.searchParams.set(
4411
+ "return_to",
4412
+ customerAccountSession?.redirectPath ?? defaultRedirectPath
4413
+ );
4414
+ recoveryLoginUrl.searchParams.set(OAUTH_RECOVERY_PARAM, "true");
4415
+ return redirect(
4416
+ `${recoveryLoginUrl.pathname}${recoveryLoginUrl.search}`
4387
4417
  );
4388
4418
  }
4389
4419
  const clientId = customerAccountId;