dodopayments 2.35.0 → 2.36.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 759472ef7ceda29e93921ba3842b3d7f64e23700c06195f12636ac0758c38ea2
4
- data.tar.gz: 966be0a02c69b7f86643da56082d9a478199a658b2865a591b62684dbe294170
3
+ metadata.gz: 92bc6483e15b2acb4336b40dc7b0506cfcf6459c1df9534b5f2298f5c914578f
4
+ data.tar.gz: 031e012d15a53b9e54b167ecd3c294c82eec15c36f6553791b9831e53cdb5355
5
5
  SHA512:
6
- metadata.gz: 9ee34c03c5070b13a2631a9a85d1e21477206be0f943dba2f08b6138949891e6eb68bec0778bb1adc4df471e6c1c89963c158c29606cd166263c66fe7b2830c5
7
- data.tar.gz: b64bd1a280eb380543f5ec863b6034f4960b3fe886e1a684b4dd3324ef313b8ca3daca25a4da406ba654c2b44397539d690da6fcc424fb2f847bdb3a06d483dc
6
+ metadata.gz: 9a58eba2a64ac73d40c74e345aca8a06e15ecea1bbaa9e64e4812431ade3778748fe4fcdf543bba82fe8031fba96f9b25ab3cfd7059fc375dd27de5d348a28a5
7
+ data.tar.gz: 365c61a379aab7d5500c558a76d7153dda3486a4fabdd4af7e90185752aeeb18f2fa45f38116e714e58d5394315decff720996ed7c8ff31c6c111e6e9b35510a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.36.0](https://github.com/dodopayments/dodopayments-ruby/compare/v2.35.0...v2.36.0) (2026-10-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * **api:** regenerate SDK from latest OpenAPI spec ([8f4451c](https://github.com/dodopayments/dodopayments-ruby/commit/8f4451cf4bee3e5ed70f7d83b0e59ca1ca822a1e))
9
+ * **api:** regenerate SDK from latest OpenAPI spec ([3089c6d](https://github.com/dodopayments/dodopayments-ruby/commit/3089c6df767756323fc412e46b4c78100727dd0b))
10
+ * **api:** regenerate SDK from latest OpenAPI spec ([075d355](https://github.com/dodopayments/dodopayments-ruby/commit/075d35555b354afd7d6b3a4d16d1924805c28b4a))
11
+
3
12
  ## [2.35.0](https://github.com/dodopayments/dodopayments-ruby/compare/v2.34.0...v2.35.0) (2026-10-24)
4
13
 
5
14
 
data/README.md CHANGED
@@ -23,7 +23,7 @@ To use this gem, install via Bundler by adding the following to your application
23
23
  <!-- x-release-please-start-version -->
24
24
 
25
25
  ```ruby
26
- gem "dodopayments", "~> 2.35.0"
26
+ gem "dodopayments", "~> 2.36.0"
27
27
  ```
28
28
 
29
29
  <!-- x-release-please-end -->
@@ -72,6 +72,26 @@ if page.next_page?
72
72
  end
73
73
  ```
74
74
 
75
+ ### Workload identity
76
+
77
+ Instead of an API key, the client can exchange an OIDC token (JWT) from your platform for a short-lived access token. The client caches the access token and gets a new one when 5 minutes or less remain.
78
+
79
+ ```ruby
80
+ dodo_payments = Dodopayments::Client.new(
81
+ workload_identity: Dodopayments::WorkloadIdentity::Config.new(
82
+ identity_provider_id: "wip_...", # defaults to ENV["DODO_PAYMENTS_IDENTITY_PROVIDER_ID"]
83
+ provider: Dodopayments::WorkloadIdentity.github_actions
84
+ )
85
+ )
86
+
87
+ # Revoke the cached access token, for example at the end of a job.
88
+ dodo_payments.workload_identity.revoke
89
+ ```
90
+
91
+ The `Dodopayments::WorkloadIdentity` module has these token sources: `github_actions`, `gitlab`, `terraform_cloud`, `gcp`, `vercel`, `kubernetes`, `file`, `aws` (wraps your function), `static`, and `auto`. A `provider` can also be any object that responds to `call` and returns the JWT.
92
+
93
+ Do not set `bearer_token` together with `workload_identity`. If you set neither, and `ENV["DODO_PAYMENTS_API_KEY"]` is not set, the client reads `ENV["DODO_PAYMENTS_IDENTITY_PROVIDER_ID"]` and uses the token source that `auto` finds.
94
+
75
95
  ### Handling errors
76
96
 
77
97
  When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `Dodopayments::Errors::APIError` will be thrown:
@@ -21,12 +21,17 @@ module Dodopayments
21
21
  # rubocop:enable Style/MutableConstant
22
22
 
23
23
  # Bearer Token for API authentication
24
- # @return [String]
24
+ # @return [String, nil]
25
25
  attr_reader :bearer_token
26
26
 
27
27
  # @return [String, nil]
28
28
  attr_reader :webhook_key
29
29
 
30
+ # Workload identity session, or `nil` if the client uses a bearer token.
31
+ #
32
+ # @return [Dodopayments::WorkloadIdentity::Session, nil]
33
+ attr_reader :workload_identity
34
+
30
35
  # @return [Dodopayments::Resources::CheckoutSessions]
31
36
  attr_reader :checkout_sessions
32
37
 
@@ -114,12 +119,114 @@ module Dodopayments
114
119
  {"authorization" => "Bearer #{@bearer_token}"}
115
120
  end
116
121
 
122
+ # @api private
123
+ #
124
+ # With workload identity, a 401 response clears the cached access token. The
125
+ # client then gets a new access token and sends the request one more time.
126
+ #
127
+ # @param req [Hash{Symbol=>Object}]
128
+ #
129
+ # @raise [Dodopayments::Errors::APIError]
130
+ # @return [Object]
131
+ def request(req)
132
+ return super if @workload_identity.nil?
133
+
134
+ token = @workload_identity.access_token
135
+ begin
136
+ super(workload_identity_authorize(req, token))
137
+ rescue Dodopayments::Errors::AuthenticationError
138
+ @workload_identity.invalidate(token)
139
+ super(workload_identity_authorize(req, @workload_identity.access_token))
140
+ end
141
+ end
142
+
143
+ # @api private
144
+ #
145
+ # @param req [Hash{Symbol=>Object}]
146
+ # @param token [String]
147
+ #
148
+ # @return [Hash{Symbol=>Object}]
149
+ private def workload_identity_authorize(req, token)
150
+ {**req, headers: {**req[:headers].to_h, "authorization" => "Bearer #{token}"}}
151
+ end
152
+
153
+ # @api private
154
+ #
155
+ # Sends a workload identity request without the client credentials. It retries
156
+ # connection errors and 5xx responses, but not 4xx responses.
157
+ #
158
+ # @param method [Symbol]
159
+ # @param path [String]
160
+ # @param headers [Hash{String=>String}]
161
+ # @param body [Object, nil]
162
+ #
163
+ # @raise [Dodopayments::Errors::APIError]
164
+ # @return [Object]
165
+ private def workload_identity_request(method:, path:, headers:, body:)
166
+ url = Dodopayments::Internal::Util.join_parsed_uri(@base_url_components, {path: path})
167
+ headers = Dodopayments::Internal::Util.normalized_headers(@headers.except("authorization"), headers)
168
+ encoded = nil
169
+ if body.nil?
170
+ headers.delete("content-type")
171
+ else
172
+ headers, encoded = Dodopayments::Internal::Util.encode_content(headers, body)
173
+ end
174
+ request = {method: method, url: url, headers: headers, body: encoded, max_retries: 0, timeout: @timeout}
175
+
176
+ retry_count = 0
177
+ begin
178
+ _, response, stream = send_request(
179
+ request,
180
+ redirect_count: 0,
181
+ retry_count: retry_count,
182
+ send_retry_header: true
183
+ )
184
+ rescue Dodopayments::Errors::APIConnectionError, Dodopayments::Errors::InternalServerError => e
185
+ if retry_count < @max_retries
186
+ sleep(retry_delay(e.headers.to_h, retry_count: retry_count))
187
+ retry_count += 1
188
+ retry
189
+ end
190
+ raise workload_identity_error(e)
191
+ rescue Dodopayments::Errors::APIStatusError => e
192
+ raise workload_identity_error(e)
193
+ end
194
+
195
+ response_headers = Dodopayments::Internal::Util.normalized_headers(response.each_header.to_h)
196
+ Dodopayments::Internal::Util.decode_content(response_headers, stream: stream, suppress_error: true)
197
+ end
198
+
199
+ # @api private
200
+ #
201
+ # @param error [Dodopayments::Errors::APIError]
202
+ #
203
+ # @return [Dodopayments::Errors::APIError]
204
+ private def workload_identity_error(error)
205
+ return error unless error.is_a?(Dodopayments::Errors::APIStatusError)
206
+
207
+ body = error.body
208
+ details = body.is_a?(Hash) ? [body[:error], body[:error_description]].compact : []
209
+ message = ["Workload identity request failed with status #{error.status}", *details].join(": ")
210
+ Dodopayments::Errors::APIStatusError.for(
211
+ url: error.url,
212
+ status: error.status.to_i,
213
+ headers: error.headers,
214
+ body: body,
215
+ request: nil,
216
+ response: nil,
217
+ message: message
218
+ )
219
+ end
220
+
117
221
  # Creates and returns a new client for interacting with the API.
118
222
  #
119
223
  # @param bearer_token [String, nil] Bearer Token for API authentication Defaults to `ENV["DODO_PAYMENTS_API_KEY"]`
120
224
  #
121
225
  # @param webhook_key [String, nil] Defaults to `ENV["DODO_PAYMENTS_WEBHOOK_KEY"]`
122
226
  #
227
+ # @param workload_identity [Dodopayments::WorkloadIdentity::Config, Hash{Symbol=>Object}, nil] Exchange an OIDC token for an access token, instead of a bearer token. See
228
+ # {Dodopayments::WorkloadIdentity}.
229
+ #
123
230
  # @param environment [:live_mode, :test_mode, nil] Specifies the environment to use for the API.
124
231
  #
125
232
  # Each environment maps to a different base URL:
@@ -138,8 +245,9 @@ module Dodopayments
138
245
  #
139
246
  # @param max_retry_delay [Float]
140
247
  def initialize(
141
- bearer_token: ENV["DODO_PAYMENTS_API_KEY"],
248
+ bearer_token: nil,
142
249
  webhook_key: ENV["DODO_PAYMENTS_WEBHOOK_KEY"],
250
+ workload_identity: nil,
143
251
  environment: nil,
144
252
  base_url: ENV["DODO_PAYMENTS_BASE_URL"],
145
253
  max_retries: self.class::DEFAULT_MAX_RETRIES,
@@ -152,7 +260,17 @@ module Dodopayments
152
260
  raise ArgumentError.new(message)
153
261
  end
154
262
 
155
- if bearer_token.nil?
263
+ unless bearer_token.nil? || workload_identity.nil?
264
+ raise ArgumentError.new("bearer_token and workload_identity cannot be used together")
265
+ end
266
+ bearer_token = ENV["DODO_PAYMENTS_API_KEY"] if bearer_token.nil? && workload_identity.nil?
267
+ if bearer_token.nil? && workload_identity.nil? && !ENV["DODO_PAYMENTS_IDENTITY_PROVIDER_ID"].to_s.empty?
268
+ provider = Dodopayments::WorkloadIdentity.auto
269
+ workload_identity = {provider: provider} unless provider.nil?
270
+ end
271
+ workload_identity = Dodopayments::WorkloadIdentity::Config.coerce(workload_identity) unless workload_identity.nil?
272
+
273
+ if bearer_token.nil? && workload_identity.nil?
156
274
  raise ArgumentError.new("bearer_token is required, and can be set via environ: \"DODO_PAYMENTS_API_KEY\"")
157
275
  end
158
276
 
@@ -169,7 +287,7 @@ module Dodopayments
169
287
  headers = parsed.merge(headers)
170
288
  end
171
289
 
172
- @bearer_token = bearer_token.to_s
290
+ @bearer_token = bearer_token&.to_s
173
291
  @webhook_key = webhook_key&.to_s
174
292
 
175
293
  super(
@@ -207,6 +325,17 @@ module Dodopayments
207
325
  @entitlements = Dodopayments::Resources::Entitlements.new(client: self)
208
326
  @product_collections = Dodopayments::Resources::ProductCollections.new(client: self)
209
327
  @moderation = Dodopayments::Resources::Moderation.new(client: self)
328
+
329
+ @workload_identity = nil
330
+ return if workload_identity.nil?
331
+ @workload_identity = Dodopayments::WorkloadIdentity::Session.new(
332
+ config: workload_identity,
333
+ context: Dodopayments::WorkloadIdentity::Context.new(
334
+ base_url: @base_url,
335
+ audience: Dodopayments::Internal::Util.uri_origin(@base_url)
336
+ ),
337
+ transport: method(:workload_identity_request)
338
+ )
210
339
  end
211
340
  end
212
341
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dodopayments
4
- VERSION = "2.35.0"
4
+ VERSION = "2.36.0"
5
5
  end
@@ -0,0 +1,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dodopayments
4
+ # Workload identity federation: exchange a short-lived OIDC token (JWT) from
5
+ # your platform for a short-lived API access token.
6
+ #
7
+ # @example
8
+ # client = Dodopayments::Client.new(
9
+ # workload_identity: Dodopayments::WorkloadIdentity::Config.new(
10
+ # identity_provider_id: "wip_...",
11
+ # provider: Dodopayments::WorkloadIdentity.github_actions
12
+ # )
13
+ # )
14
+ module WorkloadIdentity
15
+ # @api private
16
+ GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
17
+
18
+ # @api private
19
+ SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
20
+
21
+ # The client refreshes the access token when this many seconds or less remain.
22
+ REFRESH_WINDOW_IN_SECONDS = 300
23
+
24
+ # @api private
25
+ PROVIDER_TIMEOUT_IN_SECONDS = 10
26
+
27
+ # @api private
28
+ KUBERNETES_TOKEN_PATH = "/var/run/secrets/tokens/dodo-token"
29
+
30
+ # @api private
31
+ GCP_METADATA_URL =
32
+ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity"
33
+
34
+ # The values that a subject token provider gets on each token exchange.
35
+ class Context
36
+ # The base URL of the client.
37
+ # @return [URI::Generic]
38
+ attr_reader :base_url
39
+
40
+ # The default audience: the origin of the client base URL.
41
+ # @return [String]
42
+ attr_reader :audience
43
+
44
+ # @api private
45
+ #
46
+ # @param base_url [URI::Generic]
47
+ # @param audience [String]
48
+ def initialize(base_url:, audience:)
49
+ @base_url = base_url
50
+ @audience = audience
51
+ end
52
+ end
53
+
54
+ # The `workload_identity` option of {Dodopayments::Client}.
55
+ class Config
56
+ # @return [String]
57
+ attr_reader :identity_provider_id
58
+
59
+ # @return [#call]
60
+ attr_reader :provider
61
+
62
+ # @param provider [#call] Returns the subject token (a JWT string). It can take one
63
+ # argument, a {Dodopayments::WorkloadIdentity::Context}.
64
+ #
65
+ # @param identity_provider_id [String, nil] Defaults to `ENV["DODO_PAYMENTS_IDENTITY_PROVIDER_ID"]`
66
+ def initialize(provider:, identity_provider_id: ENV["DODO_PAYMENTS_IDENTITY_PROVIDER_ID"])
67
+ if identity_provider_id.nil? || identity_provider_id.to_s.empty?
68
+ raise ArgumentError.new(
69
+ "identity_provider_id is required, and can be set via environ: \"DODO_PAYMENTS_IDENTITY_PROVIDER_ID\""
70
+ )
71
+ end
72
+ unless provider.respond_to?(:call)
73
+ raise ArgumentError.new("provider must respond to `call`")
74
+ end
75
+
76
+ @identity_provider_id = identity_provider_id.to_s
77
+ @provider = provider
78
+ end
79
+
80
+ # @api private
81
+ #
82
+ # @param value [Dodopayments::WorkloadIdentity::Config, Hash{Symbol=>Object}]
83
+ #
84
+ # @return [Dodopayments::WorkloadIdentity::Config]
85
+ def self.coerce(value)
86
+ case value
87
+ in Dodopayments::WorkloadIdentity::Config
88
+ value
89
+ in Hash
90
+ new(**value)
91
+ else
92
+ raise ArgumentError.new("workload_identity must be a #{name} or a Hash, got #{value.class}")
93
+ end
94
+ end
95
+
96
+ # @return [String]
97
+ def inspect = "#<#{self.class.name} identity_provider_id=#{@identity_provider_id}>"
98
+ end
99
+
100
+ # Caches the access token of one client. All methods are thread-safe.
101
+ class Session
102
+ # @return [String]
103
+ attr_reader :identity_provider_id
104
+
105
+ # @api private
106
+ #
107
+ # @param config [Dodopayments::WorkloadIdentity::Config]
108
+ # @param context [Dodopayments::WorkloadIdentity::Context]
109
+ # @param transport [Proc] Sends a request without the client credentials.
110
+ def initialize(config:, context:, transport:)
111
+ @identity_provider_id = config.identity_provider_id
112
+ @provider = config.provider
113
+ @context = context
114
+ @transport = transport
115
+ @mutex = Mutex.new
116
+ @access_token = nil
117
+ @expires_at = nil
118
+ end
119
+
120
+ # Returns a valid access token. This method does a token exchange if no token is
121
+ # cached, or if 5 minutes or less remain. Concurrent callers share one exchange.
122
+ #
123
+ # @raise [Dodopayments::Errors::APIError]
124
+ # @return [String]
125
+ def access_token
126
+ @mutex.synchronize do
127
+ exchange! unless fresh?
128
+ @access_token.to_s
129
+ end
130
+ end
131
+
132
+ # @api private
133
+ #
134
+ # Clears the cache, but only if it still holds `token`.
135
+ #
136
+ # @param token [String, nil]
137
+ def invalidate(token = nil)
138
+ @mutex.synchronize do
139
+ clear! if token.nil? || token == @access_token
140
+ end
141
+ nil
142
+ end
143
+
144
+ # Revokes the cached access token and clears the cache. Does nothing if no token
145
+ # is cached.
146
+ #
147
+ # @raise [Dodopayments::Errors::APIError]
148
+ def revoke
149
+ token = @mutex.synchronize { @access_token }
150
+ return if token.nil?
151
+
152
+ begin
153
+ @transport.call(
154
+ method: :post,
155
+ path: "workload-identity/token/revoke",
156
+ headers: {"authorization" => "Bearer #{token}"},
157
+ body: nil
158
+ )
159
+ ensure
160
+ invalidate(token)
161
+ end
162
+ nil
163
+ end
164
+
165
+ # @return [String]
166
+ def inspect = "#<#{self.class.name} identity_provider_id=#{@identity_provider_id}>"
167
+
168
+ private def fresh?
169
+ !@access_token.nil? && !@expires_at.nil? && @expires_at - Time.now > REFRESH_WINDOW_IN_SECONDS
170
+ end
171
+
172
+ private def clear!
173
+ @access_token = nil
174
+ @expires_at = nil
175
+ end
176
+
177
+ private def subject_token
178
+ token = @provider.respond_to?(:arity) && @provider.arity.zero? ? @provider.call : @provider.call(@context)
179
+ if !token.is_a?(String) || token.strip.empty?
180
+ raise Dodopayments::Errors::Error.new("The workload identity provider did not return a token")
181
+ end
182
+ token.strip
183
+ end
184
+
185
+ private def exchange!
186
+ clear!
187
+ body = {
188
+ grant_type: GRANT_TYPE,
189
+ subject_token_type: SUBJECT_TOKEN_TYPE,
190
+ subject_token: subject_token,
191
+ identity_provider_id: @identity_provider_id
192
+ }
193
+ now = Time.now
194
+ response = @transport.call(method: :post, path: "workload-identity/token", headers: {}, body: body)
195
+ token = response.is_a?(Hash) ? response[:access_token] : nil
196
+ if !token.is_a?(String) || token.empty?
197
+ raise Dodopayments::Errors::Error.new("The token exchange response has no access_token")
198
+ end
199
+
200
+ @expires_at = expiry(response, now: now)
201
+ @access_token = token
202
+ end
203
+
204
+ private def expiry(response, now:)
205
+ expires_at = response[:expires_at]
206
+ if expires_at.is_a?(String)
207
+ begin
208
+ return Time.iso8601(expires_at)
209
+ rescue ArgumentError
210
+ nil
211
+ end
212
+ end
213
+ now + Float(response[:expires_in], exception: false).to_f
214
+ end
215
+ end
216
+
217
+ class << self
218
+ # GitHub Actions. The job must have the `id-token: write` permission.
219
+ #
220
+ # @param audience [String, nil] Defaults to the origin of the client base URL.
221
+ #
222
+ # @return [Proc]
223
+ def github_actions(audience: nil)
224
+ lambda do |context|
225
+ url = env!("ACTIONS_ID_TOKEN_REQUEST_URL")
226
+ request_token = env!("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
227
+ separator = url.include?("?") ? "&" : "?"
228
+ uri = URI("#{url}#{separator}audience=#{CGI.escape(audience || context.audience)}")
229
+ body = http_get(uri, {"authorization" => "bearer #{request_token}"})
230
+ JSON.parse(body).fetch("value")
231
+ end
232
+ end
233
+
234
+ # GitLab CI/CD. Declare the token in `id_tokens` of the job.
235
+ #
236
+ # @param env_var [String]
237
+ #
238
+ # @return [Proc]
239
+ def gitlab(env_var: "DODO_PAYMENTS_ID_TOKEN") = ->(_context = nil) { env!(env_var) }
240
+
241
+ # HCP Terraform (Terraform Cloud) dynamic credentials.
242
+ #
243
+ # @return [Proc]
244
+ def terraform_cloud
245
+ lambda do |_context = nil|
246
+ ENV["TFC_WORKLOAD_IDENTITY_TOKEN"] || env!("TFC_WORKLOAD_IDENTITY_TOKEN_DODO")
247
+ end
248
+ end
249
+
250
+ # Google Cloud: the metadata server of the attached service account.
251
+ #
252
+ # @param audience [String, nil] Defaults to the origin of the client base URL.
253
+ #
254
+ # @return [Proc]
255
+ def gcp(audience: nil)
256
+ lambda do |context|
257
+ query = URI.encode_www_form(audience: audience || context.audience, format: "full")
258
+ http_get(URI("#{GCP_METADATA_URL}?#{query}"), {"metadata-flavor" => "Google"})
259
+ end
260
+ end
261
+
262
+ # Vercel OIDC federation.
263
+ #
264
+ # @return [Proc]
265
+ def vercel = ->(_context = nil) { env!("VERCEL_OIDC_TOKEN") }
266
+
267
+ # Kubernetes: a projected service account token. The file is read on each exchange.
268
+ #
269
+ # @param path [String]
270
+ #
271
+ # @return [Proc]
272
+ def kubernetes(path: KUBERNETES_TOKEN_PATH) = file(path: path)
273
+
274
+ # A token file. The file is read on each exchange.
275
+ #
276
+ # @param path [String, nil] Defaults to `ENV["DODO_PAYMENTS_IDENTITY_TOKEN_FILE"]`
277
+ #
278
+ # @return [Proc]
279
+ def file(path: ENV["DODO_PAYMENTS_IDENTITY_TOKEN_FILE"])
280
+ if path.nil? || path.empty?
281
+ raise ArgumentError.new(
282
+ "path is required, and can be set via environ: \"DODO_PAYMENTS_IDENTITY_TOKEN_FILE\""
283
+ )
284
+ end
285
+ ->(_context = nil) { File.read(path).strip }
286
+ end
287
+
288
+ # AWS: wraps your function that returns the JWT, for example from STS
289
+ # `GetWebIdentityToken`.
290
+ #
291
+ # @param fn [#call, nil]
292
+ #
293
+ # @return [Proc]
294
+ def aws(fn = nil, &blk)
295
+ callable = fn || blk
296
+ raise ArgumentError.new("aws requires a function or a block") if callable.nil?
297
+ ->(_context = nil) { callable.call }
298
+ end
299
+
300
+ # A fixed token. Use it for tests.
301
+ #
302
+ # @param token [String]
303
+ #
304
+ # @return [Proc]
305
+ def static(token) = ->(_context = nil) { token }
306
+
307
+ # Finds a token source in the environment, in this order: GitHub Actions, HCP
308
+ # Terraform, `ENV["DODO_PAYMENTS_IDENTITY_TOKEN_FILE"]`, and the Kubernetes path.
309
+ #
310
+ # @return [Proc, nil] `nil` if no source is found.
311
+ def auto
312
+ if ENV["ACTIONS_ID_TOKEN_REQUEST_URL"] && ENV["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
313
+ return github_actions
314
+ end
315
+ if ENV["TFC_WORKLOAD_IDENTITY_TOKEN"] || ENV["TFC_WORKLOAD_IDENTITY_TOKEN_DODO"]
316
+ return terraform_cloud
317
+ end
318
+ path = ENV["DODO_PAYMENTS_IDENTITY_TOKEN_FILE"]
319
+ return file(path: path) unless path.nil? || path.empty?
320
+ return kubernetes if File.file?(KUBERNETES_TOKEN_PATH)
321
+ nil
322
+ end
323
+
324
+ private def env!(name)
325
+ value = ENV[name]
326
+ if value.nil? || value.empty?
327
+ raise Dodopayments::Errors::Error.new("The environment variable #{name} is not set")
328
+ end
329
+ value
330
+ end
331
+
332
+ private def http_get(uri, headers)
333
+ response = Net::HTTP.start(
334
+ uri.host,
335
+ uri.port,
336
+ use_ssl: uri.scheme == "https",
337
+ open_timeout: PROVIDER_TIMEOUT_IN_SECONDS,
338
+ read_timeout: PROVIDER_TIMEOUT_IN_SECONDS
339
+ ) do |http|
340
+ http.get(uri.request_uri, headers)
341
+ end
342
+ unless response.is_a?(Net::HTTPSuccess)
343
+ raise Dodopayments::Errors::Error.new(
344
+ "The subject token request to #{uri.host} failed with status #{response.code}"
345
+ )
346
+ end
347
+ response.body.to_s
348
+ end
349
+ end
350
+ end
351
+ end
data/lib/dodopayments.rb CHANGED
@@ -55,6 +55,7 @@ require_relative "dodopayments/internal/transport/pooled_net_requester"
55
55
  require_relative "dodopayments/client"
56
56
  require_relative "dodopayments/internal/cursor_page_pagination"
57
57
  require_relative "dodopayments/internal/default_page_number_pagination"
58
+ require_relative "dodopayments/workload_identity"
58
59
  require_relative "dodopayments/models/blocklist/block_by_email"
59
60
  require_relative "dodopayments/models/blocklist/block_by_customer_id"
60
61
  require_relative "dodopayments/models/blocklist/create_blocked_customer_request"
@@ -20,12 +20,15 @@ module Dodopayments
20
20
  )
21
21
 
22
22
  # Bearer Token for API authentication
23
- sig { returns(String) }
23
+ sig { returns(T.nilable(String)) }
24
24
  attr_reader :bearer_token
25
25
 
26
26
  sig { returns(T.nilable(String)) }
27
27
  attr_reader :webhook_key
28
28
 
29
+ sig { returns(T.nilable(Dodopayments::WorkloadIdentity::Session)) }
30
+ attr_reader :workload_identity
31
+
29
32
  sig { returns(Dodopayments::Resources::CheckoutSessions) }
30
33
  attr_reader :checkout_sessions
31
34
 
@@ -114,6 +117,13 @@ module Dodopayments
114
117
  params(
115
118
  bearer_token: T.nilable(String),
116
119
  webhook_key: T.nilable(String),
120
+ workload_identity:
121
+ T.nilable(
122
+ T.any(
123
+ Dodopayments::WorkloadIdentity::Config,
124
+ T::Hash[Symbol, T.anything]
125
+ )
126
+ ),
117
127
  environment: T.nilable(T.any(Symbol, String)),
118
128
  base_url: T.nilable(String),
119
129
  max_retries: Integer,
@@ -124,9 +134,12 @@ module Dodopayments
124
134
  end
125
135
  def self.new(
126
136
  # Bearer Token for API authentication Defaults to `ENV["DODO_PAYMENTS_API_KEY"]`
127
- bearer_token: ENV["DODO_PAYMENTS_API_KEY"],
137
+ bearer_token: nil,
128
138
  # Defaults to `ENV["DODO_PAYMENTS_WEBHOOK_KEY"]`
129
139
  webhook_key: ENV["DODO_PAYMENTS_WEBHOOK_KEY"],
140
+ # Exchange an OIDC token for an access token, instead of a bearer token. See
141
+ # {Dodopayments::WorkloadIdentity}.
142
+ workload_identity: nil,
130
143
  # Specifies the environment to use for the API.
131
144
  #
132
145
  # Each environment maps to a different base URL:
@@ -0,0 +1,219 @@
1
+ # typed: strong
2
+
3
+ # typed: strong
4
+
5
+ module Dodopayments
6
+ # Workload identity federation: exchange a short-lived OIDC token (JWT) from
7
+ # your platform for a short-lived API access token.
8
+ module WorkloadIdentity
9
+ GRANT_TYPE = T.let(T.unsafe(nil), String)
10
+
11
+ SUBJECT_TOKEN_TYPE = T.let(T.unsafe(nil), String)
12
+
13
+ # The client refreshes the access token when this many seconds or less remain.
14
+ REFRESH_WINDOW_IN_SECONDS = T.let(T.unsafe(nil), Integer)
15
+
16
+ PROVIDER_TIMEOUT_IN_SECONDS = T.let(T.unsafe(nil), Integer)
17
+
18
+ KUBERNETES_TOKEN_PATH = T.let(T.unsafe(nil), String)
19
+
20
+ GCP_METADATA_URL = T.let(T.unsafe(nil), String)
21
+
22
+ # The values that a subject token provider gets on each token exchange.
23
+ class Context
24
+ # The base URL of the client.
25
+ sig { returns(URI::Generic) }
26
+ attr_reader :base_url
27
+
28
+ # The default audience: the origin of the client base URL.
29
+ sig { returns(String) }
30
+ attr_reader :audience
31
+
32
+ # @api private
33
+ sig do
34
+ params(base_url: URI::Generic, audience: String).returns(
35
+ T.attached_class
36
+ )
37
+ end
38
+ def self.new(base_url:, audience:)
39
+ end
40
+ end
41
+
42
+ # The `workload_identity` option of the client.
43
+ class Config
44
+ sig { returns(String) }
45
+ attr_reader :identity_provider_id
46
+
47
+ sig { returns(T.anything) }
48
+ attr_reader :provider
49
+
50
+ sig do
51
+ params(
52
+ provider: T.anything,
53
+ identity_provider_id: T.nilable(String)
54
+ ).returns(T.attached_class)
55
+ end
56
+ def self.new(
57
+ # Returns the subject token (a JWT string). It can take one argument, a
58
+ # `WorkloadIdentity::Context`.
59
+ provider:,
60
+ # Defaults to `ENV["DODO_PAYMENTS_IDENTITY_PROVIDER_ID"]`
61
+ identity_provider_id: ENV["DODO_PAYMENTS_IDENTITY_PROVIDER_ID"]
62
+ )
63
+ end
64
+
65
+ # @api private
66
+ sig do
67
+ params(
68
+ value:
69
+ T.any(
70
+ Dodopayments::WorkloadIdentity::Config,
71
+ T::Hash[Symbol, T.anything]
72
+ )
73
+ ).returns(Dodopayments::WorkloadIdentity::Config)
74
+ end
75
+ def self.coerce(value)
76
+ end
77
+
78
+ sig { returns(String) }
79
+ def inspect
80
+ end
81
+ end
82
+
83
+ # Caches the access token of one client. All methods are thread-safe.
84
+ class Session
85
+ sig { returns(String) }
86
+ attr_reader :identity_provider_id
87
+
88
+ # @api private
89
+ sig do
90
+ params(
91
+ config: Dodopayments::WorkloadIdentity::Config,
92
+ context: Dodopayments::WorkloadIdentity::Context,
93
+ transport: T.anything
94
+ ).returns(T.attached_class)
95
+ end
96
+ def self.new(config:, context:, transport:)
97
+ end
98
+
99
+ # Returns a valid access token. This method does a token exchange if no token is
100
+ # cached, or if 5 minutes or less remain. Concurrent callers share one exchange.
101
+ sig { returns(String) }
102
+ def access_token
103
+ end
104
+
105
+ # @api private
106
+ sig { params(token: T.nilable(String)).returns(NilClass) }
107
+ def invalidate(token = nil)
108
+ end
109
+
110
+ # Revokes the cached access token and clears the cache. Does nothing if no token
111
+ # is cached.
112
+ sig { returns(NilClass) }
113
+ def revoke
114
+ end
115
+
116
+ sig { returns(String) }
117
+ def inspect
118
+ end
119
+ end
120
+
121
+ Provider =
122
+ T.type_alias do
123
+ T
124
+ .proc
125
+ .params(context: Dodopayments::WorkloadIdentity::Context)
126
+ .returns(String)
127
+ end
128
+
129
+ class << self
130
+ # GitHub Actions. The job must have the `id-token: write` permission.
131
+ sig do
132
+ params(audience: T.nilable(String)).returns(
133
+ Dodopayments::WorkloadIdentity::Provider
134
+ )
135
+ end
136
+ def github_actions(
137
+ # Defaults to the origin of the client base URL.
138
+ audience: nil
139
+ )
140
+ end
141
+
142
+ # GitLab CI/CD. Declare the token in `id_tokens` of the job.
143
+ sig do
144
+ params(env_var: String).returns(
145
+ Dodopayments::WorkloadIdentity::Provider
146
+ )
147
+ end
148
+ def gitlab(env_var: "DODO_PAYMENTS_ID_TOKEN")
149
+ end
150
+
151
+ # HCP Terraform (Terraform Cloud) dynamic credentials.
152
+ sig { returns(Dodopayments::WorkloadIdentity::Provider) }
153
+ def terraform_cloud
154
+ end
155
+
156
+ # Google Cloud: the metadata server of the attached service account.
157
+ sig do
158
+ params(audience: T.nilable(String)).returns(
159
+ Dodopayments::WorkloadIdentity::Provider
160
+ )
161
+ end
162
+ def gcp(
163
+ # Defaults to the origin of the client base URL.
164
+ audience: nil
165
+ )
166
+ end
167
+
168
+ # Vercel OIDC federation.
169
+ sig { returns(Dodopayments::WorkloadIdentity::Provider) }
170
+ def vercel
171
+ end
172
+
173
+ # Kubernetes: a projected service account token. The file is read on each
174
+ # exchange.
175
+ sig do
176
+ params(path: String).returns(Dodopayments::WorkloadIdentity::Provider)
177
+ end
178
+ def kubernetes(path: KUBERNETES_TOKEN_PATH)
179
+ end
180
+
181
+ # A token file. The file is read on each exchange.
182
+ sig do
183
+ params(path: T.nilable(String)).returns(
184
+ Dodopayments::WorkloadIdentity::Provider
185
+ )
186
+ end
187
+ def file(
188
+ # Defaults to `ENV["DODO_PAYMENTS_IDENTITY_TOKEN_FILE"]`
189
+ path: ENV["DODO_PAYMENTS_IDENTITY_TOKEN_FILE"]
190
+ )
191
+ end
192
+
193
+ # AWS: wraps your function that returns the JWT, for example from STS
194
+ # `GetWebIdentityToken`.
195
+ sig do
196
+ params(
197
+ fn: T.nilable(T.anything),
198
+ blk: T.nilable(T.proc.returns(String))
199
+ ).returns(Dodopayments::WorkloadIdentity::Provider)
200
+ end
201
+ def aws(fn = nil, &blk)
202
+ end
203
+
204
+ # A fixed token. Use it for tests.
205
+ sig do
206
+ params(token: String).returns(Dodopayments::WorkloadIdentity::Provider)
207
+ end
208
+ def static(token)
209
+ end
210
+
211
+ # Finds a token source in the environment, in this order: GitHub Actions, HCP
212
+ # Terraform, `ENV["DODO_PAYMENTS_IDENTITY_TOKEN_FILE"]`, and the Kubernetes path.
213
+ # Returns `nil` if no source is found.
214
+ sig { returns(T.nilable(Dodopayments::WorkloadIdentity::Provider)) }
215
+ def auto
216
+ end
217
+ end
218
+ end
219
+ end
@@ -13,10 +13,12 @@ module Dodopayments
13
13
  test_mode: "https://test.dodopayments.com"
14
14
  }
15
15
 
16
- attr_reader bearer_token: String
16
+ attr_reader bearer_token: String?
17
17
 
18
18
  attr_reader webhook_key: String?
19
19
 
20
+ attr_reader workload_identity: Dodopayments::WorkloadIdentity::Session?
21
+
20
22
  attr_reader checkout_sessions: Dodopayments::Resources::CheckoutSessions
21
23
 
22
24
  attr_reader payments: Dodopayments::Resources::Payments
@@ -74,6 +76,9 @@ module Dodopayments
74
76
  def initialize: (
75
77
  ?bearer_token: String?,
76
78
  ?webhook_key: String?,
79
+ ?workload_identity: Dodopayments::WorkloadIdentity::Config
80
+ | ::Hash[Symbol, top]
81
+ | nil,
77
82
  ?environment: :live_mode | :test_mode | nil,
78
83
  ?base_url: String?,
79
84
  ?max_retries: Integer,
@@ -0,0 +1,89 @@
1
+ module Dodopayments
2
+ module WorkloadIdentity
3
+ GRANT_TYPE: String
4
+
5
+ SUBJECT_TOKEN_TYPE: String
6
+
7
+ REFRESH_WINDOW_IN_SECONDS: Integer
8
+
9
+ PROVIDER_TIMEOUT_IN_SECONDS: Integer
10
+
11
+ KUBERNETES_TOKEN_PATH: String
12
+
13
+ GCP_METADATA_URL: String
14
+
15
+ type provider = ^(Dodopayments::WorkloadIdentity::Context context) -> String
16
+
17
+ class Context
18
+ attr_reader base_url: URI::Generic
19
+
20
+ attr_reader audience: String
21
+
22
+ def initialize: (base_url: URI::Generic, audience: String) -> void
23
+ end
24
+
25
+ class Config
26
+ attr_reader identity_provider_id: String
27
+
28
+ attr_reader provider: top
29
+
30
+ def initialize: (provider: top, ?identity_provider_id: String?) -> void
31
+
32
+ def self.coerce: (
33
+ Dodopayments::WorkloadIdentity::Config | ::Hash[Symbol, top] value
34
+ ) -> Dodopayments::WorkloadIdentity::Config
35
+
36
+ def inspect: -> String
37
+ end
38
+
39
+ class Session
40
+ attr_reader identity_provider_id: String
41
+
42
+ def initialize: (
43
+ config: Dodopayments::WorkloadIdentity::Config,
44
+ context: Dodopayments::WorkloadIdentity::Context,
45
+ transport: top
46
+ ) -> void
47
+
48
+ def access_token: -> String
49
+
50
+ def invalidate: (?String? token) -> nil
51
+
52
+ def revoke: -> nil
53
+
54
+ def inspect: -> String
55
+ end
56
+
57
+ def self.github_actions: (
58
+ ?audience: String?
59
+ ) -> Dodopayments::WorkloadIdentity::provider
60
+
61
+ def self.gitlab: (
62
+ ?env_var: String
63
+ ) -> Dodopayments::WorkloadIdentity::provider
64
+
65
+ def self.terraform_cloud: -> Dodopayments::WorkloadIdentity::provider
66
+
67
+ def self.gcp: (
68
+ ?audience: String?
69
+ ) -> Dodopayments::WorkloadIdentity::provider
70
+
71
+ def self.vercel: -> Dodopayments::WorkloadIdentity::provider
72
+
73
+ def self.kubernetes: (
74
+ ?path: String
75
+ ) -> Dodopayments::WorkloadIdentity::provider
76
+
77
+ def self.file: (?path: String?) -> Dodopayments::WorkloadIdentity::provider
78
+
79
+ def self.aws: (
80
+ ?top fn
81
+ ) ?{
82
+ -> String
83
+ } -> Dodopayments::WorkloadIdentity::provider
84
+
85
+ def self.static: (String token) -> Dodopayments::WorkloadIdentity::provider
86
+
87
+ def self.auto: -> Dodopayments::WorkloadIdentity::provider?
88
+ end
89
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dodopayments
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.35.0
4
+ version: 2.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dodo Payments
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-24 00:00:00.000000000 Z
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cgi
@@ -542,6 +542,7 @@ files:
542
542
  - lib/dodopayments/resources/webhooks.rb
543
543
  - lib/dodopayments/resources/webhooks/headers.rb
544
544
  - lib/dodopayments/version.rb
545
+ - lib/dodopayments/workload_identity.rb
545
546
  - manifest.yaml
546
547
  - rbi/dodopayments/client.rbi
547
548
  - rbi/dodopayments/errors.rbi
@@ -1021,6 +1022,7 @@ files:
1021
1022
  - rbi/dodopayments/resources/webhooks.rbi
1022
1023
  - rbi/dodopayments/resources/webhooks/headers.rbi
1023
1024
  - rbi/dodopayments/version.rbi
1025
+ - rbi/dodopayments/workload_identity.rbi
1024
1026
  - sig/dodopayments/client.rbs
1025
1027
  - sig/dodopayments/errors.rbs
1026
1028
  - sig/dodopayments/file_part.rbs
@@ -1499,6 +1501,7 @@ files:
1499
1501
  - sig/dodopayments/resources/webhooks.rbs
1500
1502
  - sig/dodopayments/resources/webhooks/headers.rbs
1501
1503
  - sig/dodopayments/version.rbs
1504
+ - sig/dodopayments/workload_identity.rbs
1502
1505
  homepage: https://gemdocs.org/gems/dodopayments
1503
1506
  licenses: []
1504
1507
  metadata: