polar_sh 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +5 -2
- data/README.md +42 -5
- data/lib/polar/configuration.rb +1 -0
- data/lib/polar/resources/benefit.rb +31 -0
- data/lib/polar/resources/benefit_grant.rb +10 -0
- data/lib/polar/resources/order.rb +20 -0
- data/lib/polar/resources/refund.rb +15 -0
- data/lib/polar/resources/subscription.rb +52 -0
- data/lib/polar/version.rb +1 -1
- data/lib/polar/webhook.rb +49 -0
- data/lib/polar.rb +7 -0
- data/lib/standard_webhooks.rb +137 -0
- metadata +24 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84bce050ddccdcfb7709e2815db0ead0e4591cca1fe007972be02b217c5172da
|
4
|
+
data.tar.gz: 30c54873668cabb778e3134b69b1ddf19c12fcbb72d75a65c3f43ad35d938cdc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '078fcb5ab44cd333d9fc3cf935d22c55386c1395626d7d0c181792267be2c6f7515610f5ee7755670ed82d82d79c94b8c9b9f6b06e25e5740af836777fdf06ac'
|
7
|
+
data.tar.gz: 7365e3ebcf7953f097af85d56164769da44b36856661c369adf86d2c91eefd91b29ce9eeaa2cacf5794017489f433f426917138cca9c95729baa55e783952413
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -14,22 +14,59 @@ bundle add polar_sh
|
|
14
14
|
Polar.configure do |config|
|
15
15
|
config.access_token = "polar_..."
|
16
16
|
config.sandbox = true
|
17
|
+
config.webhook_secret = "xyz..."
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
|
20
|
+
class CheckoutsController < ApplicationController
|
21
|
+
def create
|
22
|
+
checkout = Polar::Checkout::Custom.create(
|
23
|
+
customer_email: current_user.email,
|
24
|
+
metadata: {user_id: current_user.id},
|
25
|
+
product_id: "xyzxyz-...",
|
26
|
+
success_url: root_path
|
27
|
+
)
|
28
|
+
|
29
|
+
redirect_to(checkout.url, allow_other_host: true)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class WebhooksController < ApplicationController
|
34
|
+
skip_before_action :verify_authenticity_token
|
35
|
+
|
36
|
+
def handle_polar
|
37
|
+
event = Polar::Webhook.verify(request)
|
38
|
+
|
39
|
+
Rails.logger.info("Received Polar webhook: #{event.type}")
|
40
|
+
|
41
|
+
case event.type
|
42
|
+
when "order.created"
|
43
|
+
process_order(event.object)
|
44
|
+
end
|
45
|
+
|
46
|
+
head(:ok)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def process_order(order)
|
52
|
+
return unless order.status == "paid"
|
53
|
+
return unless (user = User.find(order.metadata[:user_id]))
|
54
|
+
|
55
|
+
# ...
|
56
|
+
end
|
57
|
+
end
|
21
58
|
```
|
22
59
|
|
23
60
|
## Development
|
24
61
|
|
25
62
|
```sh
|
26
|
-
|
27
|
-
|
63
|
+
bundle
|
64
|
+
rake spec
|
28
65
|
```
|
29
66
|
|
30
67
|
## Contributing
|
31
68
|
|
32
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/mikker/polar_sh
|
69
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/mikker/polar_sh>.
|
33
70
|
|
34
71
|
## License
|
35
72
|
|
data/lib/polar/configuration.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polar
|
4
|
+
class Benefit < Resource
|
5
|
+
def self.list(params = {})
|
6
|
+
response = Client.get_request("/v1/benefits", **params)
|
7
|
+
handle_list(response)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.create(params)
|
11
|
+
response = Client.post_request("/v1/benefits", **params)
|
12
|
+
handle_one(response)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.get(id)
|
16
|
+
response = Client.get_request("/v1/benefits/#{id}")
|
17
|
+
handle_one(response)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.update(id, params)
|
21
|
+
response = Client.patch_request("/v1/benefits/#{id}", **params)
|
22
|
+
handle_one(response)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.delete(id)
|
26
|
+
response = Client.delete_request("/v1/benefits/#{id}")
|
27
|
+
handle_one(response)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polar
|
4
|
+
class Order < Resource
|
5
|
+
def self.list(params = {})
|
6
|
+
response = Client.get_request("/v1/orders", **params)
|
7
|
+
handle_list(response)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.get(id)
|
11
|
+
response = Client.get_request("/v1/orders/#{id}")
|
12
|
+
handle_one(response)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.get_invoice(id)
|
16
|
+
response = Client.get_request("/v1/orders/#{id}/invoice")
|
17
|
+
handle_one(response, Invoice)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polar
|
4
|
+
class Refund < Resource
|
5
|
+
def self.list(params = {})
|
6
|
+
response = Client.get_request("/v1/refunds", **params)
|
7
|
+
handle_list(response)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.create(params)
|
11
|
+
response = Client.post_request("/v1/refunds", **params)
|
12
|
+
handle_one(response)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polar
|
4
|
+
class Subscription < Resource
|
5
|
+
def self.list(params = {})
|
6
|
+
response = Client.get_request("/v1/subscriptions", **params)
|
7
|
+
handle_list(response)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.export(params = {})
|
11
|
+
response = Client.get_request("/v1/subscriptions/export", **params)
|
12
|
+
handle_list(response)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.get(id)
|
16
|
+
response = Client.get_request("/v1/subscriptions/#{id}")
|
17
|
+
handle_one(response)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.update(id, params)
|
21
|
+
response = Client.patch_request("/v1/subscriptions/#{id}", **params)
|
22
|
+
handle_one(response)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.delete(id)
|
26
|
+
response = Client.delete_request("/v1/subscriptions/#{id}")
|
27
|
+
handle_one(response)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Customer Portal methods
|
31
|
+
def self.list_for_customer(params = {})
|
32
|
+
response = Client.get_request("/v1/customer-portal/subscriptions", **params)
|
33
|
+
handle_list(response)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.get_for_customer(id)
|
37
|
+
response = Client.get_request("/v1/customer-portal/subscriptions/#{id}")
|
38
|
+
handle_one(response)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.update_for_customer(id, params)
|
42
|
+
response = Client.patch_request("/v1/customer-portal/subscriptions/#{id}", **params)
|
43
|
+
handle_one(response)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.delete_for_customer(id)
|
47
|
+
response = Client.delete_request("/v1/customer-portal/subscriptions/#{id}")
|
48
|
+
handle_one(response)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
data/lib/polar/version.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Polar
|
2
|
+
class Webhook
|
3
|
+
def initialize(request, secret: nil)
|
4
|
+
unless (unencoded_secret = secret || Polar.config.webhook_secret) && unencoded_secret != ""
|
5
|
+
raise ArgumentError, "No webhook secret provided, set Polar.config.webhook_secret"
|
6
|
+
end
|
7
|
+
|
8
|
+
@secret = Base64.encode64(unencoded_secret)
|
9
|
+
@request = request
|
10
|
+
@payload = JSON.parse(request.raw_post, symbolize_names: true)
|
11
|
+
@type = @payload[:type]
|
12
|
+
@object = cast_object
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :secret, :request, :payload, :type, :object
|
16
|
+
|
17
|
+
def verify
|
18
|
+
StandardWebhooks::Webhook.new(@secret).verify(request.raw_post, request.headers)
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def cast_object
|
23
|
+
case type
|
24
|
+
when /^checkout\./
|
25
|
+
Checkout::Custom.handle_one(payload[:data])
|
26
|
+
when /^order\./
|
27
|
+
Order.handle_one(payload[:data])
|
28
|
+
when /^subscription\./
|
29
|
+
Subscription.handle_one(payload[:data])
|
30
|
+
when /^refund\./
|
31
|
+
Refund.handle_one(payload[:data])
|
32
|
+
when /^product\./
|
33
|
+
Product.handle_one(payload[:data])
|
34
|
+
when /^pledge\./
|
35
|
+
Pledge.handle_one(payload[:data])
|
36
|
+
when /^organization\./
|
37
|
+
Organization.handle_one(payload[:data])
|
38
|
+
when /^benefit\./
|
39
|
+
Benefit.handle_one(payload[:data])
|
40
|
+
when /^benefit_grant\./
|
41
|
+
BenefitGrant.handle_one(payload[:data])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.verify(request, secret: nil)
|
46
|
+
new(request, secret: secret).verify
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/polar.rb
CHANGED
@@ -4,20 +4,27 @@ require "ostruct"
|
|
4
4
|
require "http"
|
5
5
|
|
6
6
|
require_relative "polar/version"
|
7
|
+
require "standard_webhooks"
|
7
8
|
|
8
9
|
module Polar
|
9
10
|
autoload :Configuration, "polar/configuration"
|
10
11
|
autoload :Client, "polar/client"
|
11
12
|
autoload :Error, "polar/error"
|
12
13
|
autoload :Resource, "polar/resource"
|
14
|
+
autoload :Webhook, "polar/webhook"
|
13
15
|
|
16
|
+
autoload :Benefit, "polar/resources/benefit"
|
17
|
+
autoload :BenefitGrant, "polar/resources/benefit_grant"
|
14
18
|
autoload :Customer, "polar/resources/customer"
|
15
19
|
autoload :CustomerSession, "polar/resources/customer_session"
|
16
20
|
autoload :Discount, "polar/resources/discount"
|
17
21
|
autoload :Checkout, "polar/resources/checkout"
|
18
22
|
autoload :LicenseKey, "polar/resources/license_key"
|
23
|
+
autoload :Order, "polar/resources/order"
|
19
24
|
autoload :Organization, "polar/resources/organization"
|
20
25
|
autoload :Product, "polar/resources/product"
|
26
|
+
autoload :Refund, "polar/resources/refund"
|
27
|
+
autoload :Subscription, "polar/resources/subscription"
|
21
28
|
autoload :User, "polar/resources/user"
|
22
29
|
|
23
30
|
class << self
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "openssl"
|
5
|
+
require "base64"
|
6
|
+
require "uri"
|
7
|
+
|
8
|
+
# Constant time string comparison, for fixed length strings.
|
9
|
+
# Code borrowed from ActiveSupport
|
10
|
+
# https://github.com/rails/rails/blob/75ac626c4e21129d8296d4206a1960563cc3d4aa/activesupport/lib/active_support/security_utils.rb#L33
|
11
|
+
#
|
12
|
+
# The values compared should be of fixed length, such as strings
|
13
|
+
# that have already been processed by HMAC. Raises in case of length mismatch.
|
14
|
+
module StandardWebhooks
|
15
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
16
|
+
def fixed_length_secure_compare(a, b)
|
17
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
def fixed_length_secure_compare(a, b)
|
21
|
+
raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
|
22
|
+
|
23
|
+
l = a.unpack("C#{a.bytesize}")
|
24
|
+
|
25
|
+
res = 0
|
26
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
27
|
+
res == 0
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module_function :fixed_length_secure_compare
|
32
|
+
|
33
|
+
# Secure string comparison for strings of variable length.
|
34
|
+
#
|
35
|
+
# While a timing attack would not be able to discern the content of
|
36
|
+
# a secret compared via secure_compare, it is possible to determine
|
37
|
+
# the secret length. This should be considered when using secure_compare
|
38
|
+
# to compare weak, short secrets to user input.
|
39
|
+
def secure_compare(a, b)
|
40
|
+
a.length == b.length && fixed_length_secure_compare(a, b)
|
41
|
+
end
|
42
|
+
|
43
|
+
module_function :secure_compare
|
44
|
+
|
45
|
+
class StandardWebhooksError < StandardError
|
46
|
+
attr_reader :message
|
47
|
+
|
48
|
+
def initialize(message = nil)
|
49
|
+
@message = message
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class WebhookVerificationError < StandardWebhooksError
|
54
|
+
end
|
55
|
+
|
56
|
+
class WebhookSigningError < StandardWebhooksError
|
57
|
+
end
|
58
|
+
|
59
|
+
class Webhook
|
60
|
+
def self.new_using_raw_bytes(secret)
|
61
|
+
self.new(secret.pack("C*").force_encoding("UTF-8"))
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize(secret)
|
65
|
+
if secret.start_with?(SECRET_PREFIX)
|
66
|
+
secret = secret[SECRET_PREFIX.length..-1]
|
67
|
+
end
|
68
|
+
|
69
|
+
@secret = Base64.decode64(secret)
|
70
|
+
end
|
71
|
+
|
72
|
+
def verify(payload, headers)
|
73
|
+
msg_id = headers["webhook-id"]
|
74
|
+
msg_signature = headers["webhook-signature"]
|
75
|
+
msg_timestamp = headers["webhook-timestamp"]
|
76
|
+
|
77
|
+
if !msg_signature || !msg_id || !msg_timestamp
|
78
|
+
raise WebhookVerificationError, "Missing required headers"
|
79
|
+
end
|
80
|
+
|
81
|
+
verify_timestamp(msg_timestamp)
|
82
|
+
|
83
|
+
_, signature = sign(msg_id, msg_timestamp, payload).split(",", 2)
|
84
|
+
|
85
|
+
passed_signatures = msg_signature.split(" ")
|
86
|
+
|
87
|
+
passed_signatures.each do |versioned_signature|
|
88
|
+
version, expected_signature = versioned_signature.split(",", 2)
|
89
|
+
|
90
|
+
if version != "v1"
|
91
|
+
next
|
92
|
+
end
|
93
|
+
|
94
|
+
if ::StandardWebhooks::secure_compare(signature, expected_signature)
|
95
|
+
return JSON.parse(payload, symbolize_names: true)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
raise WebhookVerificationError, "No matching signature found"
|
100
|
+
end
|
101
|
+
|
102
|
+
def sign(msg_id, timestamp, payload)
|
103
|
+
begin
|
104
|
+
now = Integer(timestamp)
|
105
|
+
rescue
|
106
|
+
raise WebhookSigningError, "Invalid timestamp"
|
107
|
+
end
|
108
|
+
|
109
|
+
to_sign = "#{msg_id}.#{timestamp}.#{payload}"
|
110
|
+
signature = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"), @secret, to_sign)).strip
|
111
|
+
|
112
|
+
return "v1,#{signature}"
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
SECRET_PREFIX = "whsec_"
|
118
|
+
TOLERANCE = 5 * 60
|
119
|
+
|
120
|
+
def verify_timestamp(timestamp_header)
|
121
|
+
begin
|
122
|
+
now = Integer(Time.now)
|
123
|
+
timestamp = Integer(timestamp_header)
|
124
|
+
rescue
|
125
|
+
raise WebhookVerificationError, "Invalid Signature Headers"
|
126
|
+
end
|
127
|
+
|
128
|
+
if timestamp < (now - TOLERANCE)
|
129
|
+
raise WebhookVerificationError, "Message timestamp too old"
|
130
|
+
end
|
131
|
+
|
132
|
+
if timestamp > (now + TOLERANCE)
|
133
|
+
raise WebhookVerificationError, "Message timestamp too new"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: polar_sh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
|
-
original_platform: ''
|
7
6
|
authors:
|
8
7
|
- Mikkel Malmberg
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: http
|
@@ -24,6 +23,20 @@ dependencies:
|
|
24
23
|
- - "~>"
|
25
24
|
- !ruby/object:Gem::Version
|
26
25
|
version: '5'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: ostruct
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.6'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.6'
|
27
40
|
description: Interact with the Polar API
|
28
41
|
email:
|
29
42
|
- mikkel@brnbw.com
|
@@ -40,17 +53,24 @@ files:
|
|
40
53
|
- lib/polar/configuration.rb
|
41
54
|
- lib/polar/error.rb
|
42
55
|
- lib/polar/resource.rb
|
56
|
+
- lib/polar/resources/benefit.rb
|
57
|
+
- lib/polar/resources/benefit_grant.rb
|
43
58
|
- lib/polar/resources/checkout.rb
|
44
59
|
- lib/polar/resources/checkout/custom.rb
|
45
60
|
- lib/polar/resources/customer.rb
|
46
61
|
- lib/polar/resources/customer_session.rb
|
47
62
|
- lib/polar/resources/discount.rb
|
48
63
|
- lib/polar/resources/license_key.rb
|
64
|
+
- lib/polar/resources/order.rb
|
49
65
|
- lib/polar/resources/organization.rb
|
50
66
|
- lib/polar/resources/product.rb
|
67
|
+
- lib/polar/resources/refund.rb
|
68
|
+
- lib/polar/resources/subscription.rb
|
51
69
|
- lib/polar/resources/user.rb
|
52
70
|
- lib/polar/version.rb
|
71
|
+
- lib/polar/webhook.rb
|
53
72
|
- lib/polar_sh.rb
|
73
|
+
- lib/standard_webhooks.rb
|
54
74
|
homepage: https://github.com/mikker/polar_sh
|
55
75
|
licenses:
|
56
76
|
- MIT
|
@@ -72,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
92
|
- !ruby/object:Gem::Version
|
73
93
|
version: '0'
|
74
94
|
requirements: []
|
75
|
-
rubygems_version: 3.6.
|
95
|
+
rubygems_version: 3.6.9
|
76
96
|
specification_version: 4
|
77
97
|
summary: API client for Polar.sh
|
78
98
|
test_files: []
|