pago-sdk 1.0.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +131 -0
  4. data/lib/pago/base_client.rb +116 -0
  5. data/lib/pago/errors.rb +83 -0
  6. data/lib/pago/http.rb +78 -0
  7. data/lib/pago/model.rb +86 -0
  8. data/lib/pago/paginator.rb +67 -0
  9. data/lib/pago/serde.rb +186 -0
  10. data/lib/pago/service.rb +17 -0
  11. data/lib/pago/v2026_04/client.rb +124 -0
  12. data/lib/pago/v2026_04/enums.rb +4552 -0
  13. data/lib/pago/v2026_04/errors.rb +1121 -0
  14. data/lib/pago/v2026_04/models.rb +46344 -0
  15. data/lib/pago/v2026_04/services/benefit_grants.rb +48 -0
  16. data/lib/pago/v2026_04/services/benefits.rb +179 -0
  17. data/lib/pago/v2026_04/services/checkout_links.rb +131 -0
  18. data/lib/pago/v2026_04/services/checkouts.rb +185 -0
  19. data/lib/pago/v2026_04/services/custom_fields.rb +132 -0
  20. data/lib/pago/v2026_04/services/customer_meters.rb +69 -0
  21. data/lib/pago/v2026_04/services/customer_portal.rb +1181 -0
  22. data/lib/pago/v2026_04/services/customer_seats.rb +137 -0
  23. data/lib/pago/v2026_04/services/customer_sessions.rb +35 -0
  24. data/lib/pago/v2026_04/services/customers.rb +556 -0
  25. data/lib/pago/v2026_04/services/discounts.rb +131 -0
  26. data/lib/pago/v2026_04/services/disputes.rb +93 -0
  27. data/lib/pago/v2026_04/services/event_types.rb +74 -0
  28. data/lib/pago/v2026_04/services/events.rb +126 -0
  29. data/lib/pago/v2026_04/services/files.rb +135 -0
  30. data/lib/pago/v2026_04/services/license_keys.rb +183 -0
  31. data/lib/pago/v2026_04/services/members.rb +47 -0
  32. data/lib/pago/v2026_04/services/meters.rb +142 -0
  33. data/lib/pago/v2026_04/services/metrics.rb +188 -0
  34. data/lib/pago/v2026_04/services/oauth2.rb +175 -0
  35. data/lib/pago/v2026_04/services/orders.rb +238 -0
  36. data/lib/pago/v2026_04/services/organizations.rb +113 -0
  37. data/lib/pago/v2026_04/services/payments.rb +72 -0
  38. data/lib/pago/v2026_04/services/products.rb +142 -0
  39. data/lib/pago/v2026_04/services/refunds.rb +73 -0
  40. data/lib/pago/v2026_04/services/subscriptions.rb +171 -0
  41. data/lib/pago/v2026_04/services/webhooks.rb +212 -0
  42. data/lib/pago/v2026_04/unions.rb +739 -0
  43. data/lib/pago/v2026_04/webhooks.rb +86 -0
  44. data/lib/pago/version.rb +5 -0
  45. data/lib/pago/webhooks.rb +159 -0
  46. data/lib/pago.rb +39 -0
  47. data/sig/pago/v2026_04/generated.rbs +12401 -0
  48. data/sig/pago.rbs +204 -0
  49. metadata +91 -0
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pago
4
+ module V2026_04
5
+ # Webhook verification and deserialization for the 2026-04 API.
6
+ #
7
+ # @example Rack / Rails
8
+ # event = Pago::Webhooks.validate_event(
9
+ # body: request.body.read,
10
+ # headers: request.headers,
11
+ # secret: ENV.fetch("PAGO_WEBHOOK_SECRET")
12
+ # )
13
+ # case event
14
+ # when Pago::Models::WebhookOrderPaidPayload then fulfill(event.data)
15
+ # end
16
+ module Webhooks
17
+ # @return [Hash{String => Class}] event type to payload model.
18
+ def self.event_types
19
+ @event_types ||= {
20
+ "benefit.created" => Models::WebhookBenefitCreatedPayload,
21
+ "benefit.updated" => Models::WebhookBenefitUpdatedPayload,
22
+ "benefit_grant.created" => Models::WebhookBenefitGrantCreatedPayload,
23
+ "benefit_grant.cycled" => Models::WebhookBenefitGrantCycledPayload,
24
+ "benefit_grant.revoked" => Models::WebhookBenefitGrantRevokedPayload,
25
+ "benefit_grant.updated" => Models::WebhookBenefitGrantUpdatedPayload,
26
+ "checkout.created" => Models::WebhookCheckoutCreatedPayload,
27
+ "checkout.expired" => Models::WebhookCheckoutExpiredPayload,
28
+ "checkout.updated" => Models::WebhookCheckoutUpdatedPayload,
29
+ "customer.created" => Models::WebhookCustomerCreatedPayload,
30
+ "customer.deleted" => Models::WebhookCustomerDeletedPayload,
31
+ "customer.state_changed" => Models::WebhookCustomerStateChangedPayload,
32
+ "customer.updated" => Models::WebhookCustomerUpdatedPayload,
33
+ "customer_seat.assigned" => Models::WebhookCustomerSeatAssignedPayload,
34
+ "customer_seat.claimed" => Models::WebhookCustomerSeatClaimedPayload,
35
+ "customer_seat.revoked" => Models::WebhookCustomerSeatRevokedPayload,
36
+ "member.created" => Models::WebhookMemberCreatedPayload,
37
+ "member.deleted" => Models::WebhookMemberDeletedPayload,
38
+ "member.updated" => Models::WebhookMemberUpdatedPayload,
39
+ "order.created" => Models::WebhookOrderCreatedPayload,
40
+ "order.paid" => Models::WebhookOrderPaidPayload,
41
+ "order.refunded" => Models::WebhookOrderRefundedPayload,
42
+ "order.updated" => Models::WebhookOrderUpdatedPayload,
43
+ "organization.updated" => Models::WebhookOrganizationUpdatedPayload,
44
+ "product.created" => Models::WebhookProductCreatedPayload,
45
+ "product.updated" => Models::WebhookProductUpdatedPayload,
46
+ "refund.created" => Models::WebhookRefundCreatedPayload,
47
+ "refund.updated" => Models::WebhookRefundUpdatedPayload,
48
+ "subscription.active" => Models::WebhookSubscriptionActivePayload,
49
+ "subscription.canceled" => Models::WebhookSubscriptionCanceledPayload,
50
+ "subscription.created" => Models::WebhookSubscriptionCreatedPayload,
51
+ "subscription.past_due" => Models::WebhookSubscriptionPastDuePayload,
52
+ "subscription.paused" => Models::WebhookSubscriptionPausedPayload,
53
+ "subscription.resumed" => Models::WebhookSubscriptionResumedPayload,
54
+ "subscription.revoked" => Models::WebhookSubscriptionRevokedPayload,
55
+ "subscription.uncanceled" => Models::WebhookSubscriptionUncanceledPayload,
56
+ "subscription.updated" => Models::WebhookSubscriptionUpdatedPayload
57
+ }.freeze
58
+ end
59
+
60
+ # @return [Array<String>] every event type known to this SDK version.
61
+ def self.known_event_types = event_types.keys
62
+
63
+ # Verify a raw webhook request and deserialize its payload.
64
+ #
65
+ # @param body [String] the raw request body, exactly as received.
66
+ # @param headers [Hash{String => String}] the request headers.
67
+ # @param secret [String] the endpoint secret, in `whsec_<base64>` form.
68
+ # @raise [Pago::WebhookSecretError] when the secret is not a valid secret.
69
+ # @raise [Pago::WebhookVerificationError] when the signature does not match.
70
+ # @raise [Pago::WebhookUnknownTypeError] when the event type is unknown.
71
+ # @raise [Pago::WebhookError] when the payload cannot be parsed.
72
+ # @return [Pago::Model] the deserialized event payload.
73
+ def self.validate_event(body:, headers:, secret:)
74
+ data = ::Pago::WebhookVerifier.verify(body: body, headers: headers, secret: secret)
75
+ event_type = data.is_a?(Hash) ? data["type"] : nil
76
+ payload_class = event_types[event_type]
77
+ raise ::Pago::WebhookUnknownTypeError, event_type if payload_class.nil?
78
+
79
+ payload = payload_class.from_json(data)
80
+ raise ::Pago::WebhookError, "Failed to parse webhook payload" if payload.nil?
81
+
82
+ payload
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pago
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pago
4
+ # Base error raised while processing a Pago webhook.
5
+ class WebhookError < Error; end
6
+
7
+ # Raised when a webhook signature cannot be verified.
8
+ class WebhookVerificationError < WebhookError; end
9
+
10
+ # Raised when the endpoint secret is not a Standard Webhooks secret.
11
+ #
12
+ # A secret is the `whsec_` prefix followed by the base64 encoding of the
13
+ # signing key. Anything else — a missing prefix, an empty or malformed
14
+ # base64 body — is a configuration mistake, so it is reported instead of
15
+ # being silently treated as a signature mismatch.
16
+ class WebhookSecretError < WebhookVerificationError
17
+ def initialize(reason = nil)
18
+ message = "Invalid webhook secret: expected the \"whsec_\" prefix followed by base64 encoded bytes"
19
+ super(reason.nil? ? message : "#{message} (#{reason})")
20
+ end
21
+ end
22
+
23
+ # Raised when a verified webhook type is unknown to this SDK version.
24
+ class WebhookUnknownTypeError < WebhookError
25
+ # @return [String, nil] the event type carried by the payload.
26
+ attr_reader :event_type
27
+
28
+ def initialize(event_type)
29
+ @event_type = event_type
30
+ super("Unknown webhook event type: #{event_type.inspect}")
31
+ end
32
+ end
33
+
34
+ # Signature verification for incoming webhooks.
35
+ #
36
+ # Implements the Standard Webhooks specification: the `webhook-id`,
37
+ # `webhook-timestamp` and `webhook-signature` headers are required, the
38
+ # timestamp must sit within a five minute window, and the signature is an
39
+ # HMAC-SHA256 of `id.timestamp.body` keyed with the *decoded* secret — the
40
+ # base64 body of `whsec_<base64>`, never the secret string itself. The
41
+ # header may carry several space separated `v1,<base64>` signatures — any
42
+ # match accepts the payload, which is what makes secret rotation possible.
43
+ module WebhookVerifier
44
+ TOLERANCE_SECONDS = 5 * 60
45
+ SIGNATURE_VERSION = "v1"
46
+ SECRET_PREFIX = "whsec_"
47
+
48
+ module_function
49
+
50
+ # Verify a raw webhook request and return its parsed payload.
51
+ #
52
+ # @param body [String] the raw request body, exactly as received.
53
+ # @param headers [Hash{String => String}] the request headers, case insensitive.
54
+ # @param secret [String] the endpoint secret, in `whsec_<base64>` form.
55
+ # @raise [Pago::WebhookSecretError] when the secret is not a valid secret.
56
+ # @raise [Pago::WebhookVerificationError] when the signature does not match.
57
+ # @raise [Pago::WebhookError] when the body is not valid JSON.
58
+ # @return [Hash] the parsed payload.
59
+ def verify(body:, headers:, secret:)
60
+ payload = normalize_body(body)
61
+ verify_signature(payload, headers, secret)
62
+ parse(payload)
63
+ end
64
+
65
+ def normalize_body(body)
66
+ payload = body.to_s.dup.force_encoding(Encoding::UTF_8)
67
+ raise WebhookError, "Failed to parse webhook payload" unless payload.valid_encoding?
68
+
69
+ payload
70
+ end
71
+
72
+ def parse(payload)
73
+ JSON.parse(payload)
74
+ rescue JSON::ParserError
75
+ raise WebhookError, "Failed to parse webhook payload"
76
+ end
77
+
78
+ def verify_signature(body, headers, secret)
79
+ key = derive_key(secret)
80
+
81
+ normalized = (headers || {}).each_with_object({}) do |(name, value), result|
82
+ result[name.to_s.downcase] = value
83
+ end
84
+ id = normalized["webhook-id"]
85
+ timestamp = normalized["webhook-timestamp"]
86
+ signature = normalized["webhook-signature"]
87
+ raise WebhookVerificationError, "Missing required headers" if blank?(id) || blank?(timestamp) || blank?(signature)
88
+
89
+ check_tolerance(parse_timestamp(timestamp))
90
+
91
+ expected = OpenSSL::HMAC.digest("SHA256", key, "#{id}.#{timestamp}.#{body}".b)
92
+ return if signature.to_s.split(" ").any? { |candidate| match?(candidate, expected) }
93
+
94
+ raise WebhookVerificationError, "No matching signature found"
95
+ end
96
+
97
+ # Derive the HMAC key from a Standard Webhooks secret: strip the `whsec_`
98
+ # prefix and base64 decode the rest.
99
+ #
100
+ # @param secret [String] the endpoint secret.
101
+ # @raise [Pago::WebhookSecretError] when the secret is empty, unprefixed or not base64.
102
+ # @return [String] the raw signing key.
103
+ def derive_key(secret)
104
+ value = secret.to_s
105
+ raise WebhookSecretError, "the secret is empty" if value.empty?
106
+ raise WebhookSecretError, "missing the \"#{SECRET_PREFIX}\" prefix" unless value.start_with?(SECRET_PREFIX)
107
+
108
+ encoded = value[SECRET_PREFIX.length..]
109
+ raise WebhookSecretError, "nothing follows the prefix" if encoded.nil? || encoded.empty?
110
+
111
+ key = decode_base64(encoded)
112
+ raise WebhookSecretError, "the part after the prefix is not valid base64" if key.nil? || key.empty?
113
+
114
+ key
115
+ end
116
+
117
+ def parse_timestamp(value)
118
+ Integer(value.to_s, 10)
119
+ rescue ArgumentError, TypeError
120
+ raise WebhookVerificationError, "Invalid signature headers"
121
+ end
122
+
123
+ def check_tolerance(seconds)
124
+ now = Time.now.to_i
125
+ raise WebhookVerificationError, "Message timestamp too old" if seconds < now - TOLERANCE_SECONDS
126
+ raise WebhookVerificationError, "Message timestamp too new" if seconds > now + TOLERANCE_SECONDS
127
+ end
128
+
129
+ def match?(candidate, expected)
130
+ version, separator, signature = candidate.partition(",")
131
+ return false unless separator == "," && version == SIGNATURE_VERSION
132
+
133
+ decoded = decode_base64(signature)
134
+ return false if decoded.nil?
135
+
136
+ secure_compare(expected, decoded)
137
+ end
138
+
139
+ # Compare two strings without leaking their contents through timing.
140
+ def secure_compare(left, right)
141
+ if OpenSSL.respond_to?(:secure_compare)
142
+ OpenSSL.secure_compare(left, right)
143
+ elsif defined?(Rack::Utils) && Rack::Utils.respond_to?(:secure_compare)
144
+ Rack::Utils.secure_compare(left, right)
145
+ else
146
+ left.bytesize == right.bytesize && OpenSSL.fixed_length_secure_compare(left, right)
147
+ end
148
+ end
149
+
150
+ # Strict base64 decoding: padding is required and stray characters are refused.
151
+ def decode_base64(value)
152
+ value.to_s.unpack1("m0")
153
+ rescue ArgumentError
154
+ nil
155
+ end
156
+
157
+ def blank?(value) = value.nil? || value.to_s.empty?
158
+ end
159
+ end
data/lib/pago.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+ require "uri"
7
+
8
+ require_relative "pago/version"
9
+ require_relative "pago/errors"
10
+ require_relative "pago/serde"
11
+ require_relative "pago/model"
12
+ require_relative "pago/http"
13
+ require_relative "pago/paginator"
14
+ require_relative "pago/service"
15
+ require_relative "pago/base_client"
16
+ require_relative "pago/webhooks"
17
+ require_relative "pago/v2026_04/enums"
18
+ require_relative "pago/v2026_04/models"
19
+ require_relative "pago/v2026_04/unions"
20
+ require_relative "pago/v2026_04/errors"
21
+ require_relative "pago/v2026_04/webhooks"
22
+ require_relative "pago/v2026_04/client"
23
+
24
+ # Pago — a billing platform for the intelligence era.
25
+ #
26
+ # The constants below alias the latest API version, so `Pago::Client` always
27
+ # points at the newest generated client. Pin an explicit version module
28
+ # (e.g. `Pago::V2026_04::Client`) to opt out of that.
29
+ module Pago
30
+ # @return [Class] the client of the latest API version (2026-04).
31
+ Client = V2026_04::Client
32
+ Models = V2026_04::Models
33
+ Enums = V2026_04::Enums
34
+ Unions = V2026_04::Unions
35
+ Errors = V2026_04::Errors
36
+ Webhooks = V2026_04::Webhooks
37
+ Services = V2026_04::Services
38
+ LATEST_API_VERSION = "2026-04"
39
+ end