@shopify/hydrogen 2026.4.3 → 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.
@@ -437,7 +437,7 @@ function getPrivacyBanner() {
437
437
  }
438
438
 
439
439
  // package.json
440
- var version = "2026.4.3";
440
+ var version = "2026.4.5";
441
441
 
442
442
  // src/analytics-manager/ShopifyAnalytics.tsx
443
443
  function getCustomerPrivacyRequired() {
@@ -846,7 +846,7 @@ function PerfKit({ shop }) {
846
846
  "data-storefront-id": shop.hydrogenSubchannelId,
847
847
  "data-monorail-region": "global",
848
848
  "data-spa-mode": "true",
849
- "data-resource-timing-sampling-rate": "100"
849
+ "data-resource-timing-sampling-rate": "10"
850
850
  }
851
851
  });
852
852
  useEffect(() => {
@@ -1833,7 +1833,7 @@ function generateUUID() {
1833
1833
  }
1834
1834
 
1835
1835
  // src/version.ts
1836
- var LIB_VERSION = "2026.4.3";
1836
+ var LIB_VERSION = "2026.4.5";
1837
1837
 
1838
1838
  // src/utils/graphql.ts
1839
1839
  function minifyQuery(string) {
@@ -3604,11 +3604,13 @@ async function refreshToken({
3604
3604
  const customerAccount = session.get(CUSTOMER_ACCOUNT_SESSION_KEY);
3605
3605
  const refreshToken2 = customerAccount?.refreshToken;
3606
3606
  const idToken = customerAccount?.idToken;
3607
- if (!refreshToken2)
3607
+ if (!refreshToken2) {
3608
+ clearSession(session);
3608
3609
  throw new BadRequest(
3609
3610
  "Unauthorized",
3610
3611
  "No refreshToken found in the session. Make sure your session is configured correctly and passed to `createCustomerAccountClient`."
3611
3612
  );
3613
+ }
3612
3614
  newBody.append("grant_type", "refresh_token");
3613
3615
  newBody.append("refresh_token", refreshToken2);
3614
3616
  newBody.append("client_id", customerAccountId);
@@ -3633,6 +3635,9 @@ async function refreshToken({
3633
3635
  });
3634
3636
  if (!response.ok) {
3635
3637
  const text = await response.text();
3638
+ if (getOAuthError(text) === "invalid_grant") {
3639
+ clearSession(session);
3640
+ }
3636
3641
  throw new Response(text, {
3637
3642
  status: response.status,
3638
3643
  headers: {
@@ -3643,9 +3648,13 @@ async function refreshToken({
3643
3648
  const {
3644
3649
  access_token,
3645
3650
  expires_in,
3646
- refresh_token
3651
+ refresh_token,
3652
+ error
3647
3653
  } = await response.json();
3648
3654
  if (!access_token || access_token.length === 0) {
3655
+ if (error === "invalid_grant") {
3656
+ clearSession(session);
3657
+ }
3649
3658
  throw new BadRequest("Unauthorized", "Invalid access token received.");
3650
3659
  }
3651
3660
  session.set(CUSTOMER_ACCOUNT_SESSION_KEY, {
@@ -3682,7 +3691,6 @@ async function checkExpires({
3682
3691
  await locks.refresh;
3683
3692
  delete locks.refresh;
3684
3693
  } catch (error) {
3685
- clearSession(session);
3686
3694
  if (error && error.status !== 401) {
3687
3695
  throw error;
3688
3696
  } else {
@@ -3694,6 +3702,14 @@ async function checkExpires({
3694
3702
  }
3695
3703
  }
3696
3704
  }
3705
+ function getOAuthError(body) {
3706
+ try {
3707
+ const data = JSON.parse(body);
3708
+ return typeof data?.error === "string" ? data.error : void 0;
3709
+ } catch {
3710
+ return void 0;
3711
+ }
3712
+ }
3697
3713
  function generateCodeVerifier() {
3698
3714
  const rando = generateRandomCode();
3699
3715
  return base64UrlEncode(rando);
@@ -3721,9 +3737,7 @@ function convertBufferToString(hash) {
3721
3737
  return String.fromCharCode(...numberArray);
3722
3738
  }
3723
3739
  function generateState() {
3724
- const timestamp = Date.now().toString();
3725
- const randomString = Math.random().toString(36).substring(2);
3726
- return timestamp + randomString;
3740
+ return base64UrlEncode(generateRandomCode());
3727
3741
  }
3728
3742
  async function exchangeAccessToken(authAccessToken, customerAccountId, customerAccountTokenExchangeUrl, httpsOrigin, debugInfo) {
3729
3743
  const clientId = customerAccountId;
@@ -3877,6 +3891,8 @@ function createCustomerAccountHelper(customerApiVersion, shopId) {
3877
3891
 
3878
3892
  // src/customer/customer.ts
3879
3893
  var HYDROGEN_TUNNEL_DOMAIN_SUFFIX = ".tryhydrogen.dev";
3894
+ var OAUTH_RECOVERY_PARAM = "oauth_recovery";
3895
+ var OAUTH_RECOVERY_STATE_PREFIX = "oauth-recovery.";
3880
3896
  function checkTunnelDomain(hostname, useCustomAuthDomain, redirectUri) {
3881
3897
  if (hostname.endsWith(HYDROGEN_TUNNEL_DOMAIN_SUFFIX)) return;
3882
3898
  if (useCustomAuthDomain) {
@@ -4108,7 +4124,8 @@ function createCustomerAccountClient({
4108
4124
  ensureTunnel(requestUrl.hostname);
4109
4125
  ifInvalidCredentialThrowError();
4110
4126
  const loginUrl = new URL(getCustomerAccountUrl("AUTH" /* AUTH */));
4111
- const state = generateState();
4127
+ const isOAuthRecovery = requestUrl.searchParams.get(OAUTH_RECOVERY_PARAM) === "true";
4128
+ const state = `${isOAuthRecovery ? OAUTH_RECOVERY_STATE_PREFIX : ""}${generateState()}`;
4112
4129
  const nonce = generateNonce();
4113
4130
  loginUrl.searchParams.set("client_id", customerAccountId);
4114
4131
  loginUrl.searchParams.set("scope", "openid email");
@@ -4197,17 +4214,30 @@ function createCustomerAccountClient({
4197
4214
  const code = requestUrl.searchParams.get("code");
4198
4215
  const state = requestUrl.searchParams.get("state");
4199
4216
  if (!code || !state) {
4200
- clearSession(session);
4201
4217
  throw new BadRequest(
4202
4218
  "Unauthorized",
4203
4219
  "No code or state parameter found in the redirect URL."
4204
4220
  );
4205
4221
  }
4206
- if (session.get(CUSTOMER_ACCOUNT_SESSION_KEY)?.state !== state) {
4207
- clearSession(session);
4208
- throw new BadRequest(
4209
- "Unauthorized",
4210
- "The session state does not match the state parameter. Make sure that the session is configured correctly and passed to `createCustomerAccountClient`."
4222
+ const customerAccountSession = session.get(CUSTOMER_ACCOUNT_SESSION_KEY);
4223
+ if (customerAccountSession?.state !== state) {
4224
+ if (state.startsWith(OAUTH_RECOVERY_STATE_PREFIX)) {
4225
+ throw new BadRequest(
4226
+ "Unable to complete sign in. Please try again.",
4227
+ "The session state does not match the state parameter. Make sure that the session is configured correctly and passed to `createCustomerAccountClient`."
4228
+ );
4229
+ }
4230
+ warnOnce(
4231
+ "[h2:warn:customerAccount] OAuth callback state mismatch. Restarting customer login once."
4232
+ );
4233
+ const recoveryLoginUrl = new URL(loginPath, request.url);
4234
+ recoveryLoginUrl.searchParams.set(
4235
+ "return_to",
4236
+ customerAccountSession?.redirectPath ?? defaultRedirectPath
4237
+ );
4238
+ recoveryLoginUrl.searchParams.set(OAUTH_RECOVERY_PARAM, "true");
4239
+ return redirect(
4240
+ `${recoveryLoginUrl.pathname}${recoveryLoginUrl.search}`
4211
4241
  );
4212
4242
  }
4213
4243
  const clientId = customerAccountId;