polar_sh 0.1.0 → 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 +53 -19
- data/lib/polar/client.rb +1 -1
- data/lib/polar/configuration.rb +2 -1
- 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
@@ -1,38 +1,72 @@
|
|
1
|
-
#
|
1
|
+
# Polar.sh Ruby API client
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/polar_sh`. To experiment with that code, run `bin/console` for an interactive prompt.
|
3
|
+
Still in development. [API docs](https://docs.polar.sh/api)
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
-
|
11
|
-
Install the gem and add to the application's Gemfile by executing:
|
12
|
-
|
13
7
|
```bash
|
14
|
-
bundle add
|
8
|
+
bundle add polar_sh
|
15
9
|
```
|
16
10
|
|
17
|
-
|
11
|
+
## Usage
|
18
12
|
|
19
|
-
```
|
20
|
-
|
21
|
-
|
13
|
+
```ruby
|
14
|
+
Polar.configure do |config|
|
15
|
+
config.access_token = "polar_..."
|
16
|
+
config.sandbox = true
|
17
|
+
config.webhook_secret = "xyz..."
|
18
|
+
end
|
22
19
|
|
23
|
-
|
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
|
+
)
|
24
28
|
|
25
|
-
|
29
|
+
redirect_to(checkout.url, allow_other_host: true)
|
30
|
+
end
|
31
|
+
end
|
26
32
|
|
27
|
-
|
33
|
+
class WebhooksController < ApplicationController
|
34
|
+
skip_before_action :verify_authenticity_token
|
35
|
+
|
36
|
+
def handle_polar
|
37
|
+
event = Polar::Webhook.verify(request)
|
28
38
|
|
29
|
-
|
39
|
+
Rails.logger.info("Received Polar webhook: #{event.type}")
|
30
40
|
|
31
|
-
|
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
|
58
|
+
```
|
59
|
+
|
60
|
+
## Development
|
61
|
+
|
62
|
+
```sh
|
63
|
+
bundle
|
64
|
+
rake spec
|
65
|
+
```
|
32
66
|
|
33
67
|
## Contributing
|
34
68
|
|
35
|
-
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>.
|
36
70
|
|
37
71
|
## License
|
38
72
|
|
data/lib/polar/client.rb
CHANGED
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: []
|