polar-ruby 0.1.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +50 -0
  4. data/DEVELOPMENT.md +329 -0
  5. data/EXAMPLES.md +385 -0
  6. data/Gemfile +12 -0
  7. data/Gemfile.lock +115 -0
  8. data/LICENSE +23 -0
  9. data/PROJECT_SUMMARY.md +256 -0
  10. data/README.md +635 -0
  11. data/Rakefile +24 -0
  12. data/examples/demo.rb +106 -0
  13. data/lib/polar/authentication.rb +83 -0
  14. data/lib/polar/client.rb +144 -0
  15. data/lib/polar/configuration.rb +46 -0
  16. data/lib/polar/customer_portal/benefit_grants.rb +41 -0
  17. data/lib/polar/customer_portal/customers.rb +69 -0
  18. data/lib/polar/customer_portal/license_keys.rb +70 -0
  19. data/lib/polar/customer_portal/orders.rb +82 -0
  20. data/lib/polar/customer_portal/subscriptions.rb +51 -0
  21. data/lib/polar/errors.rb +96 -0
  22. data/lib/polar/http_client.rb +150 -0
  23. data/lib/polar/pagination.rb +133 -0
  24. data/lib/polar/resources/base.rb +47 -0
  25. data/lib/polar/resources/benefits.rb +64 -0
  26. data/lib/polar/resources/checkouts.rb +75 -0
  27. data/lib/polar/resources/customers.rb +120 -0
  28. data/lib/polar/resources/events.rb +45 -0
  29. data/lib/polar/resources/files.rb +57 -0
  30. data/lib/polar/resources/license_keys.rb +81 -0
  31. data/lib/polar/resources/metrics.rb +30 -0
  32. data/lib/polar/resources/oauth2.rb +61 -0
  33. data/lib/polar/resources/orders.rb +54 -0
  34. data/lib/polar/resources/organizations.rb +41 -0
  35. data/lib/polar/resources/payments.rb +29 -0
  36. data/lib/polar/resources/products.rb +58 -0
  37. data/lib/polar/resources/subscriptions.rb +55 -0
  38. data/lib/polar/resources/webhooks.rb +81 -0
  39. data/lib/polar/version.rb +5 -0
  40. data/lib/polar/webhooks.rb +174 -0
  41. data/lib/polar.rb +65 -0
  42. metadata +239 -0
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+
6
+ module Polar
7
+ module Webhooks
8
+ class << self
9
+ # Validate webhook event signature
10
+ # @param payload [String] The raw request body
11
+ # @param headers [Hash] The request headers
12
+ # @param secret [String] The webhook secret
13
+ # @return [Hash] The validated event data
14
+ # @raise [WebhookVerificationError] If signature verification fails
15
+ def validate_event(payload, headers, secret)
16
+ timestamp = extract_timestamp(headers)
17
+ signature = extract_signature(headers)
18
+
19
+ verify_timestamp(timestamp)
20
+ verify_signature(payload, timestamp, signature, secret)
21
+
22
+ JSON.parse(payload)
23
+ rescue JSON::ParserError => e
24
+ raise WebhookError, "Invalid JSON payload: #{e.message}"
25
+ end
26
+
27
+ # Verify webhook signature without parsing payload
28
+ # @param payload [String] The raw request body
29
+ # @param timestamp [String] The timestamp from headers
30
+ # @param signature [String] The signature from headers
31
+ # @param secret [String] The webhook secret
32
+ # @return [Boolean] True if signature is valid
33
+ def verify_signature(payload, timestamp, signature, secret)
34
+ expected_signature = compute_signature(payload, timestamp, secret)
35
+
36
+ raise WebhookVerificationError, 'Invalid webhook signature' unless secure_compare(signature, expected_signature)
37
+
38
+ true
39
+ end
40
+
41
+ # Extract timestamp from webhook headers
42
+ # @param headers [Hash] The request headers
43
+ # @return [String] The timestamp
44
+ def extract_timestamp(headers)
45
+ timestamp = headers['polar-timestamp'] || headers['Polar-Timestamp']
46
+
47
+ raise WebhookVerificationError, 'Missing polar-timestamp header' unless timestamp
48
+
49
+ timestamp
50
+ end
51
+
52
+ # Extract signature from webhook headers
53
+ # @param headers [Hash] The request headers
54
+ # @return [String] The signature
55
+ def extract_signature(headers)
56
+ signature = headers['polar-signature'] || headers['Polar-Signature']
57
+
58
+ raise WebhookVerificationError, 'Missing polar-signature header' unless signature
59
+
60
+ # Remove the signature prefix if present (e.g., "v1=signature")
61
+ signature.split('=').last
62
+ end
63
+
64
+ # Verify timestamp is within tolerance
65
+ # @param timestamp [String] The timestamp to verify
66
+ # @param tolerance [Integer] Maximum age in seconds (default: 300 = 5 minutes)
67
+ def verify_timestamp(timestamp, tolerance: 300)
68
+ begin
69
+ webhook_time = Time.at(timestamp.to_i)
70
+ rescue ArgumentError
71
+ raise WebhookVerificationError, 'Invalid timestamp format'
72
+ end
73
+
74
+ current_time = Time.now
75
+ age = current_time - webhook_time
76
+
77
+ raise WebhookVerificationError, "Webhook timestamp too old (#{age.to_i}s > #{tolerance}s)" if age > tolerance
78
+
79
+ raise WebhookVerificationError, 'Webhook timestamp too far in future' if age < -tolerance
80
+
81
+ true
82
+ end
83
+
84
+ # Compute expected signature for payload
85
+ # @param payload [String] The raw request body
86
+ # @param timestamp [String] The timestamp
87
+ # @param secret [String] The webhook secret
88
+ # @return [String] The computed signature
89
+ def compute_signature(payload, timestamp, secret)
90
+ signed_payload = "#{timestamp}.#{payload}"
91
+
92
+ OpenSSL::HMAC.hexdigest(
93
+ OpenSSL::Digest.new('sha256'),
94
+ secret,
95
+ signed_payload
96
+ )
97
+ end
98
+
99
+ # Secure string comparison to prevent timing attacks
100
+ # @param a [String] First string
101
+ # @param b [String] Second string
102
+ # @return [Boolean] True if strings are equal
103
+ def secure_compare(a, b)
104
+ return false unless a.length == b.length
105
+
106
+ result = 0
107
+ a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
108
+ result == 0
109
+ end
110
+
111
+ # Parse webhook event type from payload
112
+ # @param payload [String, Hash] The webhook payload
113
+ # @return [String] The event type
114
+ def parse_event_type(payload)
115
+ data = payload.is_a?(String) ? JSON.parse(payload) : payload
116
+ data['type'] || data['event_type']
117
+ rescue JSON::ParserError
118
+ nil
119
+ end
120
+
121
+ # Check if event is a specific type
122
+ # @param payload [String, Hash] The webhook payload
123
+ # @param event_type [String] The expected event type
124
+ # @return [Boolean] True if event matches type
125
+ def event_type?(payload, event_type)
126
+ parse_event_type(payload) == event_type
127
+ end
128
+ end
129
+
130
+ # Webhook event wrapper class
131
+ class Event
132
+ attr_reader :id, :type, :created_at, :data, :raw_payload
133
+
134
+ def initialize(payload)
135
+ @raw_payload = payload.is_a?(String) ? payload : payload.to_json
136
+ parsed = payload.is_a?(String) ? JSON.parse(payload) : payload
137
+
138
+ @id = parsed['id']
139
+ @type = parsed['type'] || parsed['event_type']
140
+ @created_at = parsed['created_at'] ? Time.parse(parsed['created_at']) : nil
141
+ @data = parsed['data'] || parsed
142
+ end
143
+
144
+ def subscription_event?
145
+ type&.start_with?('subscription.')
146
+ end
147
+
148
+ def order_event?
149
+ type&.start_with?('order.')
150
+ end
151
+
152
+ def payment_event?
153
+ type&.start_with?('payment.')
154
+ end
155
+
156
+ def customer_event?
157
+ type&.start_with?('customer.')
158
+ end
159
+
160
+ def benefit_event?
161
+ type&.start_with?('benefit.')
162
+ end
163
+
164
+ def to_h
165
+ {
166
+ id: id,
167
+ type: type,
168
+ created_at: created_at,
169
+ data: data
170
+ }
171
+ end
172
+ end
173
+ end
174
+ end
data/lib/polar.rb ADDED
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+ require 'faraday/multipart'
6
+ require 'jwt'
7
+ require 'json'
8
+
9
+ require_relative 'polar/version'
10
+ require_relative 'polar/configuration'
11
+ require_relative 'polar/errors'
12
+ require_relative 'polar/http_client'
13
+ require_relative 'polar/authentication'
14
+ require_relative 'polar/pagination'
15
+ require_relative 'polar/webhooks'
16
+
17
+ # Core API Resources
18
+ require_relative 'polar/resources/base'
19
+ require_relative 'polar/resources/organizations'
20
+ require_relative 'polar/resources/products'
21
+ require_relative 'polar/resources/customers'
22
+ require_relative 'polar/resources/orders'
23
+ require_relative 'polar/resources/payments'
24
+ require_relative 'polar/resources/subscriptions'
25
+ require_relative 'polar/resources/checkouts'
26
+ require_relative 'polar/resources/benefits'
27
+ require_relative 'polar/resources/license_keys'
28
+ require_relative 'polar/resources/files'
29
+ require_relative 'polar/resources/metrics'
30
+ require_relative 'polar/resources/events'
31
+ require_relative 'polar/resources/webhooks'
32
+ require_relative 'polar/resources/oauth2'
33
+
34
+ # Customer Portal API Resources
35
+ require_relative 'polar/customer_portal/customers'
36
+ require_relative 'polar/customer_portal/orders'
37
+ require_relative 'polar/customer_portal/subscriptions'
38
+ require_relative 'polar/customer_portal/benefit_grants'
39
+ require_relative 'polar/customer_portal/license_keys'
40
+
41
+ require_relative 'polar/client'
42
+
43
+ module Polar
44
+ class Error < StandardError; end
45
+
46
+ class << self
47
+ # Configure the SDK with default settings
48
+ def configure
49
+ yield(configuration)
50
+ end
51
+
52
+ def configuration
53
+ @configuration ||= Configuration.new
54
+ end
55
+
56
+ def reset_configuration!
57
+ @configuration = nil
58
+ end
59
+
60
+ # Convenience method to create a new client
61
+ def new(options = {})
62
+ Client.new(options)
63
+ end
64
+ end
65
+ end
metadata ADDED
@@ -0,0 +1,239 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: polar-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hieu Nguyen
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-multipart
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: faraday-retry
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: jwt
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.21'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.21'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.21'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.21'
124
+ - !ruby/object:Gem::Dependency
125
+ name: vcr
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '6.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '6.0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: webmock
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '3.0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '3.0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: yard
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '0.9'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '0.9'
166
+ description: A comprehensive Ruby SDK for Polar.sh, providing easy integration with
167
+ their payment infrastructure, subscription management, and merchant services.
168
+ email:
169
+ - hieunt150@gmail.com
170
+ executables: []
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - ".rspec"
175
+ - CHANGELOG.md
176
+ - DEVELOPMENT.md
177
+ - EXAMPLES.md
178
+ - Gemfile
179
+ - Gemfile.lock
180
+ - LICENSE
181
+ - PROJECT_SUMMARY.md
182
+ - README.md
183
+ - Rakefile
184
+ - examples/demo.rb
185
+ - lib/polar.rb
186
+ - lib/polar/authentication.rb
187
+ - lib/polar/client.rb
188
+ - lib/polar/configuration.rb
189
+ - lib/polar/customer_portal/benefit_grants.rb
190
+ - lib/polar/customer_portal/customers.rb
191
+ - lib/polar/customer_portal/license_keys.rb
192
+ - lib/polar/customer_portal/orders.rb
193
+ - lib/polar/customer_portal/subscriptions.rb
194
+ - lib/polar/errors.rb
195
+ - lib/polar/http_client.rb
196
+ - lib/polar/pagination.rb
197
+ - lib/polar/resources/base.rb
198
+ - lib/polar/resources/benefits.rb
199
+ - lib/polar/resources/checkouts.rb
200
+ - lib/polar/resources/customers.rb
201
+ - lib/polar/resources/events.rb
202
+ - lib/polar/resources/files.rb
203
+ - lib/polar/resources/license_keys.rb
204
+ - lib/polar/resources/metrics.rb
205
+ - lib/polar/resources/oauth2.rb
206
+ - lib/polar/resources/orders.rb
207
+ - lib/polar/resources/organizations.rb
208
+ - lib/polar/resources/payments.rb
209
+ - lib/polar/resources/products.rb
210
+ - lib/polar/resources/subscriptions.rb
211
+ - lib/polar/resources/webhooks.rb
212
+ - lib/polar/version.rb
213
+ - lib/polar/webhooks.rb
214
+ homepage: https://github.com/hieunguyentrung/polar-ruby
215
+ licenses:
216
+ - MIT
217
+ metadata:
218
+ allowed_push_host: https://rubygems.org
219
+ homepage_uri: https://github.com/hieunguyentrung/polar-ruby
220
+ source_code_uri: https://github.com/hieunguyentrung/polar-ruby
221
+ changelog_uri: https://github.com/hieunguyentrung/polar-ruby/blob/main/CHANGELOG.md
222
+ rdoc_options: []
223
+ require_paths:
224
+ - lib
225
+ required_ruby_version: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: 2.7.0
230
+ required_rubygems_version: !ruby/object:Gem::Requirement
231
+ requirements:
232
+ - - ">="
233
+ - !ruby/object:Gem::Version
234
+ version: '0'
235
+ requirements: []
236
+ rubygems_version: 3.6.7
237
+ specification_version: 4
238
+ summary: Ruby SDK for Polar.sh payment infrastructure
239
+ test_files: []