Skip to content

Commit ef80372

Browse files
authored
Merge branch 'main' into dependabot/github_actions/ruby/setup-ruby-1.265.0
2 parents 31026ae + 0d89f9b commit ef80372

File tree

180 files changed

+36524
-169
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

180 files changed

+36524
-169
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ jobs:
1111
strategy:
1212
matrix:
1313
version:
14-
- 3.0
15-
- 3.1
1614
- 3.2
1715
- 3.3
16+
- 3.4
1817
steps:
1918
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
2019
- name: Set up Ruby ${{ matrix.version }}

BREAKING_CHANGES_FOR_V15.md

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,99 @@
11
# Breaking change notice for version 15.0.0
22

3+
## Removal of positional `storefront_access_token` parameter from Storefront GraphQL Client
4+
5+
The `ShopifyAPI::Clients::Graphql::Storefront` class no longer accepts the public Storefront access token as a positional parameter. You must now use the named parameters `private_token:` or `public_token:` instead.
6+
7+
This parameter was deprecated in [PR #1302](https://github.com/Shopify/shopify-api-ruby/pull/1302) (v14.1.0).
8+
9+
### Previous implementation (deprecated in v14.1.0)
10+
11+
```ruby
12+
# Old way: passing token as positional parameter
13+
client = ShopifyAPI::Clients::Graphql::Storefront.new(shop_url, storefront_access_token)
14+
15+
# With API version
16+
client = ShopifyAPI::Clients::Graphql::Storefront.new(shop_url, storefront_access_token, api_version: "2024-01")
17+
```
18+
19+
### New implementation (required in v15.0.0)
20+
21+
```ruby
22+
# Use private token (recommended)
23+
client = ShopifyAPI::Clients::Graphql::Storefront.new(shop_url, private_token: storefront_private_access_token)
24+
25+
# Or use public token
26+
client = ShopifyAPI::Clients::Graphql::Storefront.new(shop_url, public_token: storefront_public_access_token)
27+
28+
# With API version
29+
client = ShopifyAPI::Clients::Graphql::Storefront.new(
30+
shop_url,
31+
private_token: storefront_private_access_token,
32+
api_version: "2024-01"
33+
)
34+
```
35+
36+
For more information on private vs public Storefront access tokens, see [Shopify's authentication documentation](https://shopify.dev/docs/api/usage/authentication#getting-started-with-private-access).
37+
## Removal of `LATEST_SUPPORTED_ADMIN_VERSION` and `RELEASE_CANDIDATE_ADMIN_VERSION` constants
38+
39+
The `LATEST_SUPPORTED_ADMIN_VERSION` and `RELEASE_CANDIDATE_ADMIN_VERSION` constants have been removed to prevent semantic versioning (semver) breaking changes. Previously, these constants would automatically update every quarter when new API versions were released, causing unintended breaking changes for apps.
40+
41+
### Migration Guide
42+
43+
**If you were using these constants directly:**
44+
45+
```ruby
46+
# Before (v14 and earlier)
47+
api_version = ShopifyAPI::LATEST_SUPPORTED_ADMIN_VERSION
48+
# or
49+
api_version = ShopifyAPI::RELEASE_CANDIDATE_ADMIN_VERSION
50+
51+
# After (v15+)
52+
api_version = "2025-07" # Explicitly specify the version you want to use
53+
```
54+
55+
**In your Context.setup:**
56+
57+
The `api_version` parameter has always been required in `Context.setup`, so most apps should not be affected. However, you must now explicitly specify which API version you want to use:
58+
59+
```ruby
60+
# Before (v14 and earlier)
61+
ShopifyAPI::Context.setup(
62+
api_key: "<api-key>",
63+
api_secret_key: "<api-secret-key>",
64+
host: "<https://application-host-name.com>",
65+
scope: "read_orders,read_products,etc",
66+
is_embedded: true,
67+
api_version: ShopifyAPI::LATEST_SUPPORTED_ADMIN_VERSION, # This constant no longer exists
68+
is_private: false,
69+
)
70+
71+
# After (v15+)
72+
ShopifyAPI::Context.setup(
73+
api_key: "<api-key>",
74+
api_secret_key: "<api-secret-key>",
75+
host: "<https://application-host-name.com>",
76+
scope: "read_orders,read_products,etc",
77+
is_embedded: true,
78+
api_version: "2025-07", # Explicitly specify the version
79+
is_private: false,
80+
)
81+
```
82+
83+
**Finding the right API version:**
84+
85+
You can see all supported API versions by referencing:
86+
```ruby
87+
ShopifyAPI::SUPPORTED_ADMIN_VERSIONS
88+
# => ["unstable", "2025-10", "2025-07", "2025-04", ...]
89+
```
90+
91+
**Why this change?**
92+
By requiring explicit version specification, apps can:
93+
- Control when they upgrade to new API versions
94+
- Test thoroughly before upgrading
95+
- Avoid unexpected breaking changes from automatic version updates
96+
397
## Removal of `ShopifyAPI::Webhooks::Handler`
498

599
The `ShopifyAPI::Webhooks::Handler` class has been removed in favor of `ShopifyAPI::Webhooks::WebhookHandler`. The `ShopifyAPI::Webhooks::WebhookHandler` class is now the recommended way to handle webhooks.
@@ -8,7 +102,8 @@ Make a module or class that includes or extends `ShopifyAPI::Webhooks::WebhookHa
8102

9103
In v14, adding new fields to the callback would become a breaking change. To make this code more flexible, handlers will now receive an object that can be typed and extended.
10104

11-
`data` will have the following keys
105+
`data` will have the following keys:
106+
12107
- `topic`, `String` - The topic of the webhook
13108
- `shop`, `String` - The shop domain of the webhook
14109
- `body`, `T::Hash[String, T.untyped]`- The body of the webhook
@@ -21,8 +116,8 @@ module WebhookHandler
21116
extend ShopifyAPI::Webhooks::WebhookHandler
22117

23118
class << self
24-
def handle_webhook(data:)
25-
puts "Received webhook! topic: #{data.topic} shop: #{data.shop} body: #{data.body} webhook_id: #{data.webhook_id} api_version: #{data.api_version"
119+
def handle(data:)
120+
puts "Received webhook! topic: #{data.topic} shop: #{data.shop} body: #{data.body} webhook_id: #{data.webhook_id} api_version: #{data.api_version}"
26121
end
27122
end
28123
end

BREAKING_CHANGES_FOR_V16.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Breaking change notice for version 16.0.0
2+
3+
## Minimum Ruby Version Requirement
4+
5+
The minimum required Ruby version has been updated from 3.0 to 3.2.
6+
7+
### Why this change?
8+
9+
Ruby 3.0 and 3.1 have reached End of Life (EOL).
10+
11+
### Migration Guide
12+
13+
If you're currently using Ruby 3.0 or 3.1, you'll need to upgrade to Ruby 3.2 or higher before upgrading to shopify-api-ruby v16.0.0.
14+
15+
**Note:** Ruby 3.2+ includes performance improvements and new features. Most applications should not require code changes beyond updating the Ruby version itself.
16+
## Removal of `Session#serialize` and `Session.deserialize` methods
17+
18+
The `Session#serialize` and `Session.deserialize` methods have been removed due to a security vulnerability. The `deserialize` method used `Oj.load` without safe mode, which allows instantiation of arbitrary Ruby objects.
19+
20+
These methods were originally created for session persistence when the library handled session storage. After session storage was deprecated in v12.3.0, applications became responsible for their own session persistence, making these methods unnecessary for their original purpose.
21+
22+
### Why this change?
23+
24+
**No impact on most applications:** The `shopify_app gem` stores individual session attributes in database columns and reconstructs sessions using `Session.new()`, which is the recommended pattern.
25+
26+
## Migration Guide
27+
28+
If your application was using `Session#serialize` and `Session.deserialize` for session persistence, you'll need to refactor to store individual session attributes and reconstruct sessions using `Session.new()`.
29+
30+
### Previous implementation (removed in v16.0.0)
31+
32+
```ruby
33+
# Storing a session
34+
session = ShopifyAPI::Auth::Session.new(
35+
shop: "example.myshopify.com",
36+
access_token: "shpat_xxxxx",
37+
scope: "read_products,write_orders"
38+
)
39+
40+
serialized_data = session.serialize
41+
# Store serialized_data in Redis, database, etc.
42+
redis.set("session:#{session.id}", serialized_data)
43+
44+
# Retrieving a session
45+
serialized_data = redis.get("session:#{session_id}")
46+
session = ShopifyAPI::Auth::Session.deserialize(serialized_data)
47+
```
48+
49+
### New implementation (required in v16.0.0)
50+
51+
Store individual session attributes and reconstruct using `Session.new()`:
52+
53+
## Reference: shopify_app gem implementation
54+
55+
The [shopify_app gem](https://github.com/Shopify/shopify_app) provides a reference implementation of session storage that follows these best practices:
56+
57+
**Shop Session Storage** ([source](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/shop_session_storage.rb)):
58+
```ruby
59+
# Stores attributes in database columns
60+
def store(auth_session)
61+
shop = find_or_initialize_by(shopify_domain: auth_session.shop)
62+
shop.shopify_token = auth_session.access_token
63+
shop.save!
64+
end
65+
66+
# Reconstructs using Session.new()
67+
def retrieve(id)
68+
shop = find_by(id: id)
69+
return unless shop
70+
71+
ShopifyAPI::Auth::Session.new(
72+
shop: shop.shopify_domain,
73+
access_token: shop.shopify_token
74+
)
75+
end
76+
```

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
Note: For changes to the API, see https://shopify.dev/changelog?filter=api
44
## Unreleased
5+
- ⚠️ [Breaking] Minimum required Ruby version is now 3.2. Ruby 3.0 and 3.1 are no longer supported.
6+
- ⚠️ [Breaking] Removed `Session#serialize` and `Session.deserialize` methods due to security concerns (RCE vulnerability via `Oj.load`). These methods were not used internally by the library. If your application relies on session serialization, use `Session.new()` to reconstruct sessions from stored attributes instead.
7+
8+
- Add support for expiring offline access tokens with refresh tokens. See [OAuth documentation](docs/usage/oauth.md#expiring-offline-access-tokens) for details.
9+
- Add `ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token` method to migrate existing non-expiring offline tokens to expiring tokens. See [migration documentation](docs/usage/oauth.md#migrating-non-expiring-tokens-to-expiring-tokens) for details.
10+
11+
### 15.0.0
12+
13+
- ⚠️ [Breaking] Removed deprecated `ShopifyAPI::Webhooks::Handler` interface. Apps must migrate to `ShopifyAPI::Webhooks::WebhookHandler` which provides `webhook_id` and `api_version` in addition to `topic`, `shop`, and `body`. See [BREAKING_CHANGES_FOR_V15.md](BREAKING_CHANGES_FOR_V15.md) for migration guide.
14+
- Add support for 2025-10 API version
15+
- Updated `LATEST_SUPPORTED_ADMIN_VERSION` to `2025-10`
16+
- [#1411](https://github.com/Shopify/shopify-api-ruby/pull/1411) Remove `LATEST_SUPPORTED_ADMIN_VERSION` and `RELEASE_CANDIDATE_ADMIN_VERSION` constants to prevent semver violations. Developers must now explicitly specify API versions. See the [migration guide](BREAKING_CHANGES_FOR_V15.md#removal-of-latest_supported_admin_version-and-release_candidate_admin_version-constants) for details.
517

618
- [#1405](https://github.com/Shopify/shopify-api-ruby/pull/1405) Fix webhook registration for topics containing dots (e.g., `customer.tags_added`, `customer.tags_removed`)
719

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
shopify_api (14.11.1)
4+
shopify_api (15.0.0)
55
activesupport
66
concurrent-ruby
77
hash_diff

docs/getting_started.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ ShopifyAPI::Context.setup(
2828
scope: "read_orders,read_products,etc",
2929
is_embedded: true, # Set to true if you are building an embedded app
3030
is_private: false, # Set to true if you are building a private app
31-
api_version: "2021-01" # The version of the API you would like to use
31+
api_version: "2021-01", # The version of the API you would like to use
32+
expiring_offline_access_tokens: true # Set to true to enable expiring offline access tokens with refresh tokens
3233
)
3334
```
3435

docs/usage/oauth.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ For more information on authenticating a Shopify app please see the [Types of Au
1313
- [Token Exchange](#token-exchange)
1414
- [Authorization Code Grant](#authorization-code-grant)
1515
- [Client Credentials Grant](#client-credentials-grant)
16+
- [Expiring Offline Access Tokens](#expiring-offline-access-tokens)
17+
- [Refreshing Access Tokens](#refreshing-access-tokens)
18+
- [Migrating Non-Expiring Tokens to Expiring Tokens](#migrating-non-expiring-tokens-to-expiring-tokens)
1619
- [Using OAuth Session to make authenticated API calls](#using-oauth-session-to-make-authenticated-api-calls)
1720

1821
## Session Persistence
@@ -305,6 +308,132 @@ end
305308

306309
```
307310

311+
## Expiring Offline Access Tokens
312+
313+
314+
To start requesting expiring offline access tokens, set the `expiring_offline_access_tokens` parameter to `true` when setting up the Shopify context:
315+
316+
```ruby
317+
ShopifyAPI::Context.setup(
318+
api_key: <SHOPIFY_API_KEY>,
319+
api_secret_key: <SHOPIFY_API_SECRET>,
320+
api_version: <SHOPIFY_API_VERSION>,
321+
scope: <SHOPIFY_API_SCOPES>,
322+
expiring_offline_access_tokens: true, # Enable expiring offline access tokens
323+
...
324+
)
325+
```
326+
327+
When enabled:
328+
- **Authorization Code Grant**: The OAuth flow will request expiring offline access tokens by sending `expiring: 1` parameter
329+
- **Token Exchange**: When requesting offline access tokens via token exchange, the flow will request expiring tokens
330+
331+
The resulting `Session` object will contain:
332+
- `access_token`: The access token that will eventually expire
333+
- `expires`: The expiration time for the access token
334+
- `refresh_token`: A token that can be used to refresh the access token
335+
- `refresh_token_expires`: The expiration time for the refresh token
336+
337+
### Refreshing Access Tokens
338+
339+
When your access token expires, you can use the refresh token to obtain a new access token using the `ShopifyAPI::Auth::RefreshToken.refresh_access_token` method.
340+
341+
#### Input
342+
| Parameter | Type | Required? | Notes |
343+
| -------------- | -------- | :-------: | ----------------------------------------------------------------------------------------------- |
344+
| `shop` | `String` | Yes | A Shopify domain name in the form `{exampleshop}.myshopify.com`. |
345+
| `refresh_token`| `String` | Yes | The refresh token from the session. |
346+
347+
#### Output
348+
This method returns a new `ShopifyAPI::Auth::Session` object with a fresh access token and a new refresh token. Your app should store this new session to replace the expired one.
349+
350+
#### Example
351+
```ruby
352+
def refresh_session(shop, refresh_token)
353+
begin
354+
# Refresh the access token using the refresh token
355+
new_session = ShopifyAPI::Auth::RefreshToken.refresh_access_token(
356+
shop: shop,
357+
refresh_token: refresh_token
358+
)
359+
360+
# Store the new session, replacing the old one
361+
MyApp::SessionRepository.store_shop_session(new_session)
362+
rescue ShopifyAPI::Errors::HttpResponseError => e
363+
puts("Failed to refresh access token: #{e.message}")
364+
raise e
365+
end
366+
end
367+
```
368+
#### Checking Token Expiration
369+
The `Session` object provides helper methods to check if tokens have expired:
370+
371+
```ruby
372+
session = MyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(shop)
373+
374+
# Check if the access token has expired
375+
if session.expired?
376+
# Access token has expired, refresh it
377+
new_session = ShopifyAPI::Auth::RefreshToken.refresh_access_token(
378+
shop: session.shop,
379+
refresh_token: session.refresh_token
380+
)
381+
MyApp::SessionRepository.store_shop_session(new_session)
382+
end
383+
384+
# Check if the refresh token has expired
385+
if session.refresh_token_expired?
386+
# Refresh token has expired, need to re-authenticate with OAuth
387+
end
388+
```
389+
390+
### Migrating Non-Expiring Tokens to Expiring Tokens
391+
392+
If you have existing non-expiring offline access tokens and want to migrate them to expiring tokens, you can use the `ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token` method. This performs a token exchange that converts your non-expiring offline token into an expiring one with a refresh token.
393+
394+
> [!WARNING]
395+
> This is a **one-time, irreversible migration** per shop. Once you migrate a shop's token to an expiring token, you cannot convert it back to a non-expiring token. The shop would need to reinstall your app with `expiring_offline_access_tokens: false` in your Context configuration to obtain a new non-expiring token.
396+
397+
#### Input
398+
| Parameter | Type | Required? | Notes |
399+
| -------------- | -------- | :-------: | ----------------------------------------------------------------------------------------------- |
400+
| `shop` | `String` | Yes | A Shopify domain name in the form `{exampleshop}.myshopify.com`. |
401+
| `non_expiring_offline_token` | `String` | Yes | The non-expiring offline access token to migrate. |
402+
403+
#### Output
404+
This method returns a new `ShopifyAPI::Auth::Session` object with an expiring access token and refresh token. Your app should store this new session to replace the non-expiring one.
405+
406+
#### Example
407+
```ruby
408+
def migrate_shop_to_expiring_offline_token(shop)
409+
# Retrieve the existing non-expiring session
410+
old_session = MyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(shop)
411+
412+
# Migrate to expiring token
413+
new_session = ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token(
414+
shop: shop,
415+
non_expiring_offline_token: old_session.access_token
416+
)
417+
418+
# Store the new expiring session, replacing the old one
419+
MyApp::SessionRepository.store_shop_session(new_session)
420+
end
421+
```
422+
423+
#### Migration Strategy
424+
When migrating your app to use expiring tokens, follow this order:
425+
426+
1. **Update your database schema** to add `expires_at` (timestamp), `refresh_token` (string) and `refresh_token_expires` (timestamp) columns to your session storage
427+
2. **Implement refresh logic** in your app to handle token expiration using `ShopifyAPI::Auth::RefreshToken.refresh_access_token`
428+
3. **Enable expiring tokens in your Context setup** so new installations will request and receive expiring tokens:
429+
```ruby
430+
ShopifyAPI::Context.setup(
431+
expiring_offline_access_tokens: true,
432+
# ... other config
433+
)
434+
```
435+
4. **Migrate existing non-expiring tokens** for shops that have already installed your app using the migration method above
436+
308437
## Using OAuth Session to make authenticated API calls
309438
Once your OAuth flow is complete, and you have persisted your `Session` object, you may use that `Session` object to make authenticated API calls.
310439

0 commit comments

Comments
 (0)