jamm 2.3.0 → 2.5.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 +6 -10
- data/Gemfile.lock +4 -2
- data/jamm.gemspec +1 -0
- data/lib/jamm/api/models/v1_charge_message.rb +4 -15
- data/lib/jamm/api/models/v1_error_type.rb +2 -1
- data/lib/jamm/api/models/v1_refund_info.rb +7 -7
- data/lib/jamm/api.rb +0 -1
- data/lib/jamm/client.rb +1 -0
- data/lib/jamm/version.rb +1 -1
- data/lib/jamm/webhook.rb +132 -138
- metadata +2 -3
- data/lib/jamm/api/models/charge_message_api_source.rb +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe888992acfb9739e5206ab7aaa3428f3e246998e633ff60e09999c2d96f3992
|
|
4
|
+
data.tar.gz: 6ee9932e4775b93a8b18e9dba2173871d6c36b0e727563111c8462801db481e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c33699a5edbb8cd8f52c5848bc5534b735f9ebca0990b45ceb2105408a2e5fbad7aca6205a0f788c5b6213e954d9a9c56e7d7f71f8f2cb47f491f7786afe658d
|
|
7
|
+
data.tar.gz: 73722cb39c3acbbd828c8011e4303f0ff1a8936b1dd390f34edbef5fe9dc26cf3375d6f025a8b20c8abd3e2a37fe80e3fbdc97b195dbbd899fcde93a099c3260
|
data/CHANGELOG.md
CHANGED
|
@@ -5,21 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [2.
|
|
8
|
+
## [2.5.0] - 2026-07-03
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
- Surface the refund `rfd-` id on the flat `refund_id` attribute in addition to the nested `refund.id`
|
|
14
|
-
- Parse `EVENT_TYPE_USER_ACCOUNT_DELETED` webhooks (previously raised `Unknown event type`)
|
|
12
|
+
- Added `Jamm::Webhook.verify_and_parse(raw_body)` — verifies the HMAC signature over the exact received `content` bytes and parses in one step. Unlike `Jamm::Webhook.verify`, it is not broken by JSON re-serialization (correctly handles `&`, `<`, `>` in content) and rejects bodies with duplicate top-level keys.
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
## [2.4.0] - 2026-07-01
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- `
|
|
21
|
-
- Nested webhook fields (e.g. `refund.error`) are now typed model instances instead of raw Hashes, so `error.code` / `error.message` work instead of raising `NoMethodError`
|
|
22
|
-
- `Webhook.parse` now accepts payloads with either string or symbol keys
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Send the `X-SDK-Version` request header (`ruby:<version>`) so the backend can track SDK version usage per merchant
|
|
23
19
|
|
|
24
20
|
## [2.2.0] - 2026-05-20
|
|
25
21
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
jamm (2.
|
|
4
|
+
jamm (2.5.0)
|
|
5
5
|
base64 (~> 0.2)
|
|
6
6
|
rest-client (~> 2.0)
|
|
7
7
|
typhoeus (~> 1.0, >= 1.0.1)
|
|
@@ -24,7 +24,9 @@ GEM
|
|
|
24
24
|
ffi (>= 1.15.0)
|
|
25
25
|
faker (3.5.1)
|
|
26
26
|
i18n (>= 1.8.11, < 2)
|
|
27
|
-
ffi (1.17.0)
|
|
27
|
+
ffi (1.17.0-aarch64-linux-gnu)
|
|
28
|
+
ffi (1.17.0-arm64-darwin)
|
|
29
|
+
ffi (1.17.0-x86_64-linux-gnu)
|
|
28
30
|
gimei (1.5.0)
|
|
29
31
|
hashdiff (1.1.0)
|
|
30
32
|
http-accept (1.7.0)
|
data/jamm.gemspec
CHANGED
|
@@ -62,8 +62,6 @@ module Api
|
|
|
62
62
|
|
|
63
63
|
attr_accessor :refund
|
|
64
64
|
|
|
65
|
-
attr_accessor :api_source
|
|
66
|
-
|
|
67
65
|
class EnumAttributeValidator
|
|
68
66
|
attr_reader :datatype
|
|
69
67
|
attr_reader :allowable_values
|
|
@@ -107,8 +105,7 @@ module Api
|
|
|
107
105
|
:'consumption_tax' => :'consumptionTax',
|
|
108
106
|
:'error' => :'error',
|
|
109
107
|
:'refund_id' => :'refundId',
|
|
110
|
-
:'refund' => :'refund'
|
|
111
|
-
:'api_source' => :'apiSource'
|
|
108
|
+
:'refund' => :'refund'
|
|
112
109
|
}
|
|
113
110
|
end
|
|
114
111
|
|
|
@@ -138,8 +135,7 @@ module Api
|
|
|
138
135
|
:'consumption_tax' => :'Integer',
|
|
139
136
|
:'error' => :'Apiv1Error',
|
|
140
137
|
:'refund_id' => :'String',
|
|
141
|
-
:'refund' => :'RefundInfo'
|
|
142
|
-
:'api_source' => :'ChargeMessageApiSource'
|
|
138
|
+
:'refund' => :'RefundInfo'
|
|
143
139
|
}
|
|
144
140
|
end
|
|
145
141
|
|
|
@@ -241,12 +237,6 @@ module Api
|
|
|
241
237
|
if attributes.key?(:'refund')
|
|
242
238
|
self.refund = attributes[:'refund']
|
|
243
239
|
end
|
|
244
|
-
|
|
245
|
-
if attributes.key?(:'api_source')
|
|
246
|
-
self.api_source = attributes[:'api_source']
|
|
247
|
-
else
|
|
248
|
-
self.api_source = 'API_SOURCE_UNSPECIFIED'
|
|
249
|
-
end
|
|
250
240
|
end
|
|
251
241
|
|
|
252
242
|
# Show invalid properties with the reasons. Usually used together with valid?
|
|
@@ -287,8 +277,7 @@ module Api
|
|
|
287
277
|
consumption_tax == o.consumption_tax &&
|
|
288
278
|
error == o.error &&
|
|
289
279
|
refund_id == o.refund_id &&
|
|
290
|
-
refund == o.refund
|
|
291
|
-
api_source == o.api_source
|
|
280
|
+
refund == o.refund
|
|
292
281
|
end
|
|
293
282
|
|
|
294
283
|
# @see the `==` method
|
|
@@ -300,7 +289,7 @@ module Api
|
|
|
300
289
|
# Calculates hash code according to all attributes.
|
|
301
290
|
# @return [Integer] Hash code
|
|
302
291
|
def hash
|
|
303
|
-
[id, customer, status, description, merchant_name, initial_amount, discount, final_amount, amount_refunded, currency, processed_at, jamm_fee, created_at, updated_at, original_transaction_jamm_fee, consumption_tax, error, refund_id, refund
|
|
292
|
+
[id, customer, status, description, merchant_name, initial_amount, discount, final_amount, amount_refunded, currency, processed_at, jamm_fee, created_at, updated_at, original_transaction_jamm_fee, consumption_tax, error, refund_id, refund].hash
|
|
304
293
|
end
|
|
305
294
|
|
|
306
295
|
# Builds the object from hash
|
|
@@ -38,6 +38,7 @@ module Api
|
|
|
38
38
|
PAYMENT_CUSTOMER_NOT_FOUND = "ERROR_TYPE_PAYMENT_CUSTOMER_NOT_FOUND".freeze
|
|
39
39
|
PAYMENT_CUSTOMER_INACTIVE = "ERROR_TYPE_PAYMENT_CUSTOMER_INACTIVE".freeze
|
|
40
40
|
PAYMENT_SERVICE_DISABLED = "ERROR_TYPE_PAYMENT_SERVICE_DISABLED".freeze
|
|
41
|
+
PAYMENT_BANK_UNAVAILABLE = "ERROR_TYPE_PAYMENT_BANK_UNAVAILABLE".freeze
|
|
41
42
|
CSV_VALIDATION_FAILED = "ERROR_TYPE_CSV_VALIDATION_FAILED".freeze
|
|
42
43
|
CSV_TOTP_REQUIRED = "ERROR_TYPE_CSV_TOTP_REQUIRED".freeze
|
|
43
44
|
CSV_TOTP_INVALID = "ERROR_TYPE_CSV_TOTP_INVALID".freeze
|
|
@@ -55,7 +56,7 @@ module Api
|
|
|
55
56
|
TOTP_DISABLE_FAILED = "ERROR_TYPE_TOTP_DISABLE_FAILED".freeze
|
|
56
57
|
|
|
57
58
|
def self.all_vars
|
|
58
|
-
@all_vars ||= [UNSPECIFIED, AUTH_FAILED, AUTH_REJECTED, ACCOUNT_CREATION_FAILED, ACCOUNT_MODIFICATION_FAILED, ACCOUNT_DELETION_FAILED, ACCOUNT_BANK_REGISTRATION_FAILED, KYC_REJECTED, NOTIFICATION_WEBHOOK_FAILED, NOTIFICATION_EMAIL_FAILED, NOTIFICATION_SMS_FAILED, PAYMENT_GATEWAY_UNAVAILABLE, PAYMENT_GATEWAY_FAILED, PAYMENT_VALIDATION_FAILED, PAYMENT_CHARGE_FAILED, PAYMENT_CHARGE_REJECTED, PAYMENT_CHARGE_OVER_LIMIT, PAYMENT_CHARGE_SUBSCRIPTION_EXPIRED, PAYMENT_LINK_EXPIRED, PAYMENT_CHARGE_INSUFFICIENT_FUNDS, PAYMENT_CUSTOMER_NOT_FOUND, PAYMENT_CUSTOMER_INACTIVE, PAYMENT_SERVICE_DISABLED, CSV_VALIDATION_FAILED, CSV_TOTP_REQUIRED, CSV_TOTP_INVALID, CSV_TOTP_EXPIRED, CSV_TOTP_LOCKED, CSV_BATCH_TOO_LARGE, CSV_CUSTOMER_NOT_FOUND, CSV_PROCESSING_FAILED, CSV_CHALLENGE_NOT_FOUND, CSV_DUPLICATE_USER, TOTP_SETUP_FAILED, TOTP_ALREADY_ENABLED, TOTP_NOT_ENABLED, TOTP_SETUP_INVALID, TOTP_DISABLE_FAILED].freeze
|
|
59
|
+
@all_vars ||= [UNSPECIFIED, AUTH_FAILED, AUTH_REJECTED, ACCOUNT_CREATION_FAILED, ACCOUNT_MODIFICATION_FAILED, ACCOUNT_DELETION_FAILED, ACCOUNT_BANK_REGISTRATION_FAILED, KYC_REJECTED, NOTIFICATION_WEBHOOK_FAILED, NOTIFICATION_EMAIL_FAILED, NOTIFICATION_SMS_FAILED, PAYMENT_GATEWAY_UNAVAILABLE, PAYMENT_GATEWAY_FAILED, PAYMENT_VALIDATION_FAILED, PAYMENT_CHARGE_FAILED, PAYMENT_CHARGE_REJECTED, PAYMENT_CHARGE_OVER_LIMIT, PAYMENT_CHARGE_SUBSCRIPTION_EXPIRED, PAYMENT_LINK_EXPIRED, PAYMENT_CHARGE_INSUFFICIENT_FUNDS, PAYMENT_CUSTOMER_NOT_FOUND, PAYMENT_CUSTOMER_INACTIVE, PAYMENT_SERVICE_DISABLED, PAYMENT_BANK_UNAVAILABLE, CSV_VALIDATION_FAILED, CSV_TOTP_REQUIRED, CSV_TOTP_INVALID, CSV_TOTP_EXPIRED, CSV_TOTP_LOCKED, CSV_BATCH_TOO_LARGE, CSV_CUSTOMER_NOT_FOUND, CSV_PROCESSING_FAILED, CSV_CHALLENGE_NOT_FOUND, CSV_DUPLICATE_USER, TOTP_SETUP_FAILED, TOTP_ALREADY_ENABLED, TOTP_NOT_ENABLED, TOTP_SETUP_INVALID, TOTP_DISABLE_FAILED].freeze
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
# Builds the enum from string
|
|
@@ -17,7 +17,7 @@ module Api
|
|
|
17
17
|
# RefundInfo contains refund-specific details for refund and refund_failed webhook events.
|
|
18
18
|
class RefundInfo
|
|
19
19
|
# External refund identifier (rfd-*).
|
|
20
|
-
attr_accessor :
|
|
20
|
+
attr_accessor :refund_id
|
|
21
21
|
|
|
22
22
|
# Amount refunded for this event.
|
|
23
23
|
attr_accessor :amount_refunded
|
|
@@ -39,7 +39,7 @@ module Api
|
|
|
39
39
|
# Attribute mapping from ruby-style variable name to JSON key.
|
|
40
40
|
def self.attribute_map
|
|
41
41
|
{
|
|
42
|
-
:'
|
|
42
|
+
:'refund_id' => :'refundId',
|
|
43
43
|
:'amount_refunded' => :'amountRefunded',
|
|
44
44
|
:'jamm_fee' => :'jammFee',
|
|
45
45
|
:'consumption_tax' => :'consumptionTax',
|
|
@@ -57,7 +57,7 @@ module Api
|
|
|
57
57
|
# Attribute type mapping.
|
|
58
58
|
def self.openapi_types
|
|
59
59
|
{
|
|
60
|
-
:'
|
|
60
|
+
:'refund_id' => :'String',
|
|
61
61
|
:'amount_refunded' => :'Integer',
|
|
62
62
|
:'jamm_fee' => :'Integer',
|
|
63
63
|
:'consumption_tax' => :'Integer',
|
|
@@ -88,8 +88,8 @@ module Api
|
|
|
88
88
|
h[k.to_sym] = v
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
if attributes.key?(:'
|
|
92
|
-
self.
|
|
91
|
+
if attributes.key?(:'refund_id')
|
|
92
|
+
self.refund_id = attributes[:'refund_id']
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
if attributes.key?(:'amount_refunded')
|
|
@@ -137,7 +137,7 @@ module Api
|
|
|
137
137
|
def ==(o)
|
|
138
138
|
return true if self.equal?(o)
|
|
139
139
|
self.class == o.class &&
|
|
140
|
-
|
|
140
|
+
refund_id == o.refund_id &&
|
|
141
141
|
amount_refunded == o.amount_refunded &&
|
|
142
142
|
jamm_fee == o.jamm_fee &&
|
|
143
143
|
consumption_tax == o.consumption_tax &&
|
|
@@ -155,7 +155,7 @@ module Api
|
|
|
155
155
|
# Calculates hash code according to all attributes.
|
|
156
156
|
# @return [Integer] Hash code
|
|
157
157
|
def hash
|
|
158
|
-
[
|
|
158
|
+
[refund_id, amount_refunded, jamm_fee, consumption_tax, original_transaction_fee_waived, error, processed_at].hash
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
# Builds the object from hash
|
data/lib/jamm/api.rb
CHANGED
|
@@ -19,7 +19,6 @@ require 'jamm/api/configuration'
|
|
|
19
19
|
# Models
|
|
20
20
|
require 'jamm/api/models/apiv1_error'
|
|
21
21
|
require 'jamm/api/models/apiv1_status'
|
|
22
|
-
require 'jamm/api/models/charge_message_api_source'
|
|
23
22
|
require 'jamm/api/models/customer_service_update_customer_body'
|
|
24
23
|
require 'jamm/api/models/googlerpc_status'
|
|
25
24
|
require 'jamm/api/models/protobuf_any'
|
data/lib/jamm/client.rb
CHANGED
|
@@ -9,6 +9,7 @@ module Jamm
|
|
|
9
9
|
base.config.host = Jamm.openapi.config.host
|
|
10
10
|
base.config.scheme = Jamm.openapi.config.scheme
|
|
11
11
|
base.default_headers['Authorization'] = "Bearer #{Jamm::OAuth.token}"
|
|
12
|
+
base.default_headers['X-SDK-Version'] = "ruby:#{Jamm::VERSION}"
|
|
12
13
|
|
|
13
14
|
# Platform feature, optionally set merchant id to call Jamm API
|
|
14
15
|
# on behalf of the merchant.
|
data/lib/jamm/version.rb
CHANGED
data/lib/jamm/webhook.rb
CHANGED
|
@@ -11,157 +11,39 @@ module Jamm
|
|
|
11
11
|
# Parse command is for parsing the received webhook message.
|
|
12
12
|
# It does not call anything remotely, instead returns the suitable object.
|
|
13
13
|
def self.parse(json)
|
|
14
|
-
|
|
15
|
-
# the caller decoded the JSON (e.g. JSON.parse with or without
|
|
16
|
-
# symbolize_names: true). Normalize to symbols so event-type routing,
|
|
17
|
-
# wrapper flattening, and field lookups are reliable either way.
|
|
18
|
-
json = deep_symbolize_keys(json)
|
|
19
|
-
|
|
20
|
-
out = build(Jamm::OpenAPI::MerchantWebhookMessage, json)
|
|
14
|
+
out = Jamm::OpenAPI::MerchantWebhookMessage.new(json)
|
|
21
15
|
|
|
22
16
|
case json[:event_type]
|
|
23
|
-
when Jamm::OpenAPI::EventType::CHARGE_CREATED
|
|
24
|
-
|
|
25
|
-
Jamm::OpenAPI::EventType::REFUND_SUCCEEDED,
|
|
26
|
-
Jamm::OpenAPI::EventType::REFUND_FAILED,
|
|
27
|
-
Jamm::OpenAPI::EventType::CHARGE_SUCCESS,
|
|
28
|
-
Jamm::OpenAPI::EventType::CHARGE_FAIL
|
|
29
|
-
out.content = build(Jamm::OpenAPI::ChargeMessage, flatten_charge_content(json[:content]))
|
|
17
|
+
when Jamm::OpenAPI::EventType::CHARGE_CREATED
|
|
18
|
+
out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
|
|
30
19
|
return out
|
|
31
20
|
|
|
32
|
-
when Jamm::OpenAPI::EventType::
|
|
33
|
-
out.content =
|
|
21
|
+
when Jamm::OpenAPI::EventType::CHARGE_UPDATED
|
|
22
|
+
out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
|
|
34
23
|
return out
|
|
35
24
|
|
|
36
|
-
when Jamm::OpenAPI::EventType::
|
|
37
|
-
out.content =
|
|
25
|
+
when Jamm::OpenAPI::EventType::REFUND_SUCCEEDED
|
|
26
|
+
out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
|
|
38
27
|
return out
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
raise 'Unknown event type'
|
|
42
|
-
end
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
# 1. Forward-compat: the Jamm backend can add new fields to webhook
|
|
49
|
-
# payloads at any time. The generated model `initialize` raises
|
|
50
|
-
# ArgumentError on any key outside `attribute_map`, so unknown keys are
|
|
51
|
-
# dropped first. Known keys are snake_case, matching `attribute_map`.
|
|
52
|
-
# 2. Numeric enums: the backend serializes webhook payloads with Go's
|
|
53
|
-
# `json.Marshal` (not protojson), so every enum field (status,
|
|
54
|
-
# api_source, ...) arrives as its integer value, while the generated
|
|
55
|
-
# enums are string-based. Each integer is mapped back to the enum string
|
|
56
|
-
# so it matches the values returned by the REST API.
|
|
57
|
-
# 3. Nested models: the generated `initialize` assigns nested objects
|
|
58
|
-
# verbatim (the `_deserialize` coercion only runs from `build_from_hash`,
|
|
59
|
-
# which expects camelCase keys the webhook does not use). So a nested
|
|
60
|
-
# field like `refund.error` would stay a raw Hash and `error.code` would
|
|
61
|
-
# raise NoMethodError. We coerce nested model fields (and arrays of them)
|
|
62
|
-
# recursively so the typed accessors work.
|
|
63
|
-
def self.build(klass, attributes)
|
|
64
|
-
return nil if attributes.nil?
|
|
65
|
-
|
|
66
|
-
known = klass.attribute_map
|
|
67
|
-
types = klass.openapi_types
|
|
68
|
-
filtered = attributes.each_with_object({}) do |(key, value), acc|
|
|
69
|
-
sym = key.to_sym
|
|
70
|
-
next unless known.key?(sym)
|
|
71
|
-
|
|
72
|
-
acc[sym] = coerce(types[sym], value)
|
|
73
|
-
end
|
|
29
|
+
when Jamm::OpenAPI::EventType::REFUND_FAILED
|
|
30
|
+
out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
|
|
31
|
+
return out
|
|
74
32
|
|
|
75
|
-
|
|
76
|
-
|
|
33
|
+
when Jamm::OpenAPI::EventType::CHARGE_SUCCESS
|
|
34
|
+
out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
|
|
35
|
+
return out
|
|
77
36
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def self.flatten_charge_content(content)
|
|
82
|
-
return content unless content.is_a?(Hash) && content.key?(:transaction)
|
|
83
|
-
|
|
84
|
-
refund = content[:refund]
|
|
85
|
-
# Keep `refund` as the raw Hash: `build` coerces it into a typed RefundInfo
|
|
86
|
-
# (and recursively types its nested `error`). Also surface the refund's
|
|
87
|
-
# `rfd-` id on the flat `refund_id` attribute the model documents.
|
|
88
|
-
charge = content[:transaction].merge(refund: refund)
|
|
89
|
-
charge[:refund_id] = refund[:id] if refund.is_a?(Hash) && !refund[:id].nil?
|
|
90
|
-
charge
|
|
91
|
-
end
|
|
37
|
+
when Jamm::OpenAPI::EventType::CHARGE_FAIL
|
|
38
|
+
out.content = Jamm::OpenAPI::ChargeMessage.new(json[:content])
|
|
39
|
+
return out
|
|
92
40
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# coerced by `T`. Anything else passes through untouched.
|
|
97
|
-
def self.coerce(type, value)
|
|
98
|
-
return value if value.nil?
|
|
99
|
-
|
|
100
|
-
inner = array_inner_type(type)
|
|
101
|
-
return coerce_array(inner, value) unless inner.nil?
|
|
102
|
-
|
|
103
|
-
klass = openapi_const(type)
|
|
104
|
-
return value if klass.nil?
|
|
105
|
-
|
|
106
|
-
if klass.respond_to?(:all_vars)
|
|
107
|
-
resolve_enum(klass, value)
|
|
108
|
-
elsif klass.respond_to?(:openapi_types) && value.is_a?(Hash)
|
|
109
|
-
build(klass, value)
|
|
110
|
-
else
|
|
111
|
-
value
|
|
41
|
+
when Jamm::OpenAPI::EventType::CONTRACT_ACTIVATED
|
|
42
|
+
out.content = Jamm::OpenAPI::ContractMessage.new(json[:content])
|
|
43
|
+
return out
|
|
112
44
|
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Coerce each element of an `Array<T>` field by its inner type `T`.
|
|
116
|
-
def self.coerce_array(inner_type, value)
|
|
117
|
-
return value unless value.is_a?(Array)
|
|
118
45
|
|
|
119
|
-
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Extract `T` from an `Array<T>` openapi type, or nil when not an array type.
|
|
123
|
-
def self.array_inner_type(type)
|
|
124
|
-
match = type.to_s.match(/\AArray<(.+)>\z/)
|
|
125
|
-
match && match[1]
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Map a numeric enum wire value onto its string enum constant. A value that
|
|
129
|
-
# is already a string (REST-style) passes through untouched.
|
|
130
|
-
def self.resolve_enum(enum, value)
|
|
131
|
-
return value unless value.is_a?(Integer)
|
|
132
|
-
|
|
133
|
-
vars = enum.all_vars
|
|
134
|
-
# Guard the bounds explicitly: Ruby maps negative indices from the end of
|
|
135
|
-
# the array, so any unexpected wire value must fall back to the *_UNSPECIFIED
|
|
136
|
-
# member (index 0) rather than silently selecting the wrong constant.
|
|
137
|
-
value.between?(0, vars.length - 1) ? vars[value] : vars[0]
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Resolve an openapi_types entry (e.g. :ChargeMessageApiSource, :RefundInfo)
|
|
141
|
-
# to its generated class, or nil when the type is a primitive (String,
|
|
142
|
-
# Integer, ...) or otherwise unresolvable.
|
|
143
|
-
def self.openapi_const(type)
|
|
144
|
-
return nil if type.nil?
|
|
145
|
-
|
|
146
|
-
name = type.to_s
|
|
147
|
-
return nil unless Jamm::OpenAPI.const_defined?(name)
|
|
148
|
-
|
|
149
|
-
Jamm::OpenAPI.const_get(name)
|
|
150
|
-
rescue NameError
|
|
151
|
-
nil
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Recursively convert Hash keys to symbols so parsing is robust regardless of
|
|
155
|
-
# how the caller decoded the webhook JSON.
|
|
156
|
-
def self.deep_symbolize_keys(value)
|
|
157
|
-
case value
|
|
158
|
-
when Hash
|
|
159
|
-
value.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = deep_symbolize_keys(v) }
|
|
160
|
-
when Array
|
|
161
|
-
value.map { |v| deep_symbolize_keys(v) }
|
|
162
|
-
else
|
|
163
|
-
value
|
|
164
|
-
end
|
|
46
|
+
raise 'Unknown event type'
|
|
165
47
|
end
|
|
166
48
|
|
|
167
49
|
# Verify message.
|
|
@@ -181,6 +63,118 @@ module Jamm
|
|
|
181
63
|
raise Jamm::InvalidSignatureError, 'Digests do not match'
|
|
182
64
|
end
|
|
183
65
|
|
|
66
|
+
# Verify the HMAC signature over the exact received bytes and parse, in one step.
|
|
67
|
+
#
|
|
68
|
+
# This is the recommended entry point. The backend signs the raw +content+ bytes it
|
|
69
|
+
# transmits, produced by Go's JSON encoder which HTML-escapes & < > as & <
|
|
70
|
+
# >. Re-serializing the parsed content (as +verify+ does via JSON.dump) un-escapes
|
|
71
|
+
# those characters, so its digest no longer matches. This slices the raw +content+
|
|
72
|
+
# substring out of +raw_body+ verbatim and HMACs that.
|
|
73
|
+
def self.verify_and_parse(raw_body)
|
|
74
|
+
raise ArgumentError, 'raw_body cannot be nil or empty' if raw_body.nil? || raw_body.empty?
|
|
75
|
+
|
|
76
|
+
parsed = JSON.parse(raw_body, symbolize_names: true)
|
|
77
|
+
signature = parsed[:signature]
|
|
78
|
+
raise ArgumentError, "Webhook body is missing the 'signature' field" if signature.nil? || signature.empty?
|
|
79
|
+
|
|
80
|
+
raw_content = extract_raw_content(raw_body)
|
|
81
|
+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), Jamm.client_secret, raw_content)
|
|
82
|
+
given = "sha256=#{digest}"
|
|
83
|
+
raise Jamm::InvalidSignatureError, 'Digests do not match' unless secure_compare(given, signature)
|
|
84
|
+
|
|
85
|
+
parse(parsed)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Extracts the top-level +content+ value substring from a raw webhook body verbatim,
|
|
89
|
+
# without decoding or re-serializing it, so the exact signed bytes are recovered.
|
|
90
|
+
def self.extract_raw_content(raw_body)
|
|
91
|
+
i = 0
|
|
92
|
+
n = raw_body.length
|
|
93
|
+
i += 1 while i < n && raw_body[i].match?(/\s/)
|
|
94
|
+
raise ArgumentError, 'Webhook body must be a JSON object' unless raw_body[i] == '{'
|
|
95
|
+
|
|
96
|
+
i += 1
|
|
97
|
+
# Scan every top-level key. Duplicate keys are rejected: JSON.parse keeps the LAST
|
|
98
|
+
# occurrence while this returns the FIRST, so a duplicate `content` could otherwise
|
|
99
|
+
# verify one payload and parse another (signature bypass).
|
|
100
|
+
seen = {}
|
|
101
|
+
content = nil
|
|
102
|
+
loop do
|
|
103
|
+
i += 1 while i < n && (raw_body[i].match?(/\s/) || raw_body[i] == ',')
|
|
104
|
+
break if i >= n || raw_body[i] == '}'
|
|
105
|
+
raise ArgumentError, 'Malformed webhook JSON' unless raw_body[i] == '"'
|
|
106
|
+
|
|
107
|
+
key_start = i
|
|
108
|
+
i = skip_string(raw_body, i)
|
|
109
|
+
# Decode the key (not a raw slice): otherwise an escaped duplicate such as
|
|
110
|
+
# "content" would evade both the duplicate check and the 'content' match below,
|
|
111
|
+
# while JSON.parse collapses it to `content` and keeps the last value. Normalize a
|
|
112
|
+
# malformed key to ArgumentError to match the rest of this method.
|
|
113
|
+
key = begin
|
|
114
|
+
JSON.parse(raw_body[key_start...i])
|
|
115
|
+
rescue JSON::ParserError
|
|
116
|
+
raise ArgumentError, 'Malformed webhook JSON'
|
|
117
|
+
end
|
|
118
|
+
raise ArgumentError, "Duplicate top-level key in webhook body: #{key}" if seen.key?(key)
|
|
119
|
+
|
|
120
|
+
seen[key] = true
|
|
121
|
+
i += 1 while i < n && raw_body[i].match?(/\s/)
|
|
122
|
+
raise ArgumentError, 'Malformed webhook JSON' unless raw_body[i] == ':'
|
|
123
|
+
|
|
124
|
+
i += 1
|
|
125
|
+
i += 1 while i < n && raw_body[i].match?(/\s/)
|
|
126
|
+
value_start = i
|
|
127
|
+
i = skip_value(raw_body, i)
|
|
128
|
+
content = raw_body[value_start...i] if key == 'content'
|
|
129
|
+
end
|
|
130
|
+
raise ArgumentError, "Webhook body does not contain 'content' field" if content.nil?
|
|
131
|
+
|
|
132
|
+
content
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# +i+ points at an opening '"'. Returns the index just past the closing '"'.
|
|
136
|
+
def self.skip_string(str, i)
|
|
137
|
+
i += 1
|
|
138
|
+
n = str.length
|
|
139
|
+
while i < n
|
|
140
|
+
c = str[i]
|
|
141
|
+
if c == '\\'
|
|
142
|
+
i += 2
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
return i + 1 if c == '"'
|
|
146
|
+
|
|
147
|
+
i += 1
|
|
148
|
+
end
|
|
149
|
+
raise ArgumentError, 'Unterminated string in webhook JSON'
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# +i+ points at the first char of a JSON value. Returns the index just past it.
|
|
153
|
+
def self.skip_value(str, i)
|
|
154
|
+
return skip_string(str, i) if str[i] == '"'
|
|
155
|
+
|
|
156
|
+
if str[i] == '{' || str[i] == '['
|
|
157
|
+
depth = 0
|
|
158
|
+
n = str.length
|
|
159
|
+
while i < n
|
|
160
|
+
ch = str[i]
|
|
161
|
+
if ch == '"'
|
|
162
|
+
i = skip_string(str, i)
|
|
163
|
+
next
|
|
164
|
+
elsif ch == '{' || ch == '['
|
|
165
|
+
depth += 1
|
|
166
|
+
elsif ch == '}' || ch == ']'
|
|
167
|
+
depth -= 1
|
|
168
|
+
return i + 1 if depth.zero?
|
|
169
|
+
end
|
|
170
|
+
i += 1
|
|
171
|
+
end
|
|
172
|
+
raise ArgumentError, 'Unterminated object/array in webhook JSON'
|
|
173
|
+
end
|
|
174
|
+
i += 1 while i < str.length && !",}] \t\n\r".include?(str[i])
|
|
175
|
+
i
|
|
176
|
+
end
|
|
177
|
+
|
|
184
178
|
# Securely compare two strings of equal length.
|
|
185
179
|
# This method is a port of ActiveSupport::SecurityUtils.secure_compare
|
|
186
180
|
# which works on non-Rails platforms.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jamm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jamm
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -84,7 +84,6 @@ files:
|
|
|
84
84
|
- lib/jamm/api/configuration.rb
|
|
85
85
|
- lib/jamm/api/models/apiv1_error.rb
|
|
86
86
|
- lib/jamm/api/models/apiv1_status.rb
|
|
87
|
-
- lib/jamm/api/models/charge_message_api_source.rb
|
|
88
87
|
- lib/jamm/api/models/customer_service_update_customer_body.rb
|
|
89
88
|
- lib/jamm/api/models/googlerpc_status.rb
|
|
90
89
|
- lib/jamm/api/models/protobuf_any.rb
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
=begin
|
|
2
|
-
#Jamm API
|
|
3
|
-
|
|
4
|
-
#No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
|
5
|
-
|
|
6
|
-
The version of the OpenAPI document: 1.0
|
|
7
|
-
|
|
8
|
-
Generated by: https://openapi-generator.tech
|
|
9
|
-
Generator version: 7.9.0
|
|
10
|
-
|
|
11
|
-
=end
|
|
12
|
-
|
|
13
|
-
require 'date'
|
|
14
|
-
require 'time'
|
|
15
|
-
|
|
16
|
-
module Api
|
|
17
|
-
class ChargeMessageApiSource
|
|
18
|
-
UNSPECIFIED = "API_SOURCE_UNSPECIFIED".freeze
|
|
19
|
-
OFF_SESSION_SYNC = "API_SOURCE_OFF_SESSION_SYNC".freeze
|
|
20
|
-
OFF_SESSION_ASYNC = "API_SOURCE_OFF_SESSION_ASYNC".freeze
|
|
21
|
-
ON_SESSION = "API_SOURCE_ON_SESSION".freeze
|
|
22
|
-
|
|
23
|
-
def self.all_vars
|
|
24
|
-
@all_vars ||= [UNSPECIFIED, OFF_SESSION_SYNC, OFF_SESSION_ASYNC, ON_SESSION].freeze
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Builds the enum from string
|
|
28
|
-
# @param [String] The enum value in the form of the string
|
|
29
|
-
# @return [String] The enum value
|
|
30
|
-
def self.build_from_hash(value)
|
|
31
|
-
new.build_from_hash(value)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Builds the enum from string
|
|
35
|
-
# @param [String] The enum value in the form of the string
|
|
36
|
-
# @return [String] The enum value
|
|
37
|
-
def build_from_hash(value)
|
|
38
|
-
return value if ChargeMessageApiSource.all_vars.include?(value)
|
|
39
|
-
raise "Invalid ENUM value #{value} for class #ChargeMessageApiSource"
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|