@shopify/hydrogen 2026.10.0-preview.4 → 2026.10.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.
- package/dist/cli/index.mjs +23 -12
- package/dist/client/client.d.mts +18 -15
- package/dist/client/client.mjs +8 -3
- package/dist/client/client.mjs.map +1 -1
- package/dist/client/errors.d.mts +35 -13
- package/dist/client/errors.mjs +23 -13
- package/dist/client/errors.mjs.map +1 -1
- package/dist/client/types.d.mts +140 -90
- package/dist/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/core/analytics/cart-tracker.mjs +18 -10
- package/dist/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/core/analytics/events.d.mts +17 -11
- package/dist/core/analytics/events.mjs +13 -4
- package/dist/core/analytics/events.mjs.map +1 -1
- package/dist/core/analytics/types.d.mts +207 -118
- package/dist/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/core/cache/key.d.mts +2 -0
- package/dist/core/cache/key.mjs.map +1 -1
- package/dist/core/cache/run-with-cache.d.mts +56 -8
- package/dist/core/cache/run-with-cache.mjs +17 -6
- package/dist/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/core/cache/store.d.mts +8 -6
- package/dist/core/cache/store.mjs.map +1 -1
- package/dist/core/cache/strategies.d.mts +27 -2
- package/dist/core/cache/strategies.mjs +6 -0
- package/dist/core/cache/strategies.mjs.map +1 -1
- package/dist/core/cart/actions.d.mts +37 -30
- package/dist/core/cart/actions.mjs +21 -9
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/core/cart/buyer-identity-sync.d.mts +7 -6
- package/dist/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/core/cart/cart.d.mts +59 -46
- package/dist/core/cart/cart.mjs +46 -29
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/cookie.d.mts +8 -4
- package/dist/core/cart/cookie.mjs +8 -4
- package/dist/core/cart/cookie.mjs.map +1 -1
- package/dist/core/cart/form.d.mts +34 -20
- package/dist/core/cart/form.mjs +6 -4
- package/dist/core/cart/form.mjs.map +1 -1
- package/dist/core/cart/get-cart.d.mts +27 -15
- package/dist/core/cart/get-cart.mjs +13 -8
- package/dist/core/cart/get-cart.mjs.map +1 -1
- package/dist/core/cart/quantity.d.mts +16 -5
- package/dist/core/cart/quantity.mjs +16 -5
- package/dist/core/cart/quantity.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +25 -19
- package/dist/core/cart/queries.mjs +15 -1
- package/dist/core/cart/queries.mjs.map +1 -1
- package/dist/core/cart/server-handlers.d.mts +82 -45
- package/dist/core/cart/server-handlers.mjs +4 -0
- package/dist/core/cart/server-handlers.mjs.map +1 -1
- package/dist/core/cart/state.d.mts +110 -64
- package/dist/core/cart/state.mjs +24 -6
- package/dist/core/cart/state.mjs.map +1 -1
- package/dist/core/collection/collection.d.mts +52 -53
- package/dist/core/collection/collection.mjs +5 -3
- package/dist/core/collection/collection.mjs.map +1 -1
- package/dist/core/collection/reconciler.d.mts +23 -16
- package/dist/core/collection/reconciler.mjs +8 -4
- package/dist/core/collection/reconciler.mjs.map +1 -1
- package/dist/core/collection/state.d.mts +31 -26
- package/dist/core/collection/state.mjs +4 -3
- package/dist/core/collection/state.mjs.map +1 -1
- package/dist/core/collection/url.d.mts +70 -29
- package/dist/core/collection/url.mjs +63 -25
- package/dist/core/collection/url.mjs.map +1 -1
- package/dist/core/constants.d.mts +1 -0
- package/dist/core/constants.mjs +1 -0
- package/dist/core/constants.mjs.map +1 -1
- package/dist/core/logging/logging.d.mts +14 -8
- package/dist/core/logging/logging.mjs +15 -10
- package/dist/core/logging/logging.mjs.map +1 -1
- package/dist/core/logging/types.d.mts +28 -11
- package/dist/core/logging/types.mjs.map +1 -1
- package/dist/core/money/format.d.mts +6 -4
- package/dist/core/money/format.mjs +19 -9
- package/dist/core/money/format.mjs.map +1 -1
- package/dist/core/money/types.d.mts +41 -25
- package/dist/core/predictive-search/form.d.mts +37 -7
- package/dist/core/predictive-search/form.mjs +21 -3
- package/dist/core/predictive-search/form.mjs.map +1 -1
- package/dist/core/predictive-search/queries.d.mts +18 -30
- package/dist/core/predictive-search/queries.mjs +7 -3
- package/dist/core/predictive-search/queries.mjs.map +1 -1
- package/dist/core/predictive-search/search.d.mts +25 -27
- package/dist/core/predictive-search/search.mjs +20 -19
- package/dist/core/predictive-search/search.mjs.map +1 -1
- package/dist/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/core/predictive-search/store.d.mts +65 -24
- package/dist/core/predictive-search/store.mjs +12 -1
- package/dist/core/predictive-search/store.mjs.map +1 -1
- package/dist/core/predictive-search/url.d.mts +36 -37
- package/dist/core/predictive-search/url.mjs +14 -6
- package/dist/core/predictive-search/url.mjs.map +1 -1
- package/dist/core/product/form.d.mts +63 -16
- package/dist/core/product/form.mjs +8 -2
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/options.d.mts +8 -10
- package/dist/core/product/options.mjs +8 -10
- package/dist/core/product/options.mjs.map +1 -1
- package/dist/core/product/product-form.d.mts +77 -29
- package/dist/core/product/product-form.mjs +32 -10
- package/dist/core/product/product-form.mjs.map +1 -1
- package/dist/core/product/state.d.mts +55 -32
- package/dist/core/product/url.d.mts +19 -13
- package/dist/core/product/url.mjs +10 -10
- package/dist/core/product/url.mjs.map +1 -1
- package/dist/core/request-context.d.mts +77 -19
- package/dist/core/request-context.mjs +5 -1
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.d.mts +5 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/core/request-routing/registered-routes.mjs +12 -0
- package/dist/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/core/request-routing/route-types.d.mts +43 -18
- package/dist/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +15 -5
- package/dist/core/shopify-scripts/index.mjs +15 -5
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +58 -25
- package/dist/core/standard-routes/build.d.mts +11 -12
- package/dist/core/standard-routes/build.mjs +11 -12
- package/dist/core/standard-routes/build.mjs.map +1 -1
- package/dist/core/standard-routes/types.d.mts +30 -24
- package/dist/core/url.mjs +2 -1
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.d.mts +36 -69
- package/dist/customer-account/client.mjs +7 -8
- package/dist/customer-account/client.mjs.map +1 -1
- package/dist/customer-account/errors.d.mts +45 -36
- package/dist/customer-account/errors.mjs +21 -36
- package/dist/customer-account/errors.mjs.map +1 -1
- package/dist/customer-account/graphql.d.mts +11 -23
- package/dist/customer-account/graphql.mjs +3 -9
- package/dist/customer-account/graphql.mjs.map +1 -1
- package/dist/customer-account/session.d.mts +130 -100
- package/dist/customer-account/session.mjs +32 -10
- package/dist/customer-account/session.mjs.map +1 -1
- package/dist/customer-account/type-resolver.d.mts +7 -8
- package/dist/development/client/client.d.mts +18 -15
- package/dist/development/client/client.mjs +8 -3
- package/dist/development/client/client.mjs.map +1 -1
- package/dist/development/client/errors.d.mts +35 -13
- package/dist/development/client/errors.mjs +23 -13
- package/dist/development/client/errors.mjs.map +1 -1
- package/dist/development/client/types.d.mts +140 -90
- package/dist/development/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/development/core/analytics/cart-tracker.mjs +18 -10
- package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/development/core/analytics/events.d.mts +17 -11
- package/dist/development/core/analytics/events.mjs +13 -4
- package/dist/development/core/analytics/events.mjs.map +1 -1
- package/dist/development/core/analytics/types.d.mts +207 -118
- package/dist/development/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/development/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/development/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/development/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/development/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/development/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/key.d.mts +2 -0
- package/dist/development/core/cache/key.mjs.map +1 -1
- package/dist/development/core/cache/run-with-cache.d.mts +56 -8
- package/dist/development/core/cache/run-with-cache.mjs +17 -6
- package/dist/development/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/store.d.mts +8 -6
- package/dist/development/core/cache/store.mjs.map +1 -1
- package/dist/development/core/cache/strategies.d.mts +27 -2
- package/dist/development/core/cache/strategies.mjs +6 -0
- package/dist/development/core/cache/strategies.mjs.map +1 -1
- package/dist/development/core/cart/actions.d.mts +37 -30
- package/dist/development/core/cart/actions.mjs +21 -9
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/development/core/cart/buyer-identity-sync.d.mts +6 -5
- package/dist/development/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/development/core/cart/cart.d.mts +59 -46
- package/dist/development/core/cart/cart.mjs +46 -29
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/cookie.d.mts +8 -4
- package/dist/development/core/cart/cookie.mjs +8 -4
- package/dist/development/core/cart/cookie.mjs.map +1 -1
- package/dist/development/core/cart/form.d.mts +34 -20
- package/dist/development/core/cart/form.mjs +6 -4
- package/dist/development/core/cart/form.mjs.map +1 -1
- package/dist/development/core/cart/get-cart.d.mts +27 -15
- package/dist/development/core/cart/get-cart.mjs +13 -8
- package/dist/development/core/cart/get-cart.mjs.map +1 -1
- package/dist/development/core/cart/quantity.d.mts +16 -5
- package/dist/development/core/cart/quantity.mjs +16 -5
- package/dist/development/core/cart/quantity.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +25 -19
- package/dist/development/core/cart/queries.mjs +15 -1
- package/dist/development/core/cart/queries.mjs.map +1 -1
- package/dist/development/core/cart/server-handlers.d.mts +82 -45
- package/dist/development/core/cart/server-handlers.mjs +4 -0
- package/dist/development/core/cart/server-handlers.mjs.map +1 -1
- package/dist/development/core/cart/state.d.mts +110 -64
- package/dist/development/core/cart/state.mjs +24 -6
- package/dist/development/core/cart/state.mjs.map +1 -1
- package/dist/development/core/collection/collection.d.mts +52 -53
- package/dist/development/core/collection/collection.mjs +5 -3
- package/dist/development/core/collection/collection.mjs.map +1 -1
- package/dist/development/core/collection/reconciler.d.mts +23 -16
- package/dist/development/core/collection/reconciler.mjs +8 -4
- package/dist/development/core/collection/reconciler.mjs.map +1 -1
- package/dist/development/core/collection/state.d.mts +31 -26
- package/dist/development/core/collection/state.mjs +4 -3
- package/dist/development/core/collection/state.mjs.map +1 -1
- package/dist/development/core/collection/url.d.mts +70 -29
- package/dist/development/core/collection/url.mjs +63 -25
- package/dist/development/core/collection/url.mjs.map +1 -1
- package/dist/development/core/constants.mjs.map +1 -1
- package/dist/development/core/logging/logging.d.mts +14 -8
- package/dist/development/core/logging/logging.mjs +15 -10
- package/dist/development/core/logging/logging.mjs.map +1 -1
- package/dist/development/core/logging/types.d.mts +28 -11
- package/dist/development/core/logging/types.mjs.map +1 -1
- package/dist/development/core/money/format.d.mts +6 -4
- package/dist/development/core/money/format.mjs +19 -9
- package/dist/development/core/money/format.mjs.map +1 -1
- package/dist/development/core/money/types.d.mts +41 -25
- package/dist/development/core/predictive-search/form.d.mts +37 -7
- package/dist/development/core/predictive-search/form.mjs +21 -3
- package/dist/development/core/predictive-search/form.mjs.map +1 -1
- package/dist/development/core/predictive-search/queries.d.mts +18 -30
- package/dist/development/core/predictive-search/queries.mjs +7 -3
- package/dist/development/core/predictive-search/queries.mjs.map +1 -1
- package/dist/development/core/predictive-search/search.d.mts +25 -27
- package/dist/development/core/predictive-search/search.mjs +20 -19
- package/dist/development/core/predictive-search/search.mjs.map +1 -1
- package/dist/development/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/development/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/development/core/predictive-search/store.d.mts +65 -24
- package/dist/development/core/predictive-search/store.mjs +12 -1
- package/dist/development/core/predictive-search/store.mjs.map +1 -1
- package/dist/development/core/predictive-search/url.d.mts +36 -37
- package/dist/development/core/predictive-search/url.mjs +14 -6
- package/dist/development/core/predictive-search/url.mjs.map +1 -1
- package/dist/development/core/product/form.d.mts +63 -16
- package/dist/development/core/product/form.mjs +8 -2
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/options.d.mts +8 -10
- package/dist/development/core/product/options.mjs +8 -10
- package/dist/development/core/product/options.mjs.map +1 -1
- package/dist/development/core/product/product-form.d.mts +77 -29
- package/dist/development/core/product/product-form.mjs +32 -10
- package/dist/development/core/product/product-form.mjs.map +1 -1
- package/dist/development/core/product/state.d.mts +55 -32
- package/dist/development/core/product/url.d.mts +19 -13
- package/dist/development/core/product/url.mjs +10 -10
- package/dist/development/core/product/url.mjs.map +1 -1
- package/dist/development/core/request-context.d.mts +77 -19
- package/dist/development/core/request-context.mjs +5 -1
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.d.mts +9 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs +7 -0
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/development/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/route-types.d.mts +43 -18
- package/dist/development/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/development/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/development/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +15 -5
- package/dist/development/core/shopify-scripts/index.mjs +15 -5
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +58 -25
- package/dist/development/core/standard-routes/build.d.mts +11 -12
- package/dist/development/core/standard-routes/build.mjs +11 -12
- package/dist/development/core/standard-routes/build.mjs.map +1 -1
- package/dist/development/core/standard-routes/types.d.mts +30 -24
- package/dist/development/core/url.mjs +2 -1
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/customer-account/session.d.mts +45 -54
- package/dist/development/customer-account/session.mjs +20 -4
- package/dist/development/customer-account/session.mjs.map +1 -1
- package/dist/development/globals.d.mts +20 -11
- package/dist/development/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/development/graphql/graphql.d.mts +17 -16
- package/dist/development/graphql/graphql.mjs +7 -8
- package/dist/development/graphql/graphql.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +15 -8
- package/dist/development/react/cart.d.mts +78 -32
- package/dist/development/react/cart.mjs +56 -29
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/development/react/collection.d.mts +40 -22
- package/dist/development/react/collection.mjs +27 -7
- package/dist/development/react/collection.mjs.map +1 -1
- package/dist/development/react/predictive-search.d.mts +65 -50
- package/dist/development/react/predictive-search.mjs +23 -21
- package/dist/development/react/predictive-search.mjs.map +1 -1
- package/dist/development/react/product.d.mts +45 -16
- package/dist/development/react/product.mjs +13 -11
- package/dist/development/react/product.mjs.map +1 -1
- package/dist/development/react/shop-pay.d.mts +15 -0
- package/dist/development/react/shop-pay.mjs +9 -0
- package/dist/development/react/shop-pay.mjs.map +1 -1
- package/dist/development/react/shopify-scripts.d.mts +23 -2
- package/dist/development/react/shopify-scripts.mjs +9 -0
- package/dist/development/react/shopify-scripts.mjs.map +1 -1
- package/dist/development/vue/cart.d.mts +28 -23
- package/dist/development/vue/cart.mjs +42 -25
- package/dist/development/vue/cart.mjs.map +1 -1
- package/dist/development/vue/collection.d.mts +20 -12
- package/dist/development/vue/collection.mjs +15 -7
- package/dist/development/vue/collection.mjs.map +1 -1
- package/dist/development/vue/index.d.mts +2 -2
- package/dist/development/vue/index.mjs +2 -2
- package/dist/development/vue/predictive-search.d.mts +31 -36
- package/dist/development/vue/predictive-search.mjs +16 -22
- package/dist/development/vue/predictive-search.mjs.map +1 -1
- package/dist/development/vue/product.d.mts +16 -15
- package/dist/development/vue/product.mjs +11 -11
- package/dist/development/vue/product.mjs.map +1 -1
- package/dist/development/vue/shop-pay.d.mts +14 -17
- package/dist/development/vue/shop-pay.mjs +8 -0
- package/dist/development/vue/shop-pay.mjs.map +1 -1
- package/dist/development/vue/shopify-scripts.d.mts +11 -0
- package/dist/development/vue/shopify-scripts.mjs +6 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +20 -11
- package/dist/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/graphql/graphql.d.mts +17 -16
- package/dist/graphql/graphql.mjs +7 -8
- package/dist/graphql/graphql.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +15 -8
- package/dist/react/cart.d.mts +78 -32
- package/dist/react/cart.mjs +56 -29
- package/dist/react/cart.mjs.map +1 -1
- package/dist/react/collection.d.mts +40 -22
- package/dist/react/collection.mjs +27 -7
- package/dist/react/collection.mjs.map +1 -1
- package/dist/react/predictive-search.d.mts +65 -50
- package/dist/react/predictive-search.mjs +23 -21
- package/dist/react/predictive-search.mjs.map +1 -1
- package/dist/react/product.d.mts +45 -16
- package/dist/react/product.mjs +13 -11
- package/dist/react/product.mjs.map +1 -1
- package/dist/react/shop-pay.d.mts +15 -0
- package/dist/react/shop-pay.mjs +9 -0
- package/dist/react/shop-pay.mjs.map +1 -1
- package/dist/react/shopify-scripts.d.mts +23 -2
- package/dist/react/shopify-scripts.mjs +9 -0
- package/dist/react/shopify-scripts.mjs.map +1 -1
- package/dist/ts-plugin/index.cjs +6 -0
- package/dist/ts-plugin/index.cjs.map +1 -1
- package/dist/ts-plugin/index.d.cts +6 -0
- package/dist/vite/local-https.d.mts +51 -17
- package/dist/vite/local-https.mjs +26 -5
- package/dist/vite/local-https.mjs.map +1 -1
- package/dist/vue/cart.d.mts +28 -23
- package/dist/vue/cart.mjs +42 -25
- package/dist/vue/cart.mjs.map +1 -1
- package/dist/vue/collection.d.mts +20 -12
- package/dist/vue/collection.mjs +15 -7
- package/dist/vue/collection.mjs.map +1 -1
- package/dist/vue/index.d.mts +2 -2
- package/dist/vue/index.mjs +2 -2
- package/dist/vue/predictive-search.d.mts +31 -36
- package/dist/vue/predictive-search.mjs +16 -22
- package/dist/vue/predictive-search.mjs.map +1 -1
- package/dist/vue/product.d.mts +16 -15
- package/dist/vue/product.mjs +11 -11
- package/dist/vue/product.mjs.map +1 -1
- package/dist/vue/shop-pay.d.mts +14 -17
- package/dist/vue/shop-pay.mjs +8 -0
- package/dist/vue/shop-pay.mjs.map +1 -1
- package/dist/vue/shopify-scripts.d.mts +11 -0
- package/dist/vue/shopify-scripts.mjs +6 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +3 -1
- package/skills/hydrogen-analytics/SKILL.md +1 -1
- package/skills/hydrogen-cart-ui/SKILL.md +28 -2
- package/skills/hydrogen-collection-browser/SKILL.md +9 -1
- package/skills/hydrogen-collection-browser/references/nextjs.md +1 -1
- package/skills/hydrogen-collection-browser/references/nuxt.md +1 -1
- package/skills/hydrogen-collection-browser/references/react.md +151 -32
- package/skills/hydrogen-customer-account/SKILL.md +2 -0
- package/skills/hydrogen-request-handlers/SKILL.md +21 -11
- package/skills/hydrogen-setup/references/analytics.md +14 -8
- package/skills/hydrogen-setup/steps/1-inspect-app.md +2 -2
- package/skills/hydrogen-setup/steps/6-product-detail-page.md +21 -11
- package/skills/hydrogen-storefront-client/SKILL.md +1 -1
- package/skills/hydrogen-storefront-client/references/caching.md +2 -0
- package/skills/hydrogen-variant-form/SKILL.md +25 -7
- package/skills/hydrogen-variant-form/references/nextjs.md +39 -4
- package/skills/hydrogen-variant-form/references/nuxt.md +83 -42
- package/skills/hydrogen-variant-form/references/react.md +45 -9
|
@@ -1,99 +1,90 @@
|
|
|
1
1
|
import { ShopifyRequestContext } from "../core/request-context.mjs";
|
|
2
2
|
import { ShopifyRouteSessionManager } from "../core/request-routing/route-types.mjs";
|
|
3
3
|
//#region src/customer-account/session.d.ts
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A value or a promise for that value. Session manager methods can return either.
|
|
6
|
+
*/
|
|
5
7
|
type Awaitable<T> = T | Promise<T>;
|
|
6
|
-
/**
|
|
8
|
+
/** The session storage that `isLoggedIn()` and `getAccessToken()` read. */
|
|
7
9
|
type ReadonlyCustomerSessionManager = {
|
|
8
|
-
getSessionItem(key: string): Awaitable<unknown>;
|
|
10
|
+
/** Returns the session value for a key. The customer session keeps its data under the `customerAccount` key. */getSessionItem(key: string): Awaitable<unknown>;
|
|
9
11
|
};
|
|
10
|
-
/**
|
|
12
|
+
/** The session storage that sign-in, the OAuth callback, token refresh, and logout write to. Commit the session to the response after you call `prepareLoginUrl()`, `handleOAuthCallback()`, `getOrRefreshAccessToken()`, or `logout()`. */
|
|
11
13
|
type WritableCustomerSessionManager = ShopifyRouteSessionManager;
|
|
12
|
-
/** Options for
|
|
14
|
+
/** Options for building a Shopify OAuth login URL. */
|
|
13
15
|
type PrepareLoginUrlOptions = {
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* Your storefront's origin for the OAuth redirect URL. Defaults to the origin that the session manager returns.
|
|
18
|
+
*
|
|
19
|
+
* The origin must use HTTPS, including on localhost. Use a public HTTPS tunnel for local sign-in.
|
|
18
20
|
*/
|
|
19
21
|
origin?: string;
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
22
|
-
* Cross-origin values and values over 2 048 bytes fall back to the default, `"/account"`.
|
|
23
|
+
* The same-origin path or URL to send the customer to after sign-in, such as `/account/orders`. Defaults to `/account`. Cross-origin values and paths over 2,048 bytes fall back to the default.
|
|
23
24
|
*/
|
|
24
|
-
returnTo?: string; /**
|
|
25
|
-
locale?: string; /**
|
|
26
|
-
countryCode?: string; /**
|
|
27
|
-
loginHint?: string; /**
|
|
28
|
-
loginHintMode?: string; /**
|
|
25
|
+
returnTo?: string; /** The language for Shopify's login page, sent as the `locale` search param. */
|
|
26
|
+
locale?: string; /** The customer's country, sent as the `region_country` search param. */
|
|
27
|
+
countryCode?: string; /** A value that prefills Shopify's login page, such as the customer's email address, sent as the `login_hint` search param. */
|
|
28
|
+
loginHint?: string; /** How Shopify's login page uses the login hint, such as `submit`, sent as the `login_hint_mode` search param. Hydrogen sends the value only with `loginHint`. */
|
|
29
|
+
loginHintMode?: string; /** The authentication context class to request, sent as the `acr_values` search param. */
|
|
29
30
|
acrValues?: string;
|
|
30
31
|
};
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
-
* and {@link CustomerSession.logout}. Falls back to the session manager's
|
|
34
|
-
* `getSessionOrigin()`.
|
|
33
|
+
* The storefront origin for a token refresh or logout.
|
|
35
34
|
*/
|
|
36
35
|
type RequestOriginOptions = {
|
|
37
|
-
/** HTTPS origin
|
|
36
|
+
/** Your storefront's HTTPS origin. Defaults to the origin that the session manager returns. A non-HTTPS origin throws. */origin?: string;
|
|
38
37
|
};
|
|
39
|
-
/** Options for
|
|
40
|
-
|
|
38
|
+
/** Options for logging the customer out. */
|
|
39
|
+
interface LogoutOptions extends RequestOriginOptions {
|
|
41
40
|
/**
|
|
42
|
-
* URL to
|
|
43
|
-
* Resolved as an absolute same-origin URL. Defaults to `"/"`.
|
|
41
|
+
* The URL to send the customer to after logout. A relative URL resolves against the origin. Defaults to the origin, and a cross-origin URL falls back to the origin.
|
|
44
42
|
*/
|
|
45
43
|
postLogoutRedirectUri?: string;
|
|
46
|
-
}
|
|
44
|
+
}
|
|
47
45
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* Signs customers in and out with Shopify customer accounts, and gives your server their Customer Account API access tokens. Use the session on the server only.
|
|
47
|
+
*
|
|
48
|
+
* Pass your session storage and the request context to each method. Each method makes the final response private and uncacheable.
|
|
49
|
+
*
|
|
50
|
+
* @publicDocs
|
|
50
51
|
*/
|
|
51
52
|
type CustomerSession = {
|
|
52
53
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* Returns `true` when the customer has a usable access token or a refresh token. The method never refreshes tokens.
|
|
55
|
+
*
|
|
56
|
+
* Use the result for UI state, such as an account link. Gate Customer Account API data on an access token from `getOrRefreshAccessToken()`.
|
|
56
57
|
*/
|
|
57
58
|
isLoggedIn(sessionManager: ReadonlyCustomerSessionManager, requestContext: ShopifyRequestContext): Promise<boolean>;
|
|
58
59
|
/**
|
|
59
|
-
* Returns
|
|
60
|
-
* is missing or expired. Never refreshes and never writes session state.
|
|
60
|
+
* Returns the customer's stored access token, or `undefined` when the token is missing or expired. The method never refreshes the token or writes to the session. Call `getOrRefreshAccessToken()` to refresh an expired token.
|
|
61
61
|
*/
|
|
62
62
|
getAccessToken(sessionManager: ReadonlyCustomerSessionManager, requestContext: ShopifyRequestContext): Promise<string | undefined>;
|
|
63
63
|
/**
|
|
64
|
-
* Returns a usable access token,
|
|
65
|
-
*
|
|
66
|
-
* Returns `undefined` when no refresh token
|
|
67
|
-
* the refresh fails transiently (the session is preserved for retry). A
|
|
68
|
-
* 400/401 from the token endpoint clears stored tokens. Never throws for
|
|
69
|
-
* refresh failures. Concurrent refresh requests for the same origin+token
|
|
70
|
-
* combination are deduplicated per `createCustomerSession` instance. Use only
|
|
71
|
-
* where the eventual response commits the session manager.
|
|
64
|
+
* Returns a usable access token for the customer. When the stored token is missing or expired, the session gets new tokens from Shopify and writes them to your session storage. Call the method only where you commit the session to the response.
|
|
65
|
+
*
|
|
66
|
+
* Returns `undefined` when the customer has no refresh token, or when a temporary refresh failure keeps the session for a retry. When Shopify rejects the refresh token with a `400` or `401` status, the session clears the stored tokens and returns `undefined`. Refresh failures never throw.
|
|
72
67
|
*/
|
|
73
68
|
getOrRefreshAccessToken(sessionManager: WritableCustomerSessionManager, requestContext: ShopifyRequestContext, options?: RequestOriginOptions): Promise<string | undefined>;
|
|
74
69
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
70
|
+
* Starts customer sign-in and returns the URL of Shopify's login page. Commit the session to the response, then redirect the customer to the URL.
|
|
71
|
+
*
|
|
72
|
+
* The customer has 10 minutes to finish signing in. After that, the OAuth callback fails with the `missing_pending_login` code.
|
|
78
73
|
*/
|
|
79
74
|
prepareLoginUrl(sessionManager: WritableCustomerSessionManager, requestContext: ShopifyRequestContext, options: PrepareLoginUrlOptions): Promise<string>;
|
|
80
75
|
/**
|
|
81
|
-
* Completes
|
|
82
|
-
* the code for tokens, validates the `id_token` claims (nonce, issuer, audience,
|
|
83
|
-
* expiry), and stores tokens in the session. Returns the `returnTo` path. Clears
|
|
84
|
-
* pending login state on failure.
|
|
76
|
+
* Completes sign-in from Shopify's OAuth callback request, saves the customer's tokens to your session storage, and returns the path to send the customer to. Commit the session to the response after the call.
|
|
85
77
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @throws {CustomerAccountApiError}
|
|
78
|
+
* The method throws `CustomerAccountOAuthError` when sign-in fails a check, and `CustomerAccountApiError` when the token request times out. After a failure, the customer needs to start sign-in again.
|
|
79
|
+
*
|
|
80
|
+
* @throws {CustomerAccountOAuthError} When the callback parameters, pending login, token exchange, token response, or ID token claims fail validation.
|
|
81
|
+
* @throws {CustomerAccountApiError} When the token request times out.
|
|
90
82
|
*/
|
|
91
83
|
handleOAuthCallback(sessionManager: WritableCustomerSessionManager, requestContext: ShopifyRequestContext, request: Request): Promise<string>;
|
|
92
84
|
/**
|
|
93
|
-
* Clears session
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* logout, no IdP round-trip).
|
|
85
|
+
* Clears the customer's session data and returns the URL to send the customer to. Commit the session to the response after the call.
|
|
86
|
+
*
|
|
87
|
+
* When the session has an ID token, the URL points to Shopify's logout endpoint with the post-logout URL attached. Without an ID token, the method returns the post-logout URL.
|
|
97
88
|
*/
|
|
98
89
|
logout(sessionManager: WritableCustomerSessionManager, requestContext: ShopifyRequestContext, options?: LogoutOptions): Promise<string>;
|
|
99
90
|
};
|
|
@@ -3,13 +3,29 @@ import { CUSTOMER_ACCOUNT_PATHS } from "../core/url.mjs";
|
|
|
3
3
|
|
|
4
4
|
//#region src/customer-account/session.ts
|
|
5
5
|
const log = getLogger("customer-account");
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* OAuth callback route path: `"/account/authorize"`.
|
|
8
|
+
*
|
|
9
|
+
* @publicDocs
|
|
10
|
+
*/
|
|
7
11
|
const CUSTOMER_ACCOUNT_AUTHORIZE_PATH = CUSTOMER_ACCOUNT_PATHS.authorize;
|
|
8
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Login route path: `"/account/login"`.
|
|
14
|
+
*
|
|
15
|
+
* @publicDocs
|
|
16
|
+
*/
|
|
9
17
|
const CUSTOMER_ACCOUNT_LOGIN_PATH = CUSTOMER_ACCOUNT_PATHS.login;
|
|
10
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Logout route path: `"/account/logout"`.
|
|
20
|
+
*
|
|
21
|
+
* @publicDocs
|
|
22
|
+
*/
|
|
11
23
|
const CUSTOMER_ACCOUNT_LOGOUT_PATH = CUSTOMER_ACCOUNT_PATHS.logout;
|
|
12
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Token refresh route path: `"/account/refresh"`.
|
|
26
|
+
*
|
|
27
|
+
* @publicDocs
|
|
28
|
+
*/
|
|
13
29
|
const CUSTOMER_ACCOUNT_REFRESH_PATH = CUSTOMER_ACCOUNT_PATHS.refresh;
|
|
14
30
|
const PENDING_LOGIN_TTL_IN_MINUTES = 10;
|
|
15
31
|
const SECONDS_PER_MINUTE = 60;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.mjs","names":[],"sources":["../../../src/customer-account/session.ts"],"sourcesContent":["import type { StorefrontClient } from \"../client\";\nimport {\n getCartBuyerIdentitySync,\n type CartBuyerIdentitySync,\n type CartBuyerIdentitySyncSource,\n} from \"../core/cart/buyer-identity-sync\";\nimport { DEFAULT_TIMEOUT_IN_MS } from \"../core/constants\";\nimport { getLogger } from \"../core/logging\";\nimport type { ShopifyRequestContext } from \"../core/request-context\";\nimport {\n createCallableRouteHandler,\n type CallableRouteHandler,\n type ShopifyRouteErrorResult,\n type ShopifyRouteHandlerContext,\n type ShopifyRouteRedirectResult,\n type ShopifyRouteSessionManager,\n} from \"../core/request-routing/registered-routes\";\nimport { CUSTOMER_ACCOUNT_PATHS, getSameOriginPath } from \"../core/url\";\nimport { CustomerAccountApiError, CustomerAccountOAuthError } from \"./errors\";\n\nconst log = getLogger(\"customer-account\");\n\n/** OAuth callback route path: `\"/account/authorize\"`. */\nexport const CUSTOMER_ACCOUNT_AUTHORIZE_PATH = CUSTOMER_ACCOUNT_PATHS.authorize;\n/** Login route path: `\"/account/login\"`. */\nexport const CUSTOMER_ACCOUNT_LOGIN_PATH = CUSTOMER_ACCOUNT_PATHS.login;\n/** Logout route path: `\"/account/logout\"`. */\nexport const CUSTOMER_ACCOUNT_LOGOUT_PATH = CUSTOMER_ACCOUNT_PATHS.logout;\n/** Token refresh route path: `\"/account/refresh\"`. */\nexport const CUSTOMER_ACCOUNT_REFRESH_PATH = CUSTOMER_ACCOUNT_PATHS.refresh;\n\nconst CUSTOMER_ACCOUNT_SESSION_KEY = \"customerAccount\";\nconst DEFAULT_LOGIN_RETURN_TO_PATH = \"/account\";\nconst DEFAULT_POST_LOGIN_REDIRECT_PATHNAME = \"/\";\nconst DEFAULT_POST_LOGOUT_REDIRECT_URI = \"/\";\nconst FAILED_LOGIN_PATH = \"/account?login=failed\";\nconst FORBIDDEN_STATUS = 403;\nconst FORBIDDEN_ERROR_CODE = \"forbidden\";\nconst FORBIDDEN_ERROR_MESSAGE = \"Forbidden\";\nconst NO_STORE_CACHE_CONTROL = \"no-store\";\nconst AUTHORIZATION_CODE_GRANT_TYPE = \"authorization_code\";\nconst REFRESH_TOKEN_GRANT_TYPE = \"refresh_token\";\nconst CUSTOMER_ACCOUNT_SCOPE = \"openid email customer-account-api:full\";\nconst CODE_CHALLENGE_METHOD = \"S256\";\nconst USER_AGENT = `Hydrogen ${__HYDROGEN_VERSION__}`;\nconst EXPIRY_BUFFER_IN_SECONDS = 120;\nconst PENDING_LOGIN_TTL_IN_MINUTES = 10;\nconst SECONDS_PER_MINUTE = 60;\nconst MILLISECONDS_PER_SECOND = 1_000;\nconst PENDING_LOGIN_TTL_IN_MS =\n PENDING_LOGIN_TTL_IN_MINUTES * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;\nconst RANDOM_BYTES_LENGTH = 32;\nconst MAX_RETURN_TO_LENGTH_IN_BYTES = 2_048;\nconst MAX_SET_TIMEOUT_IN_MS = 2_147_483_647;\nconst SHOP_ID_RE = /^\\d+$/;\nconst CUSTOMER_SESSION_ACCESS_TOKEN_PERSONALIZATION_REASON = \"customer-session-access-token\";\nconst CUSTOMER_SESSION_MUTATION_PERSONALIZATION_REASON = \"customer-session-mutation\";\nconst CUSTOMER_SESSION_INTERNAL_BRAND: unique symbol = Symbol(\"hydrogen.customerSessionInternal\");\n\n/** A value that is either `T` or a `Promise<T>`, used as the return type of session manager methods. */\nexport type Awaitable<T> = T | Promise<T>;\n\n/** Read-only Customer Account session storage for UI state and strict access-token reads. */\nexport type ReadonlyCustomerSessionManager = {\n getSessionItem(key: string): Awaitable<unknown>;\n};\n\n/** Writable Customer Account session storage for OAuth, refresh, and logout response boundaries. */\nexport type WritableCustomerSessionManager = ShopifyRouteSessionManager;\n\ntype CustomerAccountSessionData = {\n tokens?: {\n accessToken?: string;\n refreshToken?: string;\n idToken?: string;\n expiresAt?: number;\n };\n pendingLogin?: {\n state?: string;\n nonce?: string;\n codeVerifier?: string;\n returnTo?: string;\n origin?: string;\n createdAt?: number;\n };\n};\n\n/** Options for {@link createCustomerSession}. */\nexport type CreateCustomerSessionOptions = {\n /** Numeric Shopify shop ID as a string (digits only, e.g. `\"12345\"`). Validated against `/^\\d+$/`. */\n shopId: string;\n /** Customer Account API OAuth client ID. Must be a non-empty string. */\n customerAccountApiClientId: string;\n /**\n * Overrides the default `https://shopify.com/authentication/{shopId}` base URL\n * for OAuth endpoints (authorize, token, logout). Also used as the expected\n * `iss` claim when validating id_tokens — pointing this at a proxy causes\n * `issuer_mismatch` unless the proxy preserves the original issuer. Must use\n * HTTPS; validated at construction.\n */\n customerAccountApiUrl?: string;\n /** Custom fetch implementation. Falls back to `globalThis.fetch` when omitted. */\n fetch?: typeof globalThis.fetch;\n /** Timeout for OAuth token requests in milliseconds. Defaults to 30 000 ms. */\n defaultTimeoutInMs?: number;\n};\n\n/** Options for {@link CustomerSession.prepareLoginUrl}. */\nexport type PrepareLoginUrlOptions = {\n /**\n * Overrides the origin used for the OAuth redirect URI. Must be HTTPS —\n * unlike `createCustomerAccountClient`, `http://localhost` is not accepted.\n * Falls back to the session manager's `getSessionOrigin()`.\n */\n origin?: string;\n /**\n * Same-origin path or URL to redirect back to after login, such as `\"/account/orders\"`.\n * Cross-origin values and values over 2 048 bytes fall back to the default, `\"/account\"`.\n */\n returnTo?: string;\n /** Passed as the `locale` search param on the Shopify OAuth authorize URL. */\n locale?: string;\n /** Passed as the `region_country` search param on the Shopify OAuth authorize URL. */\n countryCode?: string;\n /** Email address to pre-fill on the login screen. Passed as `login_hint`. */\n loginHint?: string;\n /** When set to `\"submit\"` (and `loginHint` is provided), skips the login form and sends the one-time code immediately. Passed as `login_hint_mode`. */\n loginHintMode?: string;\n /** Passed as the `acr_values` search param for authentication context class. */\n acrValues?: string;\n};\n\n/**\n * Origin override accepted by {@link CustomerSession.getOrRefreshAccessToken}\n * and {@link CustomerSession.logout}. Falls back to the session manager's\n * `getSessionOrigin()`.\n */\nexport type RequestOriginOptions = {\n /** HTTPS origin string. */\n origin?: string;\n};\n\n/** Options for {@link CustomerSession.logout}. */\nexport type LogoutOptions = RequestOriginOptions & {\n /**\n * URL to redirect to after Shopify's IdP logout completes.\n * Resolved as an absolute same-origin URL. Defaults to `\"/\"`.\n */\n postLogoutRedirectUri?: string;\n};\n\n/**\n * Core session interface returned by {@link createCustomerSession}. Server-only.\n * All methods require a session manager and request context.\n */\nexport type CustomerSession = {\n /**\n * Read-only signed-in check for UI state. Returns `true` when a usable access\n * token OR a refresh token exists. Does not attempt a refresh. Use for\n * conditional UI rendering, not for authorizing access to Customer Account data.\n */\n isLoggedIn(\n sessionManager: ReadonlyCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n ): Promise<boolean>;\n /**\n * Returns only a currently usable access token, or `undefined` when the token\n * is missing or expired. Never refreshes and never writes session state.\n */\n getAccessToken(\n sessionManager: ReadonlyCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n ): Promise<string | undefined>;\n /**\n * Returns a usable access token, refreshing from Shopify's token endpoint and\n * writing updated tokens through the writable session manager when needed.\n * Returns `undefined` when no refresh token exists (unauthenticated) or when\n * the refresh fails transiently (the session is preserved for retry). A\n * 400/401 from the token endpoint clears stored tokens. Never throws for\n * refresh failures. Concurrent refresh requests for the same origin+token\n * combination are deduplicated per `createCustomerSession` instance. Use only\n * where the eventual response commits the session manager.\n */\n getOrRefreshAccessToken(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options?: RequestOriginOptions,\n ): Promise<string | undefined>;\n /**\n * Builds a Shopify OAuth authorize URL with PKCE (S256). Stores pending login\n * state (state, nonce, code verifier, returnTo, origin) in the session. Pending\n * login state expires after 10 minutes. Returns the full authorize URL string.\n */\n prepareLoginUrl(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: PrepareLoginUrlOptions,\n ): Promise<string>;\n /**\n * Completes the OAuth authorization code exchange. Validates `state`, exchanges\n * the code for tokens, validates the `id_token` claims (nonce, issuer, audience,\n * expiry), and stores tokens in the session. Returns the `returnTo` path. Clears\n * pending login state on failure.\n *\n * @throws {CustomerAccountOAuthError} On validation failures (`missing_callback_params`,\n * `state_mismatch`, `missing_pending_login`, `token_exchange_rejected`,\n * `token_exchange_failed`, `invalid_token_response`, or id_token claim mismatches).\n * @throws {CustomerAccountApiError} On token-request timeout.\n */\n handleOAuthCallback(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n ): Promise<string>;\n /**\n * Clears session tokens. When an `id_token` exists, returns the Shopify IdP\n * logout URL (with `id_token_hint` and `post_logout_redirect_uri`). When no\n * `id_token` is stored, returns the `postLogoutRedirectUri` directly (local-only\n * logout, no IdP round-trip).\n */\n logout(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options?: LogoutOptions,\n ): Promise<string>;\n};\n\n/**\n * Route handler objects returned by {@link createCustomerAccountServerHandlers}.\n * Register these with `handleShopifyRoutes` to serve the Customer Account OAuth flow.\n */\nexport type CustomerAccountServerHandlers<\n TContext extends CustomerAccountRouteHandlerContext = CustomerAccountRouteHandlerContext,\n> = {\n /** GET handler for the OAuth callback path (`/account/authorize`). */\n authorize: CallableRouteHandler<\n TContext,\n CustomerAccountRouteResult,\n typeof CUSTOMER_ACCOUNT_AUTHORIZE_PATH,\n \"GET\"\n >;\n /** GET handler that redirects to Shopify OAuth (`/account/login`). */\n login: CallableRouteHandler<\n TContext,\n CustomerAccountRouteResult,\n typeof CUSTOMER_ACCOUNT_LOGIN_PATH,\n \"GET\"\n >;\n /** POST handler for logout (`/account/logout`). Requires a same-origin POST for CSRF protection. */\n logout: CallableRouteHandler<\n TContext,\n CustomerAccountRouteResult,\n typeof CUSTOMER_ACCOUNT_LOGOUT_PATH,\n \"POST\"\n >;\n /** GET handler that refreshes the access token (`/account/refresh`). */\n refresh: CallableRouteHandler<\n TContext,\n CustomerAccountRouteResult,\n typeof CUSTOMER_ACCOUNT_REFRESH_PATH,\n \"GET\"\n >;\n};\n\n/** Customer Account handlers that also synchronize cart buyer identity, so their context requires `storefrontClient`. */\nexport type CustomerAccountServerHandlersWithCartSync =\n CustomerAccountServerHandlers<ShopifyRouteHandlerContext>;\n\ntype CustomerSessionWithInternals = CustomerSession & {\n readonly [CUSTOMER_SESSION_INTERNAL_BRAND]: true;\n};\n\ntype CreateCustomerAccountServerHandlersBaseOptions<\n TCustomerSession extends CustomerSession = CustomerSession,\n> = {\n /** The session object returned by {@link createCustomerSession}. */\n customerSession: TCustomerSession;\n /** Path to redirect to after a successful login when `return_to` is missing or cross-origin. Defaults to `\"/\"`. */\n defaultPostLoginRedirectPathname?: string;\n /** Same-origin path to redirect to when the OAuth callback throws a `CustomerAccountOAuthError` (other errors propagate). Defaults to `\"/account?login=failed\"`. Cross-origin values fall back to `\"/account\"`. */\n loginFailedRedirectPath?: string;\n /** Static origin string, or a function that resolves the origin per request for dynamic multi-origin setups. */\n origin?: string | ((request: Request) => string);\n /** URL to redirect to after logout. Defaults to `\"/\"`. */\n postLogoutRedirectUri?: string;\n};\n\n/** Options for {@link createCustomerAccountServerHandlers}. */\nexport type CreateCustomerAccountServerHandlersOptions =\n | (CreateCustomerAccountServerHandlersBaseOptions & { cartServerHandlers?: undefined })\n | (CreateCustomerAccountServerHandlersBaseOptions<CustomerSessionWithInternals> & {\n /**\n * Cart server handlers created with `createCartServerHandlers({customerSession})`.\n * When provided, the authorize, refresh, and logout routes keep the browser\n * cart's buyer identity in step with the customer session: attach on login\n * and refresh, detach on logout. Sync is best-effort — failures are logged\n * and never block the route's redirect; a failed detach during logout\n * expires the cart cookie instead.\n */\n cartServerHandlers: CartBuyerIdentitySyncSource;\n });\n\ntype CustomerAccountTokens = NonNullable<CustomerAccountSessionData[\"tokens\"]>;\ntype PendingLogin = NonNullable<CustomerAccountSessionData[\"pendingLogin\"]>;\ntype TokenEndpointResponse = {\n access_token?: unknown;\n refresh_token?: unknown;\n id_token?: unknown;\n expires_in?: unknown;\n};\ntype ParsedTokenEndpointResponse = {\n access_token: string;\n refresh_token?: string;\n id_token?: string;\n expires_in: number;\n};\ntype RefreshResult =\n | { type: \"success\"; tokens: CustomerAccountTokens }\n | { type: \"invalid\" }\n | { type: \"transient\" };\ntype TokenRefreshResult =\n | {\n /** A usable access token is available. */\n status: \"authenticated\";\n accessToken: string;\n }\n | {\n /** Refresh failed transiently, but the refreshable session remains. */\n status: \"transient\";\n accessToken: undefined;\n }\n | {\n /** No usable or refreshable customer session remains. */\n status: \"unauthenticated\";\n accessToken: undefined;\n };\ntype OAuthCallbackResult = {\n location: string;\n accessToken: string;\n};\ntype CustomerSessionInternals = {\n getOrRefreshAccessToken(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options?: RequestOriginOptions,\n ): Promise<TokenRefreshResult>;\n handleOAuthCallback(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n ): Promise<OAuthCallbackResult>;\n};\nconst customerSessionInternals = new WeakMap<CustomerSession, CustomerSessionInternals>();\ntype CustomerAccountRouteResult = ShopifyRouteRedirectResult | ShopifyRouteErrorResult;\ntype CustomerAccountRouteHandlerContext = {\n request: Request;\n sessionManager: WritableCustomerSessionManager;\n requestContext: ShopifyRequestContext;\n};\ntype CustomerAccountRuntimeRouteHandlerContext = CustomerAccountRouteHandlerContext & {\n storefrontClient?: StorefrontClient;\n};\ntype CustomerAccountServerHandlersForOptions<TOptions> = TOptions extends {\n cartServerHandlers: CartBuyerIdentitySyncSource;\n}\n ? CustomerAccountServerHandlersWithCartSync\n : CustomerAccountServerHandlers;\ntype TokenRequestParams = {\n url: string;\n origin: string;\n body: URLSearchParams;\n fetch: typeof globalThis.fetch;\n signal: AbortSignal;\n};\n\n/**\n * Creates a {@link CustomerSession} that manages the Shopify Customer Account\n * OAuth flow (PKCE, token exchange, refresh, logout). Server-only: throws in a\n * browser context. Validates `shopId`, `customerAccountApiClientId`,\n * `customerAccountApiUrl` (HTTPS), and timeout at construction.\n *\n * @example\n * ```ts\n * import { createCustomerSession } from '@shopify/hydrogen/customer-account';\n *\n * const customerSession = createCustomerSession({\n * shopId: env.SHOP_ID,\n * customerAccountApiClientId: env.CUSTOMER_ACCOUNT_API_CLIENT_ID,\n * });\n * ```\n *\n * @throws {Error} When called in a browser context, when an option fails validation, or when no `fetch` is available.\n */\nexport function createCustomerSession({\n shopId,\n customerAccountApiClientId,\n customerAccountApiUrl,\n fetch: customFetch,\n defaultTimeoutInMs = DEFAULT_TIMEOUT_IN_MS,\n}: CreateCustomerSessionOptions): CustomerSessionWithInternals {\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"Customer Account OAuth sessions cannot be used in a browser context. Use this helper from server or edge routes only.\",\n );\n }\n\n validateShopId(shopId);\n validateCustomerAccountApiClientId(customerAccountApiClientId);\n validateTimeout(defaultTimeoutInMs);\n\n const fetch = customFetch ?? globalThis.fetch;\n if (typeof fetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n\n const endpoints = createCustomerAccountEndpoints(shopId, customerAccountApiUrl);\n const refreshFlights = new Map<string, Promise<RefreshResult>>();\n\n async function getAccessToken(\n sessionManager: ReadonlyCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n ) {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_ACCESS_TOKEN_PERSONALIZATION_REASON);\n const accessToken = getUsableAccessToken(await readSessionData(sessionManager));\n return accessToken;\n }\n\n async function getOrRefreshAccessToken(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: RequestOriginOptions = {},\n ) {\n const result = await getOrRefreshAccessTokenResult(sessionManager, requestContext, options);\n return result.accessToken;\n }\n\n async function getOrRefreshAccessTokenResult(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: RequestOriginOptions = {},\n ): Promise<TokenRefreshResult> {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_ACCESS_TOKEN_PERSONALIZATION_REASON);\n const sessionData = await readSessionData(sessionManager);\n const accessToken = getUsableAccessToken(sessionData);\n if (accessToken) return { status: \"authenticated\", accessToken };\n\n const refreshToken = getRefreshToken(sessionData);\n if (!refreshToken) return { status: \"unauthenticated\", accessToken: undefined };\n\n const origin = await getResolvedOrigin(sessionManager, options.origin);\n const idToken = sessionData.tokens?.idToken;\n const refreshResult = await getRefreshResult({\n refreshFlights,\n refreshToken,\n idToken,\n origin,\n endpoints,\n customerAccountApiClientId,\n fetch,\n timeoutInMs: defaultTimeoutInMs,\n });\n\n if (refreshResult.type === \"success\") {\n const refreshedAccessToken = refreshResult.tokens.accessToken;\n if (!refreshedAccessToken) {\n return { status: \"transient\", accessToken: undefined };\n }\n await writeTokens(sessionManager, refreshResult.tokens);\n return {\n status: \"authenticated\",\n accessToken: refreshedAccessToken,\n };\n }\n\n if (refreshResult.type === \"invalid\") {\n await clearTokens(sessionManager);\n return { status: \"unauthenticated\", accessToken: undefined };\n }\n\n return { status: \"transient\", accessToken: undefined };\n }\n\n async function prepareLoginUrl(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: PrepareLoginUrlOptions,\n ) {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_MUTATION_PERSONALIZATION_REASON);\n const origin = await getResolvedOrigin(sessionManager, options.origin);\n const state = generateRandomBase64Url();\n const nonce = generateRandomBase64Url();\n const codeVerifier = generateRandomBase64Url();\n const codeChallenge = await createCodeChallenge(codeVerifier);\n const returnTo = sanitizeReturnTo(options.returnTo, origin);\n const createdAt = Date.now();\n\n const sessionData = await readSessionData(sessionManager);\n await writeSessionData(sessionManager, {\n ...sessionData,\n pendingLogin: { state, nonce, codeVerifier, returnTo, origin, createdAt },\n });\n\n const loginUrl = new URL(endpoints.authorizeUrl);\n loginUrl.searchParams.set(\"client_id\", customerAccountApiClientId);\n loginUrl.searchParams.set(\"scope\", CUSTOMER_ACCOUNT_SCOPE);\n loginUrl.searchParams.set(\"response_type\", \"code\");\n loginUrl.searchParams.set(\"redirect_uri\", `${origin}${CUSTOMER_ACCOUNT_AUTHORIZE_PATH}`);\n loginUrl.searchParams.set(\"state\", state);\n loginUrl.searchParams.set(\"nonce\", nonce);\n loginUrl.searchParams.set(\"code_challenge\", codeChallenge);\n loginUrl.searchParams.set(\"code_challenge_method\", CODE_CHALLENGE_METHOD);\n setOptionalLoginParams(loginUrl, options);\n\n return loginUrl.toString();\n }\n\n async function handleOAuthCallback(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n ) {\n return (await handleOAuthCallbackResult(sessionManager, requestContext, request)).location;\n }\n\n async function handleOAuthCallbackResult(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n ): Promise<OAuthCallbackResult> {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_MUTATION_PERSONALIZATION_REASON);\n try {\n return await completeOAuthCallback({\n sessionManager,\n request,\n endpoints,\n customerAccountApiClientId,\n fetch,\n timeoutInMs: defaultTimeoutInMs,\n });\n } catch (error) {\n await clearPendingLogin(sessionManager);\n throw error;\n }\n }\n\n async function logout(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: LogoutOptions = {},\n ) {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_MUTATION_PERSONALIZATION_REASON);\n const origin = await getResolvedOrigin(sessionManager, options.origin);\n const sessionData = await readSessionData(sessionManager);\n const idToken = sessionData.tokens?.idToken;\n const postLogoutRedirectUri = absoluteSameOriginUrl(\n options.postLogoutRedirectUri ?? origin,\n origin,\n );\n\n await sessionManager.removeSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY);\n\n if (!idToken) return postLogoutRedirectUri;\n\n const logoutUrl = new URL(endpoints.logoutUrl);\n logoutUrl.searchParams.set(\"id_token_hint\", idToken);\n logoutUrl.searchParams.set(\"post_logout_redirect_uri\", postLogoutRedirectUri);\n return logoutUrl.toString();\n }\n\n const customerSession: CustomerSessionWithInternals = {\n [CUSTOMER_SESSION_INTERNAL_BRAND]: true,\n isLoggedIn: async (sessionManager, requestContext) => {\n requestContext.markResponseAsPersonalized(\n CUSTOMER_SESSION_ACCESS_TOKEN_PERSONALIZATION_REASON,\n );\n return hasCustomerSession(await readSessionData(sessionManager));\n },\n getAccessToken,\n getOrRefreshAccessToken,\n prepareLoginUrl,\n handleOAuthCallback,\n logout,\n };\n customerSessionInternals.set(customerSession, {\n getOrRefreshAccessToken: getOrRefreshAccessTokenResult,\n handleOAuthCallback: handleOAuthCallbackResult,\n });\n return customerSession;\n}\n\nexport async function getCustomerSessionRefreshResult(\n customerSession: CustomerSession,\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: RequestOriginOptions = {},\n): Promise<TokenRefreshResult | undefined> {\n return customerSessionInternals\n .get(customerSession)\n ?.getOrRefreshAccessToken(sessionManager, requestContext, options);\n}\n\n/**\n * Creates Customer Account server handlers (`/account/login`, `/account/authorize`,\n * `/account/refresh`, `/account/logout`) for `handleShopifyRoutes`. The handlers\n * return redirect results: `login` redirects to Shopify's OAuth, `logout` redirects\n * to Shopify's logout endpoint when an `id_token` exists, and `authorize`/`refresh`\n * redirect back to the app (same-origin `return_to`). Invoke these paths via\n * full-page navigation (plain `<a>`/`<form>`), not a framework client-side\n * navigation component — client-nav cannot follow these raw redirects.\n */\nexport function createCustomerAccountServerHandlers<\n const TOptions extends CreateCustomerAccountServerHandlersOptions,\n>(options: TOptions): CustomerAccountServerHandlersForOptions<TOptions>;\nexport function createCustomerAccountServerHandlers(\n options: CreateCustomerAccountServerHandlersOptions,\n): CustomerAccountServerHandlers | CustomerAccountServerHandlersWithCartSync {\n const {\n customerSession,\n defaultPostLoginRedirectPathname = DEFAULT_POST_LOGIN_REDIRECT_PATHNAME,\n loginFailedRedirectPath = FAILED_LOGIN_PATH,\n postLogoutRedirectUri = DEFAULT_POST_LOGOUT_REDIRECT_URI,\n } = options;\n const { origin: originOption } = options;\n const cartSync = resolveCartBuyerIdentitySync(options);\n\n return {\n authorize: createCallableRouteHandler(\n CUSTOMER_ACCOUNT_AUTHORIZE_PATH,\n \"GET\",\n async (context: CustomerAccountRuntimeRouteHandlerContext) => {\n return handleAuthorizeRoute(\n customerSession,\n context,\n loginFailedRedirectPath,\n originOption,\n cartSync,\n );\n },\n ),\n login: createCallableRouteHandler(\n CUSTOMER_ACCOUNT_LOGIN_PATH,\n \"GET\",\n async (context: CustomerAccountRuntimeRouteHandlerContext) => {\n const { request, sessionManager, requestContext } = context;\n return handleLoginRoute(\n customerSession,\n sessionManager,\n requestContext,\n request,\n defaultPostLoginRedirectPathname,\n originOption,\n );\n },\n ),\n logout: createCallableRouteHandler(\n CUSTOMER_ACCOUNT_LOGOUT_PATH,\n \"POST\",\n async (context: CustomerAccountRuntimeRouteHandlerContext) => {\n return handleLogoutRoute(\n customerSession,\n context,\n postLogoutRedirectUri,\n originOption,\n cartSync,\n );\n },\n ),\n refresh: createCallableRouteHandler(\n CUSTOMER_ACCOUNT_REFRESH_PATH,\n \"GET\",\n async (context: CustomerAccountRuntimeRouteHandlerContext) => {\n return handleRefreshRoute(customerSession, context, originOption, cartSync);\n },\n ),\n };\n}\n\nfunction resolveCartBuyerIdentitySync(\n options: CreateCustomerAccountServerHandlersOptions,\n): CartBuyerIdentitySync | undefined {\n if (!options.cartServerHandlers) return undefined;\n\n const cartSync = getCartBuyerIdentitySync(options.cartServerHandlers);\n if (!cartSync) {\n throw new Error(\n \"cartServerHandlers must be created by createCartServerHandlers with the customerSession option.\",\n );\n }\n if (!customerSessionInternals.has(options.customerSession)) {\n throw new Error(\n \"Cart buyer identity sync requires the customerSession returned by createCustomerSession.\",\n );\n }\n return cartSync;\n}\n\nasync function handleLoginRoute(\n customerSession: CustomerSession,\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n defaultPostLoginRedirectPathname: string,\n originOption: string | ((request: Request) => string) | undefined,\n): Promise<CustomerAccountRouteResult> {\n const origin = await resolveRouteOrigin(sessionManager, request, originOption);\n const requestUrl = new URL(request.url);\n const defaultReturnTo = sanitizeReturnTo(\n defaultPostLoginRedirectPathname,\n origin,\n DEFAULT_POST_LOGIN_REDIRECT_PATHNAME,\n );\n const requestedReturnTo =\n requestUrl.searchParams.get(\"return_to\") ?? requestUrl.searchParams.get(\"returnTo\");\n const returnTo = sanitizeReturnTo(requestedReturnTo, origin, defaultReturnTo);\n const loginUrl = await customerSession.prepareLoginUrl(sessionManager, requestContext, {\n origin,\n returnTo,\n countryCode: requestContext.i18n.country,\n locale: getOptionalSearchParam(requestUrl, \"locale\"),\n acrValues: getOptionalSearchParam(requestUrl, \"acr_values\"),\n loginHint: getOptionalSearchParam(requestUrl, \"login_hint\"),\n loginHintMode: getOptionalSearchParam(requestUrl, \"login_hint_mode\"),\n });\n return redirectResult(loginUrl, await commitSession(sessionManager));\n}\n\nasync function handleLogoutRoute(\n customerSession: CustomerSession,\n context: CustomerAccountRuntimeRouteHandlerContext,\n postLogoutRedirectUri: string,\n originOption: string | ((request: Request) => string) | undefined,\n cartSync: CartBuyerIdentitySync | undefined,\n): Promise<CustomerAccountRouteResult> {\n const { request, sessionManager, requestContext } = context;\n const origin = await resolveRouteOrigin(sessionManager, request, originOption);\n if (!isSameOriginPost(request, origin)) return forbiddenResult();\n\n const requestUrl = new URL(request.url);\n const requestedReturnTo =\n requestUrl.searchParams.get(\"return_to\") ?? requestUrl.searchParams.get(\"returnTo\");\n const logoutUrl = await customerSession.logout(sessionManager, requestContext, {\n origin,\n postLogoutRedirectUri: sanitizeReturnTo(requestedReturnTo, origin, postLogoutRedirectUri),\n });\n\n // The redirect must complete even when detach fails: skipping it would leave\n // the Shopify IdP session alive after the app session is already destroyed.\n // Expiring the cart cookie is the fail-safe when the cart keeps the identity.\n let detachFailed = false;\n if (cartSync) {\n detachFailed = !(await syncCartBuyerIdentity(cartSync, context, null, \"logout\"));\n }\n const headers = new Headers(await commitSession(sessionManager));\n if (cartSync && detachFailed) headers.append(\"set-cookie\", cartSync.expiredCartCookie);\n return redirectResult(logoutUrl, headers);\n}\n\nasync function handleAuthorizeRoute(\n customerSession: CustomerSession,\n context: CustomerAccountRuntimeRouteHandlerContext,\n loginFailedRedirectPath: string,\n originOption: string | ((request: Request) => string) | undefined,\n cartSync: CartBuyerIdentitySync | undefined,\n): Promise<CustomerAccountRouteResult> {\n const { request, sessionManager, requestContext } = context;\n try {\n if (!cartSync) {\n const location = await customerSession.handleOAuthCallback(\n sessionManager,\n requestContext,\n request,\n );\n return redirectResult(location, await commitSession(sessionManager));\n }\n\n const { location, accessToken } = await handleOAuthCallbackWithAccessToken(\n customerSession,\n sessionManager,\n requestContext,\n request,\n );\n // Best-effort: the customer is authenticated at this point, so a failed\n // cart sync must not turn a successful login into an error response. The\n // next refresh retries the attach.\n await syncCartBuyerIdentity(cartSync, context, accessToken, \"authorize\");\n return redirectResult(location, await commitSession(sessionManager));\n } catch (error) {\n if (!(error instanceof CustomerAccountOAuthError)) throw error;\n const origin = await resolveRouteOrigin(sessionManager, request, originOption);\n return redirectResult(\n sanitizeReturnTo(loginFailedRedirectPath, origin),\n await commitSession(sessionManager),\n );\n }\n}\n\nasync function handleRefreshRoute(\n customerSession: CustomerSession,\n context: CustomerAccountRuntimeRouteHandlerContext,\n originOption: string | ((request: Request) => string) | undefined,\n cartSync: CartBuyerIdentitySync | undefined,\n): Promise<CustomerAccountRouteResult> {\n const { request, sessionManager, requestContext } = context;\n const origin = await resolveRouteOrigin(sessionManager, request, originOption);\n if (!cartSync) {\n await customerSession.getOrRefreshAccessToken(sessionManager, requestContext, { origin });\n return refreshRedirectResult(request, origin, await commitSession(sessionManager));\n }\n\n const refreshResult = await getOrRefreshAccessTokenWithStatus(\n customerSession,\n sessionManager,\n requestContext,\n { origin },\n );\n // Transient refresh failures keep the refreshable session, so the cart's\n // identity is left untouched; a definitive outcome attaches or detaches it.\n let detachFailed = false;\n if (refreshResult.status !== \"transient\") {\n const customerAccessToken = refreshResult.accessToken ?? null;\n const syncSucceeded = await syncCartBuyerIdentity(\n cartSync,\n context,\n customerAccessToken,\n \"refresh\",\n );\n detachFailed = customerAccessToken === null && !syncSucceeded;\n }\n\n const headers = new Headers(await commitSession(sessionManager));\n if (detachFailed) headers.append(\"set-cookie\", cartSync.expiredCartCookie);\n return refreshRedirectResult(request, origin, headers);\n}\n\nfunction refreshRedirectResult(\n request: Request,\n origin: string,\n headers?: HeadersInit,\n): ShopifyRouteRedirectResult {\n const requestUrl = new URL(request.url);\n const returnTo =\n requestUrl.searchParams.get(\"return_to\") ?? requestUrl.searchParams.get(\"returnTo\");\n return redirectResult(sanitizeReturnTo(returnTo, origin), headers);\n}\n\nasync function handleOAuthCallbackWithAccessToken(\n customerSession: CustomerSession,\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n): Promise<OAuthCallbackResult> {\n const internal = customerSessionInternals.get(customerSession);\n if (!internal) {\n throw new Error(\"Customer session was not created by createCustomerSession\");\n }\n return internal.handleOAuthCallback(sessionManager, requestContext, request);\n}\n\nasync function getOrRefreshAccessTokenWithStatus(\n customerSession: CustomerSession,\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: RequestOriginOptions,\n): Promise<TokenRefreshResult> {\n const internal = customerSessionInternals.get(customerSession);\n if (!internal) {\n throw new Error(\"Customer session was not created by createCustomerSession\");\n }\n return internal.getOrRefreshAccessToken(sessionManager, requestContext, options);\n}\n\nasync function syncCartBuyerIdentity(\n cartSync: CartBuyerIdentitySync,\n context: CustomerAccountRuntimeRouteHandlerContext,\n customerAccessToken: string | null,\n route: \"authorize\" | \"refresh\" | \"logout\",\n): Promise<boolean> {\n try {\n if (!context.storefrontClient) {\n throw new Error(\n \"Customer Account handlers configured with cartServerHandlers require storefrontClient.\",\n );\n }\n await cartSync.updateBuyerIdentity(\n { request: context.request, storefrontClient: context.storefrontClient },\n customerAccessToken,\n );\n return true;\n } catch (error) {\n log.error(\"cart buyer identity sync failed\", { route, error });\n return false;\n }\n}\n\nasync function resolveRouteOrigin(\n sessionManager: WritableCustomerSessionManager,\n request: Request,\n originOption: string | ((request: Request) => string) | undefined,\n) {\n if (typeof originOption === \"function\") return normalizeOrigin(originOption(request));\n return getResolvedOrigin(sessionManager, originOption);\n}\n\nasync function getResolvedOrigin(\n sessionManager: WritableCustomerSessionManager,\n originOverride: string | undefined,\n) {\n return normalizeOrigin(originOverride ?? (await sessionManager.getSessionOrigin()));\n}\n\nfunction getOptionalSearchParam(url: URL, name: string): string | undefined {\n return url.searchParams.get(name) ?? undefined;\n}\n\nfunction redirectResult(location: string, headers?: HeadersInit): ShopifyRouteRedirectResult {\n const redirectHeaders = new Headers(headers);\n redirectHeaders.set(\"cache-control\", NO_STORE_CACHE_CONTROL);\n return { type: \"redirect\", location, headers: redirectHeaders };\n}\n\nfunction forbiddenResult(): ShopifyRouteErrorResult {\n return {\n type: \"error\",\n status: FORBIDDEN_STATUS,\n error: { code: FORBIDDEN_ERROR_CODE, message: FORBIDDEN_ERROR_MESSAGE },\n headers: { \"cache-control\": NO_STORE_CACHE_CONTROL },\n };\n}\n\nasync function commitSession(\n sessionManager: WritableCustomerSessionManager,\n): Promise<HeadersInit | undefined> {\n return (await sessionManager.commit?.()) ?? undefined;\n}\n\nasync function completeOAuthCallback({\n sessionManager,\n request,\n endpoints,\n customerAccountApiClientId,\n fetch,\n timeoutInMs,\n}: {\n sessionManager: WritableCustomerSessionManager;\n request: Request;\n endpoints: CustomerAccountEndpoints;\n customerAccountApiClientId: string;\n fetch: typeof globalThis.fetch;\n timeoutInMs: number;\n}): Promise<OAuthCallbackResult> {\n const requestUrl = new URL(request.url);\n const code = requestUrl.searchParams.get(\"code\");\n const state = requestUrl.searchParams.get(\"state\");\n const sessionData = await readSessionData(sessionManager);\n const pendingLogin = getPendingLogin(sessionData);\n\n assertOAuthCallbackParams(code, state, pendingLogin);\n\n const origin = pendingLogin.origin ?? getOriginFromRequest(request);\n const tokenResponse = await exchangeAuthorizationCode({\n endpoints,\n customerAccountApiClientId,\n origin,\n code,\n codeVerifier: pendingLogin.codeVerifier,\n fetch,\n timeoutInMs,\n });\n validateIdTokenClaims(tokenResponse.id_token, {\n audience: customerAccountApiClientId,\n issuer: endpoints.issuer,\n nonce: pendingLogin.nonce,\n });\n\n await writeSessionData(sessionManager, {\n tokens: createTokensFromResponse(tokenResponse),\n });\n\n return {\n location: pendingLogin.returnTo ?? DEFAULT_LOGIN_RETURN_TO_PATH,\n accessToken: tokenResponse.access_token,\n };\n}\n\nfunction isSameOriginPost(request: Request, trustedOrigin: string): boolean {\n const origin = request.headers.get(\"origin\");\n if (origin) {\n try {\n return normalizeOrigin(origin) === trustedOrigin;\n } catch {\n return false;\n }\n }\n\n const referer = request.headers.get(\"referer\");\n if (!referer) return false;\n\n try {\n return normalizeOrigin(new URL(referer).origin) === trustedOrigin;\n } catch {\n return false;\n }\n}\n\nfunction assertOAuthCallbackParams(\n code: string | null,\n state: string | null,\n pendingLogin: Required<PendingLogin>,\n): asserts code is string {\n if (!code || !state) {\n throw new CustomerAccountOAuthError(\n \"missing_callback_params\",\n \"OAuth callback is missing code or state\",\n );\n }\n\n if (pendingLogin.state !== state) {\n throw new CustomerAccountOAuthError(\n \"state_mismatch\",\n \"OAuth callback state does not match session state\",\n );\n }\n}\n\nasync function exchangeAuthorizationCode({\n endpoints,\n customerAccountApiClientId,\n origin,\n code,\n codeVerifier,\n fetch,\n timeoutInMs,\n}: {\n endpoints: CustomerAccountEndpoints;\n customerAccountApiClientId: string;\n origin: string;\n code: string;\n codeVerifier: string;\n fetch: typeof globalThis.fetch;\n timeoutInMs: number;\n}): Promise<Required<ParsedTokenEndpointResponse>> {\n const body = new URLSearchParams({\n grant_type: AUTHORIZATION_CODE_GRANT_TYPE,\n client_id: customerAccountApiClientId,\n redirect_uri: `${origin}${CUSTOMER_ACCOUNT_AUTHORIZE_PATH}`,\n code,\n code_verifier: codeVerifier,\n });\n return withTokenRequestTimeout(timeoutInMs, async (signal) => {\n const response = await postTokenRequest({\n url: endpoints.tokenUrl,\n origin,\n body,\n fetch,\n signal,\n });\n\n if (!response.ok) {\n cancelResponseBody(response);\n const errorCode =\n response.status === 400 || response.status === 401\n ? \"token_exchange_rejected\"\n : \"token_exchange_failed\";\n throw new CustomerAccountOAuthError(\n errorCode,\n `Customer Account OAuth token exchange failed with ${response.status}`,\n );\n }\n\n return parseRequiredTokenResponse(await readTokenResponseJson(response, signal));\n });\n}\n\nasync function getRefreshResult({\n refreshFlights,\n refreshToken,\n idToken,\n origin,\n endpoints,\n customerAccountApiClientId,\n fetch,\n timeoutInMs,\n}: {\n refreshFlights: Map<string, Promise<RefreshResult>>;\n refreshToken: string;\n idToken: string | undefined;\n origin: string;\n endpoints: CustomerAccountEndpoints;\n customerAccountApiClientId: string;\n fetch: typeof globalThis.fetch;\n timeoutInMs: number;\n}) {\n const flightKey = `${origin}\\n${refreshToken}`;\n const existingFlight = refreshFlights.get(flightKey);\n if (existingFlight) return existingFlight;\n\n const flight = refreshAccessToken({\n endpoints,\n customerAccountApiClientId,\n refreshToken,\n idToken,\n origin,\n fetch,\n timeoutInMs,\n });\n refreshFlights.set(flightKey, flight);\n\n try {\n return await flight;\n } finally {\n refreshFlights.delete(flightKey);\n }\n}\n\nasync function refreshAccessToken({\n endpoints,\n customerAccountApiClientId,\n refreshToken,\n idToken,\n origin,\n fetch,\n timeoutInMs,\n}: {\n endpoints: CustomerAccountEndpoints;\n customerAccountApiClientId: string;\n refreshToken: string;\n idToken: string | undefined;\n origin: string;\n fetch: typeof globalThis.fetch;\n timeoutInMs: number;\n}): Promise<RefreshResult> {\n const body = new URLSearchParams({\n grant_type: REFRESH_TOKEN_GRANT_TYPE,\n client_id: customerAccountApiClientId,\n refresh_token: refreshToken,\n });\n\n try {\n return await withTokenRequestTimeout(timeoutInMs, async (signal) => {\n const response = await postTokenRequest({\n url: endpoints.tokenUrl,\n origin,\n body,\n fetch,\n signal,\n });\n return readRefreshResult(response, refreshToken, idToken, signal);\n });\n } catch {\n return { type: \"transient\" };\n }\n}\n\nasync function readRefreshResult(\n response: Response,\n currentRefreshToken: string,\n currentIdToken: string | undefined,\n signal: AbortSignal,\n): Promise<RefreshResult> {\n if (response.status === 400 || response.status === 401) {\n cancelResponseBody(response);\n return { type: \"invalid\" };\n }\n\n if (!response.ok) {\n cancelResponseBody(response);\n return { type: \"transient\" };\n }\n\n try {\n const tokenResponse = await readTokenResponseJson(response, signal);\n const parsedTokenResponse = parseRefreshTokenResponse(tokenResponse);\n return {\n type: \"success\",\n tokens: createTokensFromResponse({\n ...parsedTokenResponse,\n refresh_token: parsedTokenResponse.refresh_token ?? currentRefreshToken,\n id_token: parsedTokenResponse.id_token ?? currentIdToken,\n }),\n };\n } catch {\n return { type: \"transient\" };\n }\n}\n\nasync function postTokenRequest({\n url,\n origin,\n body,\n fetch,\n signal,\n}: TokenRequestParams): Promise<Response> {\n return await withAbort(\n fetch(url, {\n method: \"POST\",\n headers: new Headers({\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n \"User-Agent\": USER_AGENT,\n Origin: origin,\n }),\n body,\n cache: \"no-store\",\n redirect: \"manual\",\n signal,\n }),\n signal,\n );\n}\n\nasync function withTokenRequestTimeout<T>(\n timeoutInMs: number,\n operation: (signal: AbortSignal) => Promise<T>,\n): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort(\n new CustomerAccountApiError(\n `Customer Account OAuth request timed out after ${timeoutInMs}ms`,\n ),\n );\n }, timeoutInMs);\n\n try {\n return await operation(controller.signal);\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction withAbort<T>(promise: T | Promise<T>, signal: AbortSignal): Promise<T> {\n if (signal.aborted) return Promise.reject(getAbortReason(signal));\n\n return new Promise<T>((resolve, reject) => {\n const abort = () => {\n signal.removeEventListener(\"abort\", abort);\n reject(getAbortReason(signal));\n };\n signal.addEventListener(\"abort\", abort, { once: true });\n Promise.resolve(promise).then(\n (value) => {\n signal.removeEventListener(\"abort\", abort);\n resolve(value);\n },\n (error) => {\n signal.removeEventListener(\"abort\", abort);\n reject(error);\n },\n );\n });\n}\n\nfunction getAbortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new DOMException(\"signal is aborted without reason\", \"AbortError\");\n}\n\nasync function readTokenResponseJson(\n response: Response,\n signal: AbortSignal,\n): Promise<TokenEndpointResponse> {\n const json = await withAbort(response.json(), signal);\n if (!isObjectRecord(json)) {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response must be an object\",\n );\n }\n return {\n access_token: json.access_token,\n refresh_token: json.refresh_token,\n id_token: json.id_token,\n expires_in: json.expires_in,\n };\n}\n\nfunction parseRequiredTokenResponse(\n response: TokenEndpointResponse,\n): Required<ParsedTokenEndpointResponse> {\n const parsed = parseRefreshTokenResponse(response);\n if (typeof response.refresh_token !== \"string\" || response.refresh_token === \"\") {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response is missing refresh_token\",\n );\n }\n\n if (typeof response.id_token !== \"string\" || response.id_token === \"\") {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response is missing id_token\",\n );\n }\n return { ...parsed, refresh_token: response.refresh_token, id_token: response.id_token };\n}\n\nfunction parseRefreshTokenResponse(response: TokenEndpointResponse): ParsedTokenEndpointResponse {\n if (typeof response.access_token !== \"string\" || response.access_token === \"\") {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response is missing access_token\",\n );\n }\n\n if (\n typeof response.expires_in !== \"number\" ||\n !Number.isFinite(response.expires_in) ||\n response.expires_in <= 0\n ) {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response is missing expires_in\",\n );\n }\n\n return {\n access_token: response.access_token,\n refresh_token:\n typeof response.refresh_token === \"string\" && response.refresh_token !== \"\"\n ? response.refresh_token\n : undefined,\n id_token:\n typeof response.id_token === \"string\" && response.id_token !== \"\"\n ? response.id_token\n : undefined,\n expires_in: response.expires_in,\n };\n}\n\nfunction createTokensFromResponse(response: ParsedTokenEndpointResponse): CustomerAccountTokens {\n return {\n accessToken: response.access_token,\n refreshToken: response.refresh_token,\n idToken: response.id_token,\n expiresAt:\n Date.now() + (response.expires_in - EXPIRY_BUFFER_IN_SECONDS) * MILLISECONDS_PER_SECOND,\n };\n}\n\nfunction validateIdTokenClaims(\n idToken: string,\n expected: { audience: string; issuer: string; nonce: string },\n): void {\n const claims = parseJwtPayload(idToken);\n if (claims.nonce !== expected.nonce) {\n throw new CustomerAccountOAuthError(\n \"nonce_mismatch\",\n \"OAuth id_token nonce does not match session nonce\",\n );\n }\n\n if (claims.iss !== expected.issuer) {\n throw new CustomerAccountOAuthError(\n \"issuer_mismatch\",\n \"OAuth id_token issuer does not match Customer Account issuer\",\n );\n }\n\n if (!hasAudienceClaim(claims.aud, expected.audience)) {\n throw new CustomerAccountOAuthError(\n \"audience_mismatch\",\n \"OAuth id_token audience does not match Customer Account client ID\",\n );\n }\n\n if (\n typeof claims.exp !== \"number\" ||\n claims.exp <= Math.floor(Date.now() / MILLISECONDS_PER_SECOND)\n ) {\n throw new CustomerAccountOAuthError(\"expired_id_token\", \"OAuth id_token is expired\");\n }\n}\n\nfunction parseJwtPayload(idToken: string): Record<string, unknown> {\n try {\n const [, payload] = idToken.split(\".\");\n if (!payload) throw new Error(\"Missing JWT payload\");\n\n const decodedPayload = JSON.parse(base64UrlDecode(payload));\n if (!isObjectRecord(decodedPayload)) throw new Error(\"JWT payload must be an object\");\n\n return decodedPayload;\n } catch (cause) {\n throw new CustomerAccountOAuthError(\n \"invalid_id_token\",\n \"Customer Account OAuth id_token is invalid\",\n { cause },\n );\n }\n}\n\nfunction hasAudienceClaim(audience: unknown, expectedAudience: string): boolean {\n if (audience === expectedAudience) return true;\n if (!Array.isArray(audience)) return false;\n return audience.includes(expectedAudience);\n}\n\nasync function readSessionData(\n sessionManager: ReadonlyCustomerSessionManager,\n): Promise<CustomerAccountSessionData> {\n const value = await sessionManager.getSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY);\n return isCustomerAccountSessionData(value) ? value : {};\n}\n\nasync function writeSessionData(\n sessionManager: WritableCustomerSessionManager,\n sessionData: CustomerAccountSessionData,\n): Promise<void> {\n await sessionManager.setSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY, sessionData);\n}\n\nasync function writeTokens(\n sessionManager: WritableCustomerSessionManager,\n tokens: CustomerAccountTokens,\n): Promise<void> {\n const sessionData = await readSessionData(sessionManager);\n await writeSessionData(sessionManager, { ...sessionData, tokens });\n}\n\nasync function clearTokens(sessionManager: WritableCustomerSessionManager): Promise<void> {\n const sessionData = await readSessionData(sessionManager);\n const { pendingLogin } = sessionData;\n if (pendingLogin) {\n await writeSessionData(sessionManager, { pendingLogin });\n return;\n }\n\n await sessionManager.removeSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY);\n}\n\nasync function clearPendingLogin(sessionManager: WritableCustomerSessionManager): Promise<void> {\n const sessionData = await readSessionData(sessionManager);\n const { tokens } = sessionData;\n if (tokens) {\n await writeSessionData(sessionManager, { tokens });\n return;\n }\n\n await sessionManager.removeSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY);\n}\n\nfunction getUsableAccessToken(sessionData: CustomerAccountSessionData): string | undefined {\n const tokens = sessionData.tokens;\n if (!isTokenUsable(tokens)) return undefined;\n return tokens.accessToken;\n}\n\nfunction hasCustomerSession(sessionData: CustomerAccountSessionData): boolean {\n return (\n getUsableAccessToken(sessionData) !== undefined || getRefreshToken(sessionData) !== undefined\n );\n}\n\nfunction getRefreshToken(sessionData: CustomerAccountSessionData): string | undefined {\n const refreshToken = sessionData.tokens?.refreshToken;\n return typeof refreshToken === \"string\" && refreshToken !== \"\" ? refreshToken : undefined;\n}\n\nfunction getPendingLogin(sessionData: CustomerAccountSessionData): Required<PendingLogin> {\n const pendingLogin = sessionData.pendingLogin;\n if (isPendingLoginReady(pendingLogin)) return pendingLogin;\n throw new CustomerAccountOAuthError(\n \"missing_pending_login\",\n \"OAuth callback has no pending login state\",\n );\n}\n\nfunction isTokenUsable(\n tokens: CustomerAccountSessionData[\"tokens\"],\n): tokens is Required<CustomerAccountTokens> {\n if (!tokens || typeof tokens.accessToken !== \"string\" || tokens.accessToken === \"\") return false;\n if (typeof tokens.expiresAt !== \"number\") return false;\n return tokens.expiresAt > Date.now();\n}\n\nfunction isPendingLoginReady(\n pendingLogin: PendingLogin | undefined,\n): pendingLogin is Required<PendingLogin> {\n if (!pendingLogin) return false;\n if (!isNonEmptyString(pendingLogin.state)) return false;\n if (!isNonEmptyString(pendingLogin.nonce)) return false;\n if (!isNonEmptyString(pendingLogin.codeVerifier)) return false;\n if (!isNonEmptyString(pendingLogin.returnTo)) return false;\n if (!isNonEmptyString(pendingLogin.origin)) return false;\n if (typeof pendingLogin.createdAt !== \"number\") return false;\n return Date.now() - pendingLogin.createdAt <= PENDING_LOGIN_TTL_IN_MS;\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value !== \"\";\n}\n\nfunction isCustomerAccountSessionData(value: unknown): value is CustomerAccountSessionData {\n if (!isObjectRecord(value) || Array.isArray(value)) return false;\n const tokens = value.tokens;\n const pendingLogin = value.pendingLogin;\n if (tokens !== undefined && (!isObjectRecord(tokens) || Array.isArray(tokens))) return false;\n return (\n pendingLogin === undefined || (isObjectRecord(pendingLogin) && !Array.isArray(pendingLogin))\n );\n}\n\nfunction setOptionalLoginParams(loginUrl: URL, options: PrepareLoginUrlOptions): void {\n if (options.locale) loginUrl.searchParams.set(\"locale\", options.locale);\n if (options.countryCode) loginUrl.searchParams.set(\"region_country\", options.countryCode);\n if (options.acrValues) loginUrl.searchParams.set(\"acr_values\", options.acrValues);\n if (!options.loginHint) return;\n\n loginUrl.searchParams.set(\"login_hint\", options.loginHint);\n if (options.loginHintMode) loginUrl.searchParams.set(\"login_hint_mode\", options.loginHintMode);\n}\n\nfunction sanitizeReturnTo(\n returnTo: string | null | undefined,\n origin: string,\n fallbackReturnTo = DEFAULT_LOGIN_RETURN_TO_PATH,\n): string {\n const path = getSameOriginPath(returnTo, origin);\n if (!path || new TextEncoder().encode(path).byteLength > MAX_RETURN_TO_LENGTH_IN_BYTES) {\n return fallbackReturnTo;\n }\n return path;\n}\n\nfunction absoluteSameOriginUrl(url: string, origin: string): string {\n const parsedUrl = new URL(url, origin);\n if (parsedUrl.origin !== origin) return origin;\n return parsedUrl.toString();\n}\n\nasync function createCodeChallenge(codeVerifier: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(codeVerifier));\n return base64UrlEncode(String.fromCharCode(...new Uint8Array(digest)));\n}\n\nfunction generateRandomBase64Url(): string {\n const bytes = new Uint8Array(RANDOM_BYTES_LENGTH);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(String.fromCharCode(...bytes));\n}\n\nfunction base64UrlEncode(value: string): string {\n return btoa(value).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n}\n\nfunction base64UrlDecode(value: string): string {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const paddingLength = (4 - (base64.length % 4)) % 4;\n return atob(base64.padEnd(base64.length + paddingLength, \"=\"));\n}\n\ntype CustomerAccountEndpoints = {\n authorizeUrl: string;\n tokenUrl: string;\n logoutUrl: string;\n issuer: string;\n};\n\nfunction createCustomerAccountEndpoints(\n shopId: string,\n customerAccountApiUrl: string | undefined,\n): CustomerAccountEndpoints {\n const authBaseUrl = normalizeAuthBaseUrl(\n customerAccountApiUrl ?? `https://shopify.com/authentication/${shopId}`,\n );\n return {\n authorizeUrl: `${authBaseUrl}/oauth/authorize`,\n tokenUrl: `${authBaseUrl}/oauth/token`,\n logoutUrl: `${authBaseUrl}/logout`,\n issuer: authBaseUrl,\n };\n}\n\nfunction normalizeAuthBaseUrl(customerAccountApiUrl: string): string {\n const url = new URL(customerAccountApiUrl);\n if (url.protocol !== \"https:\") {\n throw new Error(\"customerAccountApiUrl must use HTTPS\");\n }\n return url.toString().replace(/\\/$/, \"\");\n}\n\nfunction getOriginFromRequest(request: Request): string {\n return normalizeOrigin(new URL(request.url).origin);\n}\n\nfunction normalizeOrigin(origin: string): string {\n const url = new URL(origin);\n if (url.protocol === \"https:\") return url.origin;\n throw new Error(\n \"Customer Account OAuth origin must use HTTPS. Use a public HTTPS tunnel for local Customer Account login.\",\n );\n}\n\nfunction cancelResponseBody(response: Response): void {\n try {\n void response.body?.cancel().catch(() => undefined);\n } catch {}\n}\n\nfunction validateShopId(shopId: string): void {\n if (!SHOP_ID_RE.test(shopId)) {\n throw new Error(\"shopId must be a numeric Shopify shop ID string\");\n }\n}\n\nfunction validateCustomerAccountApiClientId(customerAccountApiClientId: string): void {\n if (typeof customerAccountApiClientId !== \"string\" || customerAccountApiClientId.trim() === \"\") {\n throw new Error(\"customerAccountApiClientId is required\");\n }\n}\n\nfunction validateTimeout(timeoutInMs: number): void {\n if (\n !Number.isSafeInteger(timeoutInMs) ||\n timeoutInMs <= 0 ||\n timeoutInMs > MAX_SET_TIMEOUT_IN_MS\n ) {\n throw new Error(\n `defaultTimeoutInMs must be a positive safe integer no greater than ${MAX_SET_TIMEOUT_IN_MS}`,\n );\n }\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":";;;;AAoBA,MAAM,MAAM,UAAU,mBAAmB;;AAGzC,MAAa,kCAAkC,uBAAuB;;AAEtE,MAAa,8BAA8B,uBAAuB;;AAElE,MAAa,+BAA+B,uBAAuB;;AAEnE,MAAa,gCAAgC,uBAAuB;AAiBpE,MAAM,+BAA+B;AACrC,MAAM,qBAAqB;AAC3B,MAAM,0BAA0B;AAChC,MAAM,0BACJ,+BAA+B,qBAAqB;AA8StD,MAAM,2CAA2B,IAAI,SAAoD;AAgPzF,eAAsB,gCACpB,iBACA,gBACA,gBACA,UAAgC,EAAE,EACO;AACzC,QAAO,yBACJ,IAAI,gBAAgB,EACnB,wBAAwB,gBAAgB,gBAAgB,QAAQ"}
|
|
1
|
+
{"version":3,"file":"session.mjs","names":[],"sources":["../../../src/customer-account/session.ts"],"sourcesContent":["import type { StorefrontClient } from \"../client\";\nimport {\n getCartBuyerIdentitySync,\n type CartBuyerIdentitySync,\n type CartBuyerIdentitySyncSource,\n} from \"../core/cart/buyer-identity-sync\";\nimport { DEFAULT_TIMEOUT_IN_MS } from \"../core/constants\";\nimport { getLogger } from \"../core/logging\";\nimport type { ShopifyRequestContext } from \"../core/request-context\";\nimport {\n createCallableRouteHandler,\n type CallableRouteHandler,\n type ShopifyRouteErrorResult,\n type ShopifyRouteHandlerContext,\n type ShopifyRouteRedirectResult,\n type ShopifyRouteSessionManager,\n} from \"../core/request-routing/registered-routes\";\nimport { CUSTOMER_ACCOUNT_PATHS, getSameOriginPath } from \"../core/url\";\nimport { CustomerAccountApiError, CustomerAccountOAuthError } from \"./errors\";\n\nconst log = getLogger(\"customer-account\");\n\n/**\n * OAuth callback route path: `\"/account/authorize\"`.\n *\n * @publicDocs\n */\nexport const CUSTOMER_ACCOUNT_AUTHORIZE_PATH = CUSTOMER_ACCOUNT_PATHS.authorize;\n/**\n * Login route path: `\"/account/login\"`.\n *\n * @publicDocs\n */\nexport const CUSTOMER_ACCOUNT_LOGIN_PATH = CUSTOMER_ACCOUNT_PATHS.login;\n/**\n * Logout route path: `\"/account/logout\"`.\n *\n * @publicDocs\n */\nexport const CUSTOMER_ACCOUNT_LOGOUT_PATH = CUSTOMER_ACCOUNT_PATHS.logout;\n/**\n * Token refresh route path: `\"/account/refresh\"`.\n *\n * @publicDocs\n */\nexport const CUSTOMER_ACCOUNT_REFRESH_PATH = CUSTOMER_ACCOUNT_PATHS.refresh;\n\nconst CUSTOMER_ACCOUNT_SESSION_KEY = \"customerAccount\";\nconst DEFAULT_LOGIN_RETURN_TO_PATH = \"/account\";\nconst DEFAULT_POST_LOGIN_REDIRECT_PATHNAME = \"/\";\nconst DEFAULT_POST_LOGOUT_REDIRECT_URI = \"/\";\nconst FAILED_LOGIN_PATH = \"/account?login=failed\";\nconst FORBIDDEN_STATUS = 403;\nconst FORBIDDEN_ERROR_CODE = \"forbidden\";\nconst FORBIDDEN_ERROR_MESSAGE = \"Forbidden\";\nconst NO_STORE_CACHE_CONTROL = \"no-store\";\nconst AUTHORIZATION_CODE_GRANT_TYPE = \"authorization_code\";\nconst REFRESH_TOKEN_GRANT_TYPE = \"refresh_token\";\nconst CUSTOMER_ACCOUNT_SCOPE = \"openid email customer-account-api:full\";\nconst CODE_CHALLENGE_METHOD = \"S256\";\nconst USER_AGENT = `Hydrogen ${__HYDROGEN_VERSION__}`;\nconst EXPIRY_BUFFER_IN_SECONDS = 120;\nconst PENDING_LOGIN_TTL_IN_MINUTES = 10;\nconst SECONDS_PER_MINUTE = 60;\nconst MILLISECONDS_PER_SECOND = 1_000;\nconst PENDING_LOGIN_TTL_IN_MS =\n PENDING_LOGIN_TTL_IN_MINUTES * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;\nconst RANDOM_BYTES_LENGTH = 32;\nconst MAX_RETURN_TO_LENGTH_IN_BYTES = 2_048;\nconst MAX_SET_TIMEOUT_IN_MS = 2_147_483_647;\nconst SHOP_ID_RE = /^\\d+$/;\nconst CUSTOMER_SESSION_ACCESS_TOKEN_PERSONALIZATION_REASON = \"customer-session-access-token\";\nconst CUSTOMER_SESSION_MUTATION_PERSONALIZATION_REASON = \"customer-session-mutation\";\nconst CUSTOMER_SESSION_INTERNAL_BRAND: unique symbol = Symbol(\"hydrogen.customerSessionInternal\");\n\n/**\n * A value or a promise for that value. Session manager methods can return either.\n */\nexport type Awaitable<T> = T | Promise<T>;\n\n/** The session storage that `isLoggedIn()` and `getAccessToken()` read. */\nexport type ReadonlyCustomerSessionManager = {\n /** Returns the session value for a key. The customer session keeps its data under the `customerAccount` key. */\n getSessionItem(key: string): Awaitable<unknown>;\n};\n\n/** The session storage that sign-in, the OAuth callback, token refresh, and logout write to. Commit the session to the response after you call `prepareLoginUrl()`, `handleOAuthCallback()`, `getOrRefreshAccessToken()`, or `logout()`. */\nexport type WritableCustomerSessionManager = ShopifyRouteSessionManager;\n\ntype CustomerAccountSessionData = {\n tokens?: {\n accessToken?: string;\n refreshToken?: string;\n idToken?: string;\n expiresAt?: number;\n };\n pendingLogin?: {\n state?: string;\n nonce?: string;\n codeVerifier?: string;\n returnTo?: string;\n origin?: string;\n createdAt?: number;\n };\n};\n\n/** Options for creating a customer session. */\nexport type CreateCustomerSessionOptions = {\n /** Numeric Shopify shop ID as a string of digits, such as `12345`. */\n shopId: string;\n /** The Customer Account API OAuth client ID. Must contain at least one non-whitespace character. */\n customerAccountApiClientId: string;\n /**\n * The base URL for Shopify's OAuth authorize, token, and logout endpoints. Defaults to `https://shopify.com/authentication/{shopId}`.\n *\n * The URL must use HTTPS, or creating the session throws. Sign-in fails with the `issuer_mismatch` code when the ID token's issuer differs from this URL.\n */\n customerAccountApiUrl?: string;\n /** A custom fetch implementation. Defaults to the global fetch. */\n fetch?: typeof globalThis.fetch;\n /** The timeout for OAuth token requests in milliseconds. Must be a positive integer no greater than 2,147,483,647. Defaults to `30000`. */\n defaultTimeoutInMs?: number;\n};\n\n/** Options for building a Shopify OAuth login URL. */\nexport type PrepareLoginUrlOptions = {\n /**\n * Your storefront's origin for the OAuth redirect URL. Defaults to the origin that the session manager returns.\n *\n * The origin must use HTTPS, including on localhost. Use a public HTTPS tunnel for local sign-in.\n */\n origin?: string;\n /**\n * The same-origin path or URL to send the customer to after sign-in, such as `/account/orders`. Defaults to `/account`. Cross-origin values and paths over 2,048 bytes fall back to the default.\n */\n returnTo?: string;\n /** The language for Shopify's login page, sent as the `locale` search param. */\n locale?: string;\n /** The customer's country, sent as the `region_country` search param. */\n countryCode?: string;\n /** A value that prefills Shopify's login page, such as the customer's email address, sent as the `login_hint` search param. */\n loginHint?: string;\n /** How Shopify's login page uses the login hint, such as `submit`, sent as the `login_hint_mode` search param. Hydrogen sends the value only with `loginHint`. */\n loginHintMode?: string;\n /** The authentication context class to request, sent as the `acr_values` search param. */\n acrValues?: string;\n};\n\n/**\n * The storefront origin for a token refresh or logout.\n */\nexport type RequestOriginOptions = {\n /** Your storefront's HTTPS origin. Defaults to the origin that the session manager returns. A non-HTTPS origin throws. */\n origin?: string;\n};\n\n/** Options for logging the customer out. */\nexport interface LogoutOptions extends RequestOriginOptions {\n /**\n * The URL to send the customer to after logout. A relative URL resolves against the origin. Defaults to the origin, and a cross-origin URL falls back to the origin.\n */\n postLogoutRedirectUri?: string;\n}\n\n/**\n * Signs customers in and out with Shopify customer accounts, and gives your server their Customer Account API access tokens. Use the session on the server only.\n *\n * Pass your session storage and the request context to each method. Each method makes the final response private and uncacheable.\n *\n * @publicDocs\n */\nexport type CustomerSession = {\n /**\n * Returns `true` when the customer has a usable access token or a refresh token. The method never refreshes tokens.\n *\n * Use the result for UI state, such as an account link. Gate Customer Account API data on an access token from `getOrRefreshAccessToken()`.\n */\n isLoggedIn(\n sessionManager: ReadonlyCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n ): Promise<boolean>;\n /**\n * Returns the customer's stored access token, or `undefined` when the token is missing or expired. The method never refreshes the token or writes to the session. Call `getOrRefreshAccessToken()` to refresh an expired token.\n */\n getAccessToken(\n sessionManager: ReadonlyCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n ): Promise<string | undefined>;\n /**\n * Returns a usable access token for the customer. When the stored token is missing or expired, the session gets new tokens from Shopify and writes them to your session storage. Call the method only where you commit the session to the response.\n *\n * Returns `undefined` when the customer has no refresh token, or when a temporary refresh failure keeps the session for a retry. When Shopify rejects the refresh token with a `400` or `401` status, the session clears the stored tokens and returns `undefined`. Refresh failures never throw.\n */\n getOrRefreshAccessToken(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options?: RequestOriginOptions,\n ): Promise<string | undefined>;\n /**\n * Starts customer sign-in and returns the URL of Shopify's login page. Commit the session to the response, then redirect the customer to the URL.\n *\n * The customer has 10 minutes to finish signing in. After that, the OAuth callback fails with the `missing_pending_login` code.\n */\n prepareLoginUrl(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: PrepareLoginUrlOptions,\n ): Promise<string>;\n /**\n * Completes sign-in from Shopify's OAuth callback request, saves the customer's tokens to your session storage, and returns the path to send the customer to. Commit the session to the response after the call.\n *\n * The method throws `CustomerAccountOAuthError` when sign-in fails a check, and `CustomerAccountApiError` when the token request times out. After a failure, the customer needs to start sign-in again.\n *\n * @throws {CustomerAccountOAuthError} When the callback parameters, pending login, token exchange, token response, or ID token claims fail validation.\n * @throws {CustomerAccountApiError} When the token request times out.\n */\n handleOAuthCallback(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n ): Promise<string>;\n /**\n * Clears the customer's session data and returns the URL to send the customer to. Commit the session to the response after the call.\n *\n * When the session has an ID token, the URL points to Shopify's logout endpoint with the post-logout URL attached. Without an ID token, the method returns the post-logout URL.\n */\n logout(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options?: LogoutOptions,\n ): Promise<string>;\n};\n\n/**\n * The login, authorize, refresh, and logout routes for customer accounts. Add the object to the `handlers` option of `handleShopifyRoutes`.\n */\nexport type CustomerAccountServerHandlers<\n TContext extends CustomerAccountRouteHandlerContext = CustomerAccountRouteHandlerContext,\n> = {\n /** Completes sign-in at `/account/authorize`, the OAuth callback, and redirects the customer to the return path. Accepts `GET`. */\n authorize: CallableRouteHandler<\n TContext,\n CustomerAccountRouteResult,\n typeof CUSTOMER_ACCOUNT_AUTHORIZE_PATH,\n \"GET\"\n >;\n /** Starts sign-in at `/account/login` and redirects the customer to Shopify's login page. Accepts `GET`. Pass `return_to`, `locale`, `acr_values`, `login_hint`, and `login_hint_mode` as search params. The route sends the request context's country to Shopify. */\n login: CallableRouteHandler<\n TContext,\n CustomerAccountRouteResult,\n typeof CUSTOMER_ACCOUNT_LOGIN_PATH,\n \"GET\"\n >;\n /**\n * Logs the customer out at `/account/logout`. Accepts `POST`. For CSRF protection, the route returns a `403` response unless the `Origin` or `Referer` header matches your storefront's origin.\n *\n * Through `handleShopifyRoutes`, a GET request to the logout path returns a `405` response. Log out with a `<form method=\"post\" action=\"/account/logout\">` and a submit button, which works without JavaScript.\n */\n logout: CallableRouteHandler<\n TContext,\n CustomerAccountRouteResult,\n typeof CUSTOMER_ACCOUNT_LOGOUT_PATH,\n \"POST\"\n >;\n /** Refreshes the access token at `/account/refresh` and redirects the customer to the same-origin `return_to` path, which defaults to `/account`. Accepts `GET`. */\n refresh: CallableRouteHandler<\n TContext,\n CustomerAccountRouteResult,\n typeof CUSTOMER_ACCOUNT_REFRESH_PATH,\n \"GET\"\n >;\n};\n\n/**\n * Customer account routes that also keep the cart's buyer identity in step with the signed-in customer. These routes need `storefrontClient` in the handler context.\n */\nexport type CustomerAccountServerHandlersWithCartSync =\n CustomerAccountServerHandlers<ShopifyRouteHandlerContext>;\n\n/** A customer session that `createCustomerSession` returns. Cart buyer identity sync requires this session. */\ntype CustomerSessionWithInternals = CustomerSession & {\n readonly [CUSTOMER_SESSION_INTERNAL_BRAND]: true;\n};\n\n/** Options for the customer account routes. */\ntype CreateCustomerAccountServerHandlersBaseOptions = {\n /** The path to send the customer to after sign-in when the login request has no same-origin `return_to` search param. Defaults to `/`. */\n defaultPostLoginRedirectPathname?: string;\n /** The same-origin path to send the customer to when sign-in fails with `CustomerAccountOAuthError`. Defaults to `/account?login=failed`. A cross-origin value falls back to `/account`. The authorize route rethrows other errors. */\n loginFailedRedirectPath?: string;\n /** Your storefront's HTTPS origin, or a function that returns the origin for a request. Defaults to the origin that the session manager returns. */\n origin?: string | ((request: Request) => string);\n /** The URL to send the customer to after logout. Defaults to `/`. A same-origin `return_to` search param on the logout request overrides this value. */\n postLogoutRedirectUri?: string;\n};\n\n/** Options for the customer account routes without cart buyer identity sync. */\ninterface CustomerAccountServerHandlersOptionsWithoutCart extends CreateCustomerAccountServerHandlersBaseOptions {\n /** The customer session from `createCustomerSession`. */\n customerSession: CustomerSession;\n /**\n * The cart server handlers that `createCartServerHandlers` returns with its `customerSession` option. Pass the cart handlers to keep the cart's buyer identity in step with the signed-in customer.\n *\n * The authorize and refresh routes attach the customer to the cart. The logout route, and a refresh that ends the session, detach the customer from the cart. When a sync fails, the route logs the error and still redirects. When a detach fails, the route also expires the `cart` cookie. The sync needs `storefrontClient` in the handler context.\n *\n * Creating the handlers throws when the cart handlers lack the `customerSession` option, or when the customer session doesn't come from `createCustomerSession`.\n */\n cartServerHandlers?: undefined;\n}\n\n/** Options for the customer account routes that keep the cart's buyer identity in step with the signed-in customer. */\ninterface CustomerAccountServerHandlersOptionsWithCart extends CreateCustomerAccountServerHandlersBaseOptions {\n /** The customer session from `createCustomerSession`. */\n customerSession: CustomerSessionWithInternals;\n /**\n * The cart server handlers that `createCartServerHandlers` returns with its `customerSession` option. Pass the cart handlers to keep the cart's buyer identity in step with the signed-in customer.\n *\n * The authorize and refresh routes attach the customer to the cart. The logout route, and a refresh that ends the session, detach the customer from the cart. When a sync fails, the route logs the error and still redirects. When a detach fails, the route also expires the `cart` cookie. The sync needs `storefrontClient` in the handler context.\n *\n * Creating the handlers throws when the cart handlers lack the `customerSession` option, or when the customer session doesn't come from `createCustomerSession`.\n */\n cartServerHandlers: CartBuyerIdentitySyncSource;\n}\n\n/** Options for the customer account routes, with optional cart buyer identity sync. */\nexport type CreateCustomerAccountServerHandlersOptions =\n | CustomerAccountServerHandlersOptionsWithoutCart\n | CustomerAccountServerHandlersOptionsWithCart;\n\ntype CustomerAccountTokens = NonNullable<CustomerAccountSessionData[\"tokens\"]>;\ntype PendingLogin = NonNullable<CustomerAccountSessionData[\"pendingLogin\"]>;\ntype TokenEndpointResponse = {\n access_token?: unknown;\n refresh_token?: unknown;\n id_token?: unknown;\n expires_in?: unknown;\n};\ntype ParsedTokenEndpointResponse = {\n access_token: string;\n refresh_token?: string;\n id_token?: string;\n expires_in: number;\n};\ntype RefreshResult =\n | { type: \"success\"; tokens: CustomerAccountTokens }\n | { type: \"invalid\" }\n | { type: \"transient\" };\ntype TokenRefreshResult =\n | {\n /** A usable access token is available. */\n status: \"authenticated\";\n accessToken: string;\n }\n | {\n /** Refresh failed transiently, but the refreshable session remains. */\n status: \"transient\";\n accessToken: undefined;\n }\n | {\n /** No usable or refreshable customer session remains. */\n status: \"unauthenticated\";\n accessToken: undefined;\n };\ntype OAuthCallbackResult = {\n location: string;\n accessToken: string;\n};\ntype CustomerSessionInternals = {\n getOrRefreshAccessToken(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options?: RequestOriginOptions,\n ): Promise<TokenRefreshResult>;\n handleOAuthCallback(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n ): Promise<OAuthCallbackResult>;\n};\nconst customerSessionInternals = new WeakMap<CustomerSession, CustomerSessionInternals>();\n/** A redirect, or a `403` error when a logout request comes from another origin. */\ntype CustomerAccountRouteResult = ShopifyRouteRedirectResult | ShopifyRouteErrorResult;\ntype CustomerAccountRouteHandlerContext = {\n request: Request;\n sessionManager: WritableCustomerSessionManager;\n requestContext: ShopifyRequestContext;\n};\ntype CustomerAccountRuntimeRouteHandlerContext = CustomerAccountRouteHandlerContext & {\n storefrontClient?: StorefrontClient;\n};\ntype CustomerAccountServerHandlersForOptions<TOptions> = TOptions extends {\n cartServerHandlers: CartBuyerIdentitySyncSource;\n}\n ? CustomerAccountServerHandlersWithCartSync\n : CustomerAccountServerHandlers;\ntype TokenRequestParams = {\n url: string;\n origin: string;\n body: URLSearchParams;\n fetch: typeof globalThis.fetch;\n signal: AbortSignal;\n};\n\n/**\n * Creates a customer session that signs customers in and out with Shopify customer accounts and manages their access tokens. Use the session on the server only.\n *\n * The function throws when the shop ID, client ID, API URL, or timeout is invalid, when no fetch implementation is available, and when the function runs in a browser.\n *\n * @example\n * ```ts\n * import { createCustomerSession } from '@shopify/hydrogen/customer-account';\n *\n * const customerSession = createCustomerSession({\n * shopId: env.SHOP_ID,\n * customerAccountApiClientId: env.CUSTOMER_ACCOUNT_API_CLIENT_ID,\n * });\n * ```\n *\n * @param options - The shop ID, the OAuth client ID, the authentication base URL, a custom fetch, and the token request timeout.\n * @returns A customer session with methods for sign-in, the OAuth callback, access tokens, and logout.\n * @throws {Error} When called in a browser, when an option fails validation, or when no fetch is available.\n * @publicDocs\n */\nexport function createCustomerSession({\n shopId,\n customerAccountApiClientId,\n customerAccountApiUrl,\n fetch: customFetch,\n defaultTimeoutInMs = DEFAULT_TIMEOUT_IN_MS,\n}: CreateCustomerSessionOptions): CustomerSessionWithInternals {\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"Customer Account OAuth sessions cannot be used in a browser context. Use this helper from server or edge routes only.\",\n );\n }\n\n validateShopId(shopId);\n validateCustomerAccountApiClientId(customerAccountApiClientId);\n validateTimeout(defaultTimeoutInMs);\n\n const fetch = customFetch ?? globalThis.fetch;\n if (typeof fetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n\n const endpoints = createCustomerAccountEndpoints(shopId, customerAccountApiUrl);\n const refreshFlights = new Map<string, Promise<RefreshResult>>();\n\n async function getAccessToken(\n sessionManager: ReadonlyCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n ) {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_ACCESS_TOKEN_PERSONALIZATION_REASON);\n const accessToken = getUsableAccessToken(await readSessionData(sessionManager));\n return accessToken;\n }\n\n async function getOrRefreshAccessToken(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: RequestOriginOptions = {},\n ) {\n const result = await getOrRefreshAccessTokenResult(sessionManager, requestContext, options);\n return result.accessToken;\n }\n\n async function getOrRefreshAccessTokenResult(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: RequestOriginOptions = {},\n ): Promise<TokenRefreshResult> {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_ACCESS_TOKEN_PERSONALIZATION_REASON);\n const sessionData = await readSessionData(sessionManager);\n const accessToken = getUsableAccessToken(sessionData);\n if (accessToken) return { status: \"authenticated\", accessToken };\n\n const refreshToken = getRefreshToken(sessionData);\n if (!refreshToken) return { status: \"unauthenticated\", accessToken: undefined };\n\n const origin = await getResolvedOrigin(sessionManager, options.origin);\n const idToken = sessionData.tokens?.idToken;\n const refreshResult = await getRefreshResult({\n refreshFlights,\n refreshToken,\n idToken,\n origin,\n endpoints,\n customerAccountApiClientId,\n fetch,\n timeoutInMs: defaultTimeoutInMs,\n });\n\n if (refreshResult.type === \"success\") {\n const refreshedAccessToken = refreshResult.tokens.accessToken;\n if (!refreshedAccessToken) {\n return { status: \"transient\", accessToken: undefined };\n }\n await writeTokens(sessionManager, refreshResult.tokens);\n return {\n status: \"authenticated\",\n accessToken: refreshedAccessToken,\n };\n }\n\n if (refreshResult.type === \"invalid\") {\n await clearTokens(sessionManager);\n return { status: \"unauthenticated\", accessToken: undefined };\n }\n\n return { status: \"transient\", accessToken: undefined };\n }\n\n async function prepareLoginUrl(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: PrepareLoginUrlOptions,\n ) {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_MUTATION_PERSONALIZATION_REASON);\n const origin = await getResolvedOrigin(sessionManager, options.origin);\n const state = generateRandomBase64Url();\n const nonce = generateRandomBase64Url();\n const codeVerifier = generateRandomBase64Url();\n const codeChallenge = await createCodeChallenge(codeVerifier);\n const returnTo = sanitizeReturnTo(options.returnTo, origin);\n const createdAt = Date.now();\n\n const sessionData = await readSessionData(sessionManager);\n await writeSessionData(sessionManager, {\n ...sessionData,\n pendingLogin: { state, nonce, codeVerifier, returnTo, origin, createdAt },\n });\n\n const loginUrl = new URL(endpoints.authorizeUrl);\n loginUrl.searchParams.set(\"client_id\", customerAccountApiClientId);\n loginUrl.searchParams.set(\"scope\", CUSTOMER_ACCOUNT_SCOPE);\n loginUrl.searchParams.set(\"response_type\", \"code\");\n loginUrl.searchParams.set(\"redirect_uri\", `${origin}${CUSTOMER_ACCOUNT_AUTHORIZE_PATH}`);\n loginUrl.searchParams.set(\"state\", state);\n loginUrl.searchParams.set(\"nonce\", nonce);\n loginUrl.searchParams.set(\"code_challenge\", codeChallenge);\n loginUrl.searchParams.set(\"code_challenge_method\", CODE_CHALLENGE_METHOD);\n setOptionalLoginParams(loginUrl, options);\n\n return loginUrl.toString();\n }\n\n async function handleOAuthCallback(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n ) {\n return (await handleOAuthCallbackResult(sessionManager, requestContext, request)).location;\n }\n\n async function handleOAuthCallbackResult(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n ): Promise<OAuthCallbackResult> {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_MUTATION_PERSONALIZATION_REASON);\n try {\n return await completeOAuthCallback({\n sessionManager,\n request,\n endpoints,\n customerAccountApiClientId,\n fetch,\n timeoutInMs: defaultTimeoutInMs,\n });\n } catch (error) {\n await clearPendingLogin(sessionManager);\n throw error;\n }\n }\n\n async function logout(\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: LogoutOptions = {},\n ) {\n requestContext.markResponseAsPersonalized(CUSTOMER_SESSION_MUTATION_PERSONALIZATION_REASON);\n const origin = await getResolvedOrigin(sessionManager, options.origin);\n const sessionData = await readSessionData(sessionManager);\n const idToken = sessionData.tokens?.idToken;\n const postLogoutRedirectUri = absoluteSameOriginUrl(\n options.postLogoutRedirectUri ?? origin,\n origin,\n );\n\n await sessionManager.removeSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY);\n\n if (!idToken) return postLogoutRedirectUri;\n\n const logoutUrl = new URL(endpoints.logoutUrl);\n logoutUrl.searchParams.set(\"id_token_hint\", idToken);\n logoutUrl.searchParams.set(\"post_logout_redirect_uri\", postLogoutRedirectUri);\n return logoutUrl.toString();\n }\n\n const customerSession: CustomerSessionWithInternals = {\n [CUSTOMER_SESSION_INTERNAL_BRAND]: true,\n isLoggedIn: async (sessionManager, requestContext) => {\n requestContext.markResponseAsPersonalized(\n CUSTOMER_SESSION_ACCESS_TOKEN_PERSONALIZATION_REASON,\n );\n return hasCustomerSession(await readSessionData(sessionManager));\n },\n getAccessToken,\n getOrRefreshAccessToken,\n prepareLoginUrl,\n handleOAuthCallback,\n logout,\n };\n customerSessionInternals.set(customerSession, {\n getOrRefreshAccessToken: getOrRefreshAccessTokenResult,\n handleOAuthCallback: handleOAuthCallbackResult,\n });\n return customerSession;\n}\n\nexport async function getCustomerSessionRefreshResult(\n customerSession: CustomerSession,\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: RequestOriginOptions = {},\n): Promise<TokenRefreshResult | undefined> {\n return customerSessionInternals\n .get(customerSession)\n ?.getOrRefreshAccessToken(sessionManager, requestContext, options);\n}\n\n/**\n * Creates the customer account routes under `/account` for sign-in, the OAuth callback, token refresh, and logout. Add the returned object to the `handlers` option of `handleShopifyRoutes`.\n *\n * The login route redirects to Shopify's login page. The authorize and refresh routes redirect back to a same-origin path in your app. The logout route redirects to Shopify's logout endpoint when the session has an ID token, and to the post-logout URL otherwise. Each redirect commits the session and sends `cache-control: no-store`.\n *\n * Link to these paths with full-page navigation, such as a plain `<a>` link or a `<form>`. Client-side navigation components can't follow these redirects.\n *\n * @returns The login, authorize, refresh, and logout routes.\n * @publicDocs\n */\nexport function createCustomerAccountServerHandlers<\n const TOptions extends CreateCustomerAccountServerHandlersOptions,\n>(options: TOptions): CustomerAccountServerHandlersForOptions<TOptions>;\n/**\n * @param options The customer session, the redirect paths, and optional cart handlers for the account routes.\n * @returns The login, authorize, refresh, and logout routes.\n */\nexport function createCustomerAccountServerHandlers(\n options: CreateCustomerAccountServerHandlersOptions,\n): CustomerAccountServerHandlers | CustomerAccountServerHandlersWithCartSync {\n const {\n customerSession,\n defaultPostLoginRedirectPathname = DEFAULT_POST_LOGIN_REDIRECT_PATHNAME,\n loginFailedRedirectPath = FAILED_LOGIN_PATH,\n postLogoutRedirectUri = DEFAULT_POST_LOGOUT_REDIRECT_URI,\n } = options;\n const { origin: originOption } = options;\n const cartSync = resolveCartBuyerIdentitySync(options);\n\n return {\n authorize: createCallableRouteHandler(\n CUSTOMER_ACCOUNT_AUTHORIZE_PATH,\n \"GET\",\n async (context: CustomerAccountRuntimeRouteHandlerContext) => {\n return handleAuthorizeRoute(\n customerSession,\n context,\n loginFailedRedirectPath,\n originOption,\n cartSync,\n );\n },\n ),\n login: createCallableRouteHandler(\n CUSTOMER_ACCOUNT_LOGIN_PATH,\n \"GET\",\n async (context: CustomerAccountRuntimeRouteHandlerContext) => {\n const { request, sessionManager, requestContext } = context;\n return handleLoginRoute(\n customerSession,\n sessionManager,\n requestContext,\n request,\n defaultPostLoginRedirectPathname,\n originOption,\n );\n },\n ),\n logout: createCallableRouteHandler(\n CUSTOMER_ACCOUNT_LOGOUT_PATH,\n \"POST\",\n async (context: CustomerAccountRuntimeRouteHandlerContext) => {\n return handleLogoutRoute(\n customerSession,\n context,\n postLogoutRedirectUri,\n originOption,\n cartSync,\n );\n },\n ),\n refresh: createCallableRouteHandler(\n CUSTOMER_ACCOUNT_REFRESH_PATH,\n \"GET\",\n async (context: CustomerAccountRuntimeRouteHandlerContext) => {\n return handleRefreshRoute(customerSession, context, originOption, cartSync);\n },\n ),\n };\n}\n\nfunction resolveCartBuyerIdentitySync(\n options: CreateCustomerAccountServerHandlersOptions,\n): CartBuyerIdentitySync | undefined {\n if (!options.cartServerHandlers) return undefined;\n\n const cartSync = getCartBuyerIdentitySync(options.cartServerHandlers);\n if (!cartSync) {\n throw new Error(\n \"cartServerHandlers must be created by createCartServerHandlers with the customerSession option.\",\n );\n }\n if (!customerSessionInternals.has(options.customerSession)) {\n throw new Error(\n \"Cart buyer identity sync requires the customerSession returned by createCustomerSession.\",\n );\n }\n return cartSync;\n}\n\nasync function handleLoginRoute(\n customerSession: CustomerSession,\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n defaultPostLoginRedirectPathname: string,\n originOption: string | ((request: Request) => string) | undefined,\n): Promise<CustomerAccountRouteResult> {\n const origin = await resolveRouteOrigin(sessionManager, request, originOption);\n const requestUrl = new URL(request.url);\n const defaultReturnTo = sanitizeReturnTo(\n defaultPostLoginRedirectPathname,\n origin,\n DEFAULT_POST_LOGIN_REDIRECT_PATHNAME,\n );\n const requestedReturnTo =\n requestUrl.searchParams.get(\"return_to\") ?? requestUrl.searchParams.get(\"returnTo\");\n const returnTo = sanitizeReturnTo(requestedReturnTo, origin, defaultReturnTo);\n const loginUrl = await customerSession.prepareLoginUrl(sessionManager, requestContext, {\n origin,\n returnTo,\n countryCode: requestContext.i18n.country,\n locale: getOptionalSearchParam(requestUrl, \"locale\"),\n acrValues: getOptionalSearchParam(requestUrl, \"acr_values\"),\n loginHint: getOptionalSearchParam(requestUrl, \"login_hint\"),\n loginHintMode: getOptionalSearchParam(requestUrl, \"login_hint_mode\"),\n });\n return redirectResult(loginUrl, await commitSession(sessionManager));\n}\n\nasync function handleLogoutRoute(\n customerSession: CustomerSession,\n context: CustomerAccountRuntimeRouteHandlerContext,\n postLogoutRedirectUri: string,\n originOption: string | ((request: Request) => string) | undefined,\n cartSync: CartBuyerIdentitySync | undefined,\n): Promise<CustomerAccountRouteResult> {\n const { request, sessionManager, requestContext } = context;\n const origin = await resolveRouteOrigin(sessionManager, request, originOption);\n if (!isSameOriginPost(request, origin)) return forbiddenResult();\n\n const requestUrl = new URL(request.url);\n const requestedReturnTo =\n requestUrl.searchParams.get(\"return_to\") ?? requestUrl.searchParams.get(\"returnTo\");\n const logoutUrl = await customerSession.logout(sessionManager, requestContext, {\n origin,\n postLogoutRedirectUri: sanitizeReturnTo(requestedReturnTo, origin, postLogoutRedirectUri),\n });\n\n // The redirect must complete even when detach fails: skipping it would leave\n // the Shopify IdP session alive after the app session is already destroyed.\n // Expiring the cart cookie is the fail-safe when the cart keeps the identity.\n let detachFailed = false;\n if (cartSync) {\n detachFailed = !(await syncCartBuyerIdentity(cartSync, context, null, \"logout\"));\n }\n const headers = new Headers(await commitSession(sessionManager));\n if (cartSync && detachFailed) headers.append(\"set-cookie\", cartSync.expiredCartCookie);\n return redirectResult(logoutUrl, headers);\n}\n\nasync function handleAuthorizeRoute(\n customerSession: CustomerSession,\n context: CustomerAccountRuntimeRouteHandlerContext,\n loginFailedRedirectPath: string,\n originOption: string | ((request: Request) => string) | undefined,\n cartSync: CartBuyerIdentitySync | undefined,\n): Promise<CustomerAccountRouteResult> {\n const { request, sessionManager, requestContext } = context;\n try {\n if (!cartSync) {\n const location = await customerSession.handleOAuthCallback(\n sessionManager,\n requestContext,\n request,\n );\n return redirectResult(location, await commitSession(sessionManager));\n }\n\n const { location, accessToken } = await handleOAuthCallbackWithAccessToken(\n customerSession,\n sessionManager,\n requestContext,\n request,\n );\n // Best-effort: the customer is authenticated at this point, so a failed\n // cart sync must not turn a successful login into an error response. The\n // next refresh retries the attach.\n await syncCartBuyerIdentity(cartSync, context, accessToken, \"authorize\");\n return redirectResult(location, await commitSession(sessionManager));\n } catch (error) {\n if (!(error instanceof CustomerAccountOAuthError)) throw error;\n const origin = await resolveRouteOrigin(sessionManager, request, originOption);\n return redirectResult(\n sanitizeReturnTo(loginFailedRedirectPath, origin),\n await commitSession(sessionManager),\n );\n }\n}\n\nasync function handleRefreshRoute(\n customerSession: CustomerSession,\n context: CustomerAccountRuntimeRouteHandlerContext,\n originOption: string | ((request: Request) => string) | undefined,\n cartSync: CartBuyerIdentitySync | undefined,\n): Promise<CustomerAccountRouteResult> {\n const { request, sessionManager, requestContext } = context;\n const origin = await resolveRouteOrigin(sessionManager, request, originOption);\n if (!cartSync) {\n await customerSession.getOrRefreshAccessToken(sessionManager, requestContext, { origin });\n return refreshRedirectResult(request, origin, await commitSession(sessionManager));\n }\n\n const refreshResult = await getOrRefreshAccessTokenWithStatus(\n customerSession,\n sessionManager,\n requestContext,\n { origin },\n );\n // Transient refresh failures keep the refreshable session, so the cart's\n // identity is left untouched; a definitive outcome attaches or detaches it.\n let detachFailed = false;\n if (refreshResult.status !== \"transient\") {\n const customerAccessToken = refreshResult.accessToken ?? null;\n const syncSucceeded = await syncCartBuyerIdentity(\n cartSync,\n context,\n customerAccessToken,\n \"refresh\",\n );\n detachFailed = customerAccessToken === null && !syncSucceeded;\n }\n\n const headers = new Headers(await commitSession(sessionManager));\n if (detachFailed) headers.append(\"set-cookie\", cartSync.expiredCartCookie);\n return refreshRedirectResult(request, origin, headers);\n}\n\nfunction refreshRedirectResult(\n request: Request,\n origin: string,\n headers?: HeadersInit,\n): ShopifyRouteRedirectResult {\n const requestUrl = new URL(request.url);\n const returnTo =\n requestUrl.searchParams.get(\"return_to\") ?? requestUrl.searchParams.get(\"returnTo\");\n return redirectResult(sanitizeReturnTo(returnTo, origin), headers);\n}\n\nasync function handleOAuthCallbackWithAccessToken(\n customerSession: CustomerSession,\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n request: Request,\n): Promise<OAuthCallbackResult> {\n const internal = customerSessionInternals.get(customerSession);\n if (!internal) {\n throw new Error(\"Customer session was not created by createCustomerSession\");\n }\n return internal.handleOAuthCallback(sessionManager, requestContext, request);\n}\n\nasync function getOrRefreshAccessTokenWithStatus(\n customerSession: CustomerSession,\n sessionManager: WritableCustomerSessionManager,\n requestContext: ShopifyRequestContext,\n options: RequestOriginOptions,\n): Promise<TokenRefreshResult> {\n const internal = customerSessionInternals.get(customerSession);\n if (!internal) {\n throw new Error(\"Customer session was not created by createCustomerSession\");\n }\n return internal.getOrRefreshAccessToken(sessionManager, requestContext, options);\n}\n\nasync function syncCartBuyerIdentity(\n cartSync: CartBuyerIdentitySync,\n context: CustomerAccountRuntimeRouteHandlerContext,\n customerAccessToken: string | null,\n route: \"authorize\" | \"refresh\" | \"logout\",\n): Promise<boolean> {\n try {\n if (!context.storefrontClient) {\n throw new Error(\n \"Customer Account handlers configured with cartServerHandlers require storefrontClient.\",\n );\n }\n await cartSync.updateBuyerIdentity(\n { request: context.request, storefrontClient: context.storefrontClient },\n customerAccessToken,\n );\n return true;\n } catch (error) {\n log.error(\"cart buyer identity sync failed\", { route, error });\n return false;\n }\n}\n\nasync function resolveRouteOrigin(\n sessionManager: WritableCustomerSessionManager,\n request: Request,\n originOption: string | ((request: Request) => string) | undefined,\n) {\n if (typeof originOption === \"function\") return normalizeOrigin(originOption(request));\n return getResolvedOrigin(sessionManager, originOption);\n}\n\nasync function getResolvedOrigin(\n sessionManager: WritableCustomerSessionManager,\n originOverride: string | undefined,\n) {\n return normalizeOrigin(originOverride ?? (await sessionManager.getSessionOrigin()));\n}\n\nfunction getOptionalSearchParam(url: URL, name: string): string | undefined {\n return url.searchParams.get(name) ?? undefined;\n}\n\nfunction redirectResult(location: string, headers?: HeadersInit): ShopifyRouteRedirectResult {\n const redirectHeaders = new Headers(headers);\n redirectHeaders.set(\"cache-control\", NO_STORE_CACHE_CONTROL);\n return { type: \"redirect\", location, headers: redirectHeaders };\n}\n\nfunction forbiddenResult(): ShopifyRouteErrorResult {\n return {\n type: \"error\",\n status: FORBIDDEN_STATUS,\n error: { code: FORBIDDEN_ERROR_CODE, message: FORBIDDEN_ERROR_MESSAGE },\n headers: { \"cache-control\": NO_STORE_CACHE_CONTROL },\n };\n}\n\nasync function commitSession(\n sessionManager: WritableCustomerSessionManager,\n): Promise<HeadersInit | undefined> {\n return (await sessionManager.commit?.()) ?? undefined;\n}\n\nasync function completeOAuthCallback({\n sessionManager,\n request,\n endpoints,\n customerAccountApiClientId,\n fetch,\n timeoutInMs,\n}: {\n sessionManager: WritableCustomerSessionManager;\n request: Request;\n endpoints: CustomerAccountEndpoints;\n customerAccountApiClientId: string;\n fetch: typeof globalThis.fetch;\n timeoutInMs: number;\n}): Promise<OAuthCallbackResult> {\n const requestUrl = new URL(request.url);\n const code = requestUrl.searchParams.get(\"code\");\n const state = requestUrl.searchParams.get(\"state\");\n const sessionData = await readSessionData(sessionManager);\n const pendingLogin = getPendingLogin(sessionData);\n\n assertOAuthCallbackParams(code, state, pendingLogin);\n\n const origin = pendingLogin.origin ?? getOriginFromRequest(request);\n const tokenResponse = await exchangeAuthorizationCode({\n endpoints,\n customerAccountApiClientId,\n origin,\n code,\n codeVerifier: pendingLogin.codeVerifier,\n fetch,\n timeoutInMs,\n });\n validateIdTokenClaims(tokenResponse.id_token, {\n audience: customerAccountApiClientId,\n issuer: endpoints.issuer,\n nonce: pendingLogin.nonce,\n });\n\n await writeSessionData(sessionManager, {\n tokens: createTokensFromResponse(tokenResponse),\n });\n\n return {\n location: pendingLogin.returnTo ?? DEFAULT_LOGIN_RETURN_TO_PATH,\n accessToken: tokenResponse.access_token,\n };\n}\n\nfunction isSameOriginPost(request: Request, trustedOrigin: string): boolean {\n const origin = request.headers.get(\"origin\");\n if (origin) {\n try {\n return normalizeOrigin(origin) === trustedOrigin;\n } catch {\n return false;\n }\n }\n\n const referer = request.headers.get(\"referer\");\n if (!referer) return false;\n\n try {\n return normalizeOrigin(new URL(referer).origin) === trustedOrigin;\n } catch {\n return false;\n }\n}\n\nfunction assertOAuthCallbackParams(\n code: string | null,\n state: string | null,\n pendingLogin: Required<PendingLogin>,\n): asserts code is string {\n if (!code || !state) {\n throw new CustomerAccountOAuthError(\n \"missing_callback_params\",\n \"OAuth callback is missing code or state\",\n );\n }\n\n if (pendingLogin.state !== state) {\n throw new CustomerAccountOAuthError(\n \"state_mismatch\",\n \"OAuth callback state does not match session state\",\n );\n }\n}\n\nasync function exchangeAuthorizationCode({\n endpoints,\n customerAccountApiClientId,\n origin,\n code,\n codeVerifier,\n fetch,\n timeoutInMs,\n}: {\n endpoints: CustomerAccountEndpoints;\n customerAccountApiClientId: string;\n origin: string;\n code: string;\n codeVerifier: string;\n fetch: typeof globalThis.fetch;\n timeoutInMs: number;\n}): Promise<Required<ParsedTokenEndpointResponse>> {\n const body = new URLSearchParams({\n grant_type: AUTHORIZATION_CODE_GRANT_TYPE,\n client_id: customerAccountApiClientId,\n redirect_uri: `${origin}${CUSTOMER_ACCOUNT_AUTHORIZE_PATH}`,\n code,\n code_verifier: codeVerifier,\n });\n return withTokenRequestTimeout(timeoutInMs, async (signal) => {\n const response = await postTokenRequest({\n url: endpoints.tokenUrl,\n origin,\n body,\n fetch,\n signal,\n });\n\n if (!response.ok) {\n cancelResponseBody(response);\n const errorCode =\n response.status === 400 || response.status === 401\n ? \"token_exchange_rejected\"\n : \"token_exchange_failed\";\n throw new CustomerAccountOAuthError(\n errorCode,\n `Customer Account OAuth token exchange failed with ${response.status}`,\n );\n }\n\n return parseRequiredTokenResponse(await readTokenResponseJson(response, signal));\n });\n}\n\nasync function getRefreshResult({\n refreshFlights,\n refreshToken,\n idToken,\n origin,\n endpoints,\n customerAccountApiClientId,\n fetch,\n timeoutInMs,\n}: {\n refreshFlights: Map<string, Promise<RefreshResult>>;\n refreshToken: string;\n idToken: string | undefined;\n origin: string;\n endpoints: CustomerAccountEndpoints;\n customerAccountApiClientId: string;\n fetch: typeof globalThis.fetch;\n timeoutInMs: number;\n}) {\n const flightKey = `${origin}\\n${refreshToken}`;\n const existingFlight = refreshFlights.get(flightKey);\n if (existingFlight) return existingFlight;\n\n const flight = refreshAccessToken({\n endpoints,\n customerAccountApiClientId,\n refreshToken,\n idToken,\n origin,\n fetch,\n timeoutInMs,\n });\n refreshFlights.set(flightKey, flight);\n\n try {\n return await flight;\n } finally {\n refreshFlights.delete(flightKey);\n }\n}\n\nasync function refreshAccessToken({\n endpoints,\n customerAccountApiClientId,\n refreshToken,\n idToken,\n origin,\n fetch,\n timeoutInMs,\n}: {\n endpoints: CustomerAccountEndpoints;\n customerAccountApiClientId: string;\n refreshToken: string;\n idToken: string | undefined;\n origin: string;\n fetch: typeof globalThis.fetch;\n timeoutInMs: number;\n}): Promise<RefreshResult> {\n const body = new URLSearchParams({\n grant_type: REFRESH_TOKEN_GRANT_TYPE,\n client_id: customerAccountApiClientId,\n refresh_token: refreshToken,\n });\n\n try {\n return await withTokenRequestTimeout(timeoutInMs, async (signal) => {\n const response = await postTokenRequest({\n url: endpoints.tokenUrl,\n origin,\n body,\n fetch,\n signal,\n });\n return readRefreshResult(response, refreshToken, idToken, signal);\n });\n } catch {\n return { type: \"transient\" };\n }\n}\n\nasync function readRefreshResult(\n response: Response,\n currentRefreshToken: string,\n currentIdToken: string | undefined,\n signal: AbortSignal,\n): Promise<RefreshResult> {\n if (response.status === 400 || response.status === 401) {\n cancelResponseBody(response);\n return { type: \"invalid\" };\n }\n\n if (!response.ok) {\n cancelResponseBody(response);\n return { type: \"transient\" };\n }\n\n try {\n const tokenResponse = await readTokenResponseJson(response, signal);\n const parsedTokenResponse = parseRefreshTokenResponse(tokenResponse);\n return {\n type: \"success\",\n tokens: createTokensFromResponse({\n ...parsedTokenResponse,\n refresh_token: parsedTokenResponse.refresh_token ?? currentRefreshToken,\n id_token: parsedTokenResponse.id_token ?? currentIdToken,\n }),\n };\n } catch {\n return { type: \"transient\" };\n }\n}\n\nasync function postTokenRequest({\n url,\n origin,\n body,\n fetch,\n signal,\n}: TokenRequestParams): Promise<Response> {\n return await withAbort(\n fetch(url, {\n method: \"POST\",\n headers: new Headers({\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n \"User-Agent\": USER_AGENT,\n Origin: origin,\n }),\n body,\n cache: \"no-store\",\n redirect: \"manual\",\n signal,\n }),\n signal,\n );\n}\n\nasync function withTokenRequestTimeout<T>(\n timeoutInMs: number,\n operation: (signal: AbortSignal) => Promise<T>,\n): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort(\n new CustomerAccountApiError(\n `Customer Account OAuth request timed out after ${timeoutInMs}ms`,\n ),\n );\n }, timeoutInMs);\n\n try {\n return await operation(controller.signal);\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction withAbort<T>(promise: T | Promise<T>, signal: AbortSignal): Promise<T> {\n if (signal.aborted) return Promise.reject(getAbortReason(signal));\n\n return new Promise<T>((resolve, reject) => {\n const abort = () => {\n signal.removeEventListener(\"abort\", abort);\n reject(getAbortReason(signal));\n };\n signal.addEventListener(\"abort\", abort, { once: true });\n Promise.resolve(promise).then(\n (value) => {\n signal.removeEventListener(\"abort\", abort);\n resolve(value);\n },\n (error) => {\n signal.removeEventListener(\"abort\", abort);\n reject(error);\n },\n );\n });\n}\n\nfunction getAbortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new DOMException(\"signal is aborted without reason\", \"AbortError\");\n}\n\nasync function readTokenResponseJson(\n response: Response,\n signal: AbortSignal,\n): Promise<TokenEndpointResponse> {\n const json = await withAbort(response.json(), signal);\n if (!isObjectRecord(json)) {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response must be an object\",\n );\n }\n return {\n access_token: json.access_token,\n refresh_token: json.refresh_token,\n id_token: json.id_token,\n expires_in: json.expires_in,\n };\n}\n\nfunction parseRequiredTokenResponse(\n response: TokenEndpointResponse,\n): Required<ParsedTokenEndpointResponse> {\n const parsed = parseRefreshTokenResponse(response);\n if (typeof response.refresh_token !== \"string\" || response.refresh_token === \"\") {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response is missing refresh_token\",\n );\n }\n\n if (typeof response.id_token !== \"string\" || response.id_token === \"\") {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response is missing id_token\",\n );\n }\n return { ...parsed, refresh_token: response.refresh_token, id_token: response.id_token };\n}\n\nfunction parseRefreshTokenResponse(response: TokenEndpointResponse): ParsedTokenEndpointResponse {\n if (typeof response.access_token !== \"string\" || response.access_token === \"\") {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response is missing access_token\",\n );\n }\n\n if (\n typeof response.expires_in !== \"number\" ||\n !Number.isFinite(response.expires_in) ||\n response.expires_in <= 0\n ) {\n throw new CustomerAccountOAuthError(\n \"invalid_token_response\",\n \"Customer Account OAuth response is missing expires_in\",\n );\n }\n\n return {\n access_token: response.access_token,\n refresh_token:\n typeof response.refresh_token === \"string\" && response.refresh_token !== \"\"\n ? response.refresh_token\n : undefined,\n id_token:\n typeof response.id_token === \"string\" && response.id_token !== \"\"\n ? response.id_token\n : undefined,\n expires_in: response.expires_in,\n };\n}\n\nfunction createTokensFromResponse(response: ParsedTokenEndpointResponse): CustomerAccountTokens {\n return {\n accessToken: response.access_token,\n refreshToken: response.refresh_token,\n idToken: response.id_token,\n expiresAt:\n Date.now() + (response.expires_in - EXPIRY_BUFFER_IN_SECONDS) * MILLISECONDS_PER_SECOND,\n };\n}\n\nfunction validateIdTokenClaims(\n idToken: string,\n expected: { audience: string; issuer: string; nonce: string },\n): void {\n const claims = parseJwtPayload(idToken);\n if (claims.nonce !== expected.nonce) {\n throw new CustomerAccountOAuthError(\n \"nonce_mismatch\",\n \"OAuth id_token nonce does not match session nonce\",\n );\n }\n\n if (claims.iss !== expected.issuer) {\n throw new CustomerAccountOAuthError(\n \"issuer_mismatch\",\n \"OAuth id_token issuer does not match Customer Account issuer\",\n );\n }\n\n if (!hasAudienceClaim(claims.aud, expected.audience)) {\n throw new CustomerAccountOAuthError(\n \"audience_mismatch\",\n \"OAuth id_token audience does not match Customer Account client ID\",\n );\n }\n\n if (\n typeof claims.exp !== \"number\" ||\n claims.exp <= Math.floor(Date.now() / MILLISECONDS_PER_SECOND)\n ) {\n throw new CustomerAccountOAuthError(\"expired_id_token\", \"OAuth id_token is expired\");\n }\n}\n\nfunction parseJwtPayload(idToken: string): Record<string, unknown> {\n try {\n const [, payload] = idToken.split(\".\");\n if (!payload) throw new Error(\"Missing JWT payload\");\n\n const decodedPayload = JSON.parse(base64UrlDecode(payload));\n if (!isObjectRecord(decodedPayload)) throw new Error(\"JWT payload must be an object\");\n\n return decodedPayload;\n } catch (cause) {\n throw new CustomerAccountOAuthError(\n \"invalid_id_token\",\n \"Customer Account OAuth id_token is invalid\",\n { cause },\n );\n }\n}\n\nfunction hasAudienceClaim(audience: unknown, expectedAudience: string): boolean {\n if (audience === expectedAudience) return true;\n if (!Array.isArray(audience)) return false;\n return audience.includes(expectedAudience);\n}\n\nasync function readSessionData(\n sessionManager: ReadonlyCustomerSessionManager,\n): Promise<CustomerAccountSessionData> {\n const value = await sessionManager.getSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY);\n return isCustomerAccountSessionData(value) ? value : {};\n}\n\nasync function writeSessionData(\n sessionManager: WritableCustomerSessionManager,\n sessionData: CustomerAccountSessionData,\n): Promise<void> {\n await sessionManager.setSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY, sessionData);\n}\n\nasync function writeTokens(\n sessionManager: WritableCustomerSessionManager,\n tokens: CustomerAccountTokens,\n): Promise<void> {\n const sessionData = await readSessionData(sessionManager);\n await writeSessionData(sessionManager, { ...sessionData, tokens });\n}\n\nasync function clearTokens(sessionManager: WritableCustomerSessionManager): Promise<void> {\n const sessionData = await readSessionData(sessionManager);\n const { pendingLogin } = sessionData;\n if (pendingLogin) {\n await writeSessionData(sessionManager, { pendingLogin });\n return;\n }\n\n await sessionManager.removeSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY);\n}\n\nasync function clearPendingLogin(sessionManager: WritableCustomerSessionManager): Promise<void> {\n const sessionData = await readSessionData(sessionManager);\n const { tokens } = sessionData;\n if (tokens) {\n await writeSessionData(sessionManager, { tokens });\n return;\n }\n\n await sessionManager.removeSessionItem(CUSTOMER_ACCOUNT_SESSION_KEY);\n}\n\nfunction getUsableAccessToken(sessionData: CustomerAccountSessionData): string | undefined {\n const tokens = sessionData.tokens;\n if (!isTokenUsable(tokens)) return undefined;\n return tokens.accessToken;\n}\n\nfunction hasCustomerSession(sessionData: CustomerAccountSessionData): boolean {\n return (\n getUsableAccessToken(sessionData) !== undefined || getRefreshToken(sessionData) !== undefined\n );\n}\n\nfunction getRefreshToken(sessionData: CustomerAccountSessionData): string | undefined {\n const refreshToken = sessionData.tokens?.refreshToken;\n return typeof refreshToken === \"string\" && refreshToken !== \"\" ? refreshToken : undefined;\n}\n\nfunction getPendingLogin(sessionData: CustomerAccountSessionData): Required<PendingLogin> {\n const pendingLogin = sessionData.pendingLogin;\n if (isPendingLoginReady(pendingLogin)) return pendingLogin;\n throw new CustomerAccountOAuthError(\n \"missing_pending_login\",\n \"OAuth callback has no pending login state\",\n );\n}\n\nfunction isTokenUsable(\n tokens: CustomerAccountSessionData[\"tokens\"],\n): tokens is Required<CustomerAccountTokens> {\n if (!tokens || typeof tokens.accessToken !== \"string\" || tokens.accessToken === \"\") return false;\n if (typeof tokens.expiresAt !== \"number\") return false;\n return tokens.expiresAt > Date.now();\n}\n\nfunction isPendingLoginReady(\n pendingLogin: PendingLogin | undefined,\n): pendingLogin is Required<PendingLogin> {\n if (!pendingLogin) return false;\n if (!isNonEmptyString(pendingLogin.state)) return false;\n if (!isNonEmptyString(pendingLogin.nonce)) return false;\n if (!isNonEmptyString(pendingLogin.codeVerifier)) return false;\n if (!isNonEmptyString(pendingLogin.returnTo)) return false;\n if (!isNonEmptyString(pendingLogin.origin)) return false;\n if (typeof pendingLogin.createdAt !== \"number\") return false;\n return Date.now() - pendingLogin.createdAt <= PENDING_LOGIN_TTL_IN_MS;\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value !== \"\";\n}\n\nfunction isCustomerAccountSessionData(value: unknown): value is CustomerAccountSessionData {\n if (!isObjectRecord(value) || Array.isArray(value)) return false;\n const tokens = value.tokens;\n const pendingLogin = value.pendingLogin;\n if (tokens !== undefined && (!isObjectRecord(tokens) || Array.isArray(tokens))) return false;\n return (\n pendingLogin === undefined || (isObjectRecord(pendingLogin) && !Array.isArray(pendingLogin))\n );\n}\n\nfunction setOptionalLoginParams(loginUrl: URL, options: PrepareLoginUrlOptions): void {\n if (options.locale) loginUrl.searchParams.set(\"locale\", options.locale);\n if (options.countryCode) loginUrl.searchParams.set(\"region_country\", options.countryCode);\n if (options.acrValues) loginUrl.searchParams.set(\"acr_values\", options.acrValues);\n if (!options.loginHint) return;\n\n loginUrl.searchParams.set(\"login_hint\", options.loginHint);\n if (options.loginHintMode) loginUrl.searchParams.set(\"login_hint_mode\", options.loginHintMode);\n}\n\nfunction sanitizeReturnTo(\n returnTo: string | null | undefined,\n origin: string,\n fallbackReturnTo = DEFAULT_LOGIN_RETURN_TO_PATH,\n): string {\n const path = getSameOriginPath(returnTo, origin);\n if (!path || new TextEncoder().encode(path).byteLength > MAX_RETURN_TO_LENGTH_IN_BYTES) {\n return fallbackReturnTo;\n }\n return path;\n}\n\nfunction absoluteSameOriginUrl(url: string, origin: string): string {\n const parsedUrl = new URL(url, origin);\n if (parsedUrl.origin !== origin) return origin;\n return parsedUrl.toString();\n}\n\nasync function createCodeChallenge(codeVerifier: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(codeVerifier));\n return base64UrlEncode(String.fromCharCode(...new Uint8Array(digest)));\n}\n\nfunction generateRandomBase64Url(): string {\n const bytes = new Uint8Array(RANDOM_BYTES_LENGTH);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(String.fromCharCode(...bytes));\n}\n\nfunction base64UrlEncode(value: string): string {\n return btoa(value).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n}\n\nfunction base64UrlDecode(value: string): string {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const paddingLength = (4 - (base64.length % 4)) % 4;\n return atob(base64.padEnd(base64.length + paddingLength, \"=\"));\n}\n\ntype CustomerAccountEndpoints = {\n authorizeUrl: string;\n tokenUrl: string;\n logoutUrl: string;\n issuer: string;\n};\n\nfunction createCustomerAccountEndpoints(\n shopId: string,\n customerAccountApiUrl: string | undefined,\n): CustomerAccountEndpoints {\n const authBaseUrl = normalizeAuthBaseUrl(\n customerAccountApiUrl ?? `https://shopify.com/authentication/${shopId}`,\n );\n return {\n authorizeUrl: `${authBaseUrl}/oauth/authorize`,\n tokenUrl: `${authBaseUrl}/oauth/token`,\n logoutUrl: `${authBaseUrl}/logout`,\n issuer: authBaseUrl,\n };\n}\n\nfunction normalizeAuthBaseUrl(customerAccountApiUrl: string): string {\n const url = new URL(customerAccountApiUrl);\n if (url.protocol !== \"https:\") {\n throw new Error(\"customerAccountApiUrl must use HTTPS\");\n }\n return url.toString().replace(/\\/$/, \"\");\n}\n\nfunction getOriginFromRequest(request: Request): string {\n return normalizeOrigin(new URL(request.url).origin);\n}\n\nfunction normalizeOrigin(origin: string): string {\n const url = new URL(origin);\n if (url.protocol === \"https:\") return url.origin;\n throw new Error(\n \"Customer Account OAuth origin must use HTTPS. Use a public HTTPS tunnel for local Customer Account login.\",\n );\n}\n\nfunction cancelResponseBody(response: Response): void {\n try {\n void response.body?.cancel().catch(() => undefined);\n } catch {}\n}\n\nfunction validateShopId(shopId: string): void {\n if (!SHOP_ID_RE.test(shopId)) {\n throw new Error(\"shopId must be a numeric Shopify shop ID string\");\n }\n}\n\nfunction validateCustomerAccountApiClientId(customerAccountApiClientId: string): void {\n if (typeof customerAccountApiClientId !== \"string\" || customerAccountApiClientId.trim() === \"\") {\n throw new Error(\"customerAccountApiClientId is required\");\n }\n}\n\nfunction validateTimeout(timeoutInMs: number): void {\n if (\n !Number.isSafeInteger(timeoutInMs) ||\n timeoutInMs <= 0 ||\n timeoutInMs > MAX_SET_TIMEOUT_IN_MS\n ) {\n throw new Error(\n `defaultTimeoutInMs must be a positive safe integer no greater than ${MAX_SET_TIMEOUT_IN_MS}`,\n );\n }\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":";;;;AAoBA,MAAM,MAAM,UAAU,mBAAmB;;;;;;AAOzC,MAAa,kCAAkC,uBAAuB;;;;;;AAMtE,MAAa,8BAA8B,uBAAuB;;;;;;AAMlE,MAAa,+BAA+B,uBAAuB;;;;;;AAMnE,MAAa,gCAAgC,uBAAuB;AAiBpE,MAAM,+BAA+B;AACrC,MAAM,qBAAqB;AAC3B,MAAM,0BAA0B;AAChC,MAAM,0BACJ,+BAA+B,qBAAqB;AAyTtD,MAAM,2CAA2B,IAAI,SAAoD;AAmPzF,eAAsB,gCACpB,iBACA,gBACA,gBACA,UAAgC,EAAE,EACO;AACzC,QAAO,yBACJ,IAAI,gBAAgB,EACnB,wBAAwB,gBAAgB,gBAAgB,QAAQ"}
|
|
@@ -4,9 +4,14 @@ import { ShopifyStandardActions } from "./vendor/standard-actions.mjs";
|
|
|
4
4
|
import { StorefrontAnalytics } from "./core/analytics/types.mjs";
|
|
5
5
|
|
|
6
6
|
//#region src/globals.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* The `window.Shopify` object that Shopify's scripts and Hydrogen share in the browser.
|
|
9
|
+
*
|
|
10
|
+
* @publicDocs
|
|
11
|
+
*/
|
|
7
12
|
type ShopifyGlobal = {
|
|
8
13
|
actions: ShopifyStandardActions;
|
|
9
|
-
analytics?: StorefrontAnalytics;
|
|
14
|
+
analytics?: StorefrontAnalytics; /** The store settings that Shopify's storefront web components read. */
|
|
10
15
|
components: {
|
|
11
16
|
config: {
|
|
12
17
|
storeDomain: string;
|
|
@@ -15,11 +20,11 @@ type ShopifyGlobal = {
|
|
|
15
20
|
country?: string;
|
|
16
21
|
language?: string;
|
|
17
22
|
};
|
|
18
|
-
};
|
|
19
|
-
country: I18nConfig["country"] | string;
|
|
23
|
+
}; /** The customer's country code. Defaults to `US`. */
|
|
24
|
+
country: I18nConfig["country"] | string; /** The active currency code, in uppercase. */
|
|
20
25
|
currency?: {
|
|
21
26
|
active: string;
|
|
22
|
-
};
|
|
27
|
+
}; /** Shopify's Customer Privacy API, which reads and updates the customer's consent. */
|
|
23
28
|
customerPrivacy: {
|
|
24
29
|
config?: {
|
|
25
30
|
isHeadless?: boolean;
|
|
@@ -36,14 +41,18 @@ type ShopifyGlobal = {
|
|
|
36
41
|
setTrackingConsent: (consent: Record<string, unknown>) => Promise<unknown>;
|
|
37
42
|
shouldShowBanner: () => boolean;
|
|
38
43
|
shouldShowGDPRBanner: () => boolean;
|
|
39
|
-
};
|
|
40
|
-
locale: Lowercase<I18nConfig["language"]> | string;
|
|
41
|
-
|
|
44
|
+
}; /** The customer's language code, in lowercase. Defaults to `en`. */
|
|
45
|
+
locale: Lowercase<I18nConfig["language"]> | string;
|
|
46
|
+
/**
|
|
47
|
+
* Navigates to a storefront URL.
|
|
48
|
+
* @deprecated Use `Shopify.routes.navigate` instead.
|
|
49
|
+
*/
|
|
50
|
+
navigate?: (url: string) => void | Promise<void>; /** The storefront's route settings and navigation helper. */
|
|
42
51
|
routes: {
|
|
43
|
-
root: string; /** @
|
|
44
|
-
apiProxyPrefix?: string; /** @
|
|
45
|
-
match?: (url: string) => ShopifyStandardRouteMatch | null; /** @
|
|
46
|
-
resolve?: (url: string) => string; /**
|
|
52
|
+
/** The locale path prefix with a trailing slash, or `"/"` when the storefront has no prefix. */root: string; /** @private */
|
|
53
|
+
apiProxyPrefix?: string; /** @private */
|
|
54
|
+
match?: (url: string) => ShopifyStandardRouteMatch | null; /** @private */
|
|
55
|
+
resolve?: (url: string) => string; /** Navigates to a storefront URL after resolving it to your app's route. */
|
|
47
56
|
navigate?: (url: string) => void | Promise<void>;
|
|
48
57
|
[key: string]: unknown;
|
|
49
58
|
}; /** The shop's permanent `*.myshopify.com` domain. */
|