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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +50 -0
- data/DEVELOPMENT.md +329 -0
- data/EXAMPLES.md +385 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +115 -0
- data/LICENSE +23 -0
- data/PROJECT_SUMMARY.md +256 -0
- data/README.md +635 -0
- data/Rakefile +24 -0
- data/examples/demo.rb +106 -0
- data/lib/polar/authentication.rb +83 -0
- data/lib/polar/client.rb +144 -0
- data/lib/polar/configuration.rb +46 -0
- data/lib/polar/customer_portal/benefit_grants.rb +41 -0
- data/lib/polar/customer_portal/customers.rb +69 -0
- data/lib/polar/customer_portal/license_keys.rb +70 -0
- data/lib/polar/customer_portal/orders.rb +82 -0
- data/lib/polar/customer_portal/subscriptions.rb +51 -0
- data/lib/polar/errors.rb +96 -0
- data/lib/polar/http_client.rb +150 -0
- data/lib/polar/pagination.rb +133 -0
- data/lib/polar/resources/base.rb +47 -0
- data/lib/polar/resources/benefits.rb +64 -0
- data/lib/polar/resources/checkouts.rb +75 -0
- data/lib/polar/resources/customers.rb +120 -0
- data/lib/polar/resources/events.rb +45 -0
- data/lib/polar/resources/files.rb +57 -0
- data/lib/polar/resources/license_keys.rb +81 -0
- data/lib/polar/resources/metrics.rb +30 -0
- data/lib/polar/resources/oauth2.rb +61 -0
- data/lib/polar/resources/orders.rb +54 -0
- data/lib/polar/resources/organizations.rb +41 -0
- data/lib/polar/resources/payments.rb +29 -0
- data/lib/polar/resources/products.rb +58 -0
- data/lib/polar/resources/subscriptions.rb +55 -0
- data/lib/polar/resources/webhooks.rb +81 -0
- data/lib/polar/version.rb +5 -0
- data/lib/polar/webhooks.rb +174 -0
- data/lib/polar.rb +65 -0
- 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: []
|