univapay-client-sdk 1.0.0 → 1.0.1
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/README.md +3 -3
- data/lib/univapay_client_sdk/apis/base_api.rb +1 -1
- data/lib/univapay_client_sdk/extensions.rb +93 -0
- data/test/test_app_jwt.rb +117 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1342c0449b6ee293db45dd5b397e5bc1ea581b98f57e4dd8b7f3790a05d191da
|
|
4
|
+
data.tar.gz: ca0a24c38e04b189c162e280f2ec33be5f808bdd3c15cfa11a375d490be422cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1cc3a5d944d5c0e40e84b1ae3edc4ca6aee4d8123de4d325f23e1a99295aa093caa06a71057e1784313e8028a0b959d1c89a9ded07b895446fd09b5d78910111
|
|
7
|
+
data.tar.gz: 4446484ea04820ceee34cf5d50f758bf54a320c00ffc75cff2a90d4c1959209fc9d795d8aefd15de4d5f49e894aebfb07a521adb23273b05da10ef3d5680517f
|
data/README.md
CHANGED
|
@@ -36,16 +36,16 @@ We will assume that all requests are going to originate from a backend server th
|
|
|
36
36
|
Install the gem from the command line:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
gem install univapay-client-sdk -v 1.0.
|
|
39
|
+
gem install univapay-client-sdk -v 1.0.1
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Or add the gem to your Gemfile and run `bundle`:
|
|
43
43
|
|
|
44
44
|
```ruby
|
|
45
|
-
gem 'univapay-client-sdk', '1.0.
|
|
45
|
+
gem 'univapay-client-sdk', '1.0.1'
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
For additional gem details, see the [RubyGems page for the univapay-client-sdk gem](https://rubygems.org/gems/univapay-client-sdk/versions/1.0.
|
|
48
|
+
For additional gem details, see the [RubyGems page for the univapay-client-sdk gem](https://rubygems.org/gems/univapay-client-sdk/versions/1.0.1).
|
|
49
49
|
|
|
50
50
|
## IRB Console Usage
|
|
51
51
|
|
|
@@ -10,7 +10,7 @@ module UnivapayClientSdk
|
|
|
10
10
|
attr_accessor :config, :http_call_back
|
|
11
11
|
|
|
12
12
|
def self.user_agent
|
|
13
|
-
'Ruby-SDK/1.0.
|
|
13
|
+
'Ruby-SDK/1.0.1 (OS: {os-info}, Engine: {engine}/{engine-version})'
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def self.user_agent_parameters
|
|
@@ -124,4 +124,97 @@ module UnivapayClientSdk
|
|
|
124
124
|
get_subscription(store_id, id, polling: true)
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
|
+
# ── App token (JWT) claim decoding ────────────────────────────────────────
|
|
128
|
+
#
|
|
129
|
+
# A UnivaPay app token JWT carries the context it was issued for. A
|
|
130
|
+
# store-level token has both `merchant_id` and `store_id`; a merchant-level
|
|
131
|
+
# token has only `merchant_id`.
|
|
132
|
+
#
|
|
133
|
+
# Decoding only reads the payload segment -- it does NOT verify the
|
|
134
|
+
# signature, which is deliberate. The value is the caller's own credential,
|
|
135
|
+
# already trusted by virtue of being configured on the client; nothing here
|
|
136
|
+
# is an authorization decision. Never use these values to authenticate a
|
|
137
|
+
# third party's token.
|
|
138
|
+
module AppJwt
|
|
139
|
+
# Matches the canonical 8-4-4-4-12 hexadecimal UUID form.
|
|
140
|
+
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i.freeze
|
|
141
|
+
|
|
142
|
+
# Decodes the payload segment of a JWT without verifying its signature.
|
|
143
|
+
#
|
|
144
|
+
# @param jwt_token [String, nil] The JWT to decode.
|
|
145
|
+
# @return [Hash, nil] The decoded claims, or nil unless the token is a
|
|
146
|
+
# well-formed three-segment JWT whose payload segment is base64url-encoded
|
|
147
|
+
# JSON describing an object.
|
|
148
|
+
def self.decode_payload(jwt_token)
|
|
149
|
+
return nil if jwt_token.nil? || !jwt_token.is_a?(String) || jwt_token.empty?
|
|
150
|
+
|
|
151
|
+
segments = jwt_token.split('.', -1)
|
|
152
|
+
return nil unless segments.length == 3
|
|
153
|
+
|
|
154
|
+
begin
|
|
155
|
+
# unpack1('m0') is strict base64 and needs no `base64` gem, which stopped
|
|
156
|
+
# being a default gem in Ruby 3.4. It requires correct padding, so
|
|
157
|
+
# translate base64url to base64 and pad first.
|
|
158
|
+
base64 = segments[1].tr('-_', '+/')
|
|
159
|
+
base64 += '=' * ((4 - (base64.length % 4)) % 4)
|
|
160
|
+
payload = JSON.parse(base64.unpack1('m0'))
|
|
161
|
+
rescue ArgumentError, JSON::ParserError
|
|
162
|
+
return nil
|
|
163
|
+
end
|
|
164
|
+
payload.is_a?(Hash) ? payload : nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Reads a claim from a JWT payload and returns it only if it is a UUID.
|
|
168
|
+
#
|
|
169
|
+
# Anything else -- claim absent, nil, not a string, or a string that is not
|
|
170
|
+
# a canonical UUID -- yields nil, so a caller never has to distinguish "not
|
|
171
|
+
# set" from "could not decode".
|
|
172
|
+
#
|
|
173
|
+
# @param jwt_token [String, nil] The JWT to decode.
|
|
174
|
+
# @param claim [String] Name of the claim to read.
|
|
175
|
+
# @return [String, nil] The claim value as a UUID string, or nil.
|
|
176
|
+
def self.read_uuid_claim(jwt_token, claim)
|
|
177
|
+
payload = decode_payload(jwt_token)
|
|
178
|
+
return nil if payload.nil?
|
|
179
|
+
|
|
180
|
+
value = payload[claim]
|
|
181
|
+
value.is_a?(String) && UUID_PATTERN.match?(value) ? value : nil
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Reopened onto the generated client class to expose the app token's context.
|
|
186
|
+
class Client
|
|
187
|
+
# The merchant this client's app token was issued for, decoded from the
|
|
188
|
+
# configured JWT.
|
|
189
|
+
#
|
|
190
|
+
# Both merchant-level and store-level app tokens carry a merchant, so this
|
|
191
|
+
# is set for either kind of token.
|
|
192
|
+
#
|
|
193
|
+
# @return [String, nil] The merchant id as a UUID string, or nil if no JWT
|
|
194
|
+
# is configured or its `merchant_id` claim is absent or not a UUID.
|
|
195
|
+
def get_current_merchant_id
|
|
196
|
+
AppJwt.read_uuid_claim(jwt_token_or_nil, 'merchant_id')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# The store this client's app token was issued for, decoded from the
|
|
200
|
+
# configured JWT.
|
|
201
|
+
#
|
|
202
|
+
# Only store-level app tokens are scoped to a store. A merchant-level token
|
|
203
|
+
# carries no `store_id` claim, so this returns nil for one -- use `stores`
|
|
204
|
+
# to list the merchant's stores instead.
|
|
205
|
+
#
|
|
206
|
+
# @return [String, nil] The store id as a UUID string, or nil if no JWT is
|
|
207
|
+
# configured or its `store_id` claim is absent or not a UUID.
|
|
208
|
+
def get_current_store_id
|
|
209
|
+
AppJwt.read_uuid_claim(jwt_token_or_nil, 'store_id')
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
# The configured JWT, or nil when no credentials are set.
|
|
215
|
+
def jwt_token_or_nil
|
|
216
|
+
config.bearer_auth_credentials&.jwt_token
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
127
220
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Custom test (not auto-generated): pins the App Token claim-decoding contract
|
|
2
|
+
# behind get_current_merchant_id / get_current_store_id.
|
|
3
|
+
#
|
|
4
|
+
# This contract is implemented seven times -- once per SDK -- and has already
|
|
5
|
+
# drifted twice: the Python SDK accepted non-canonical UUIDs that the others
|
|
6
|
+
# rejected, and the TypeScript SDK rejected a payload segment carrying '='
|
|
7
|
+
# padding that the others accepted. Neither was caught by a test, because none
|
|
8
|
+
# existed.
|
|
9
|
+
#
|
|
10
|
+
# So the cases below are deliberately a *shared table*: keep them identical in
|
|
11
|
+
# all seven SDKs. The failure being guarded against is the languages disagreeing
|
|
12
|
+
# with each other, which no single-language suite can see.
|
|
13
|
+
#
|
|
14
|
+
# Everything here is synthetic and offline -- no network, no environment, no real
|
|
15
|
+
# credential. It must pass in CI, where no token is configured.
|
|
16
|
+
|
|
17
|
+
require 'json'
|
|
18
|
+
require 'minitest/autorun'
|
|
19
|
+
require 'univapay_client_sdk'
|
|
20
|
+
|
|
21
|
+
class AppJwtTest < Minitest::Test
|
|
22
|
+
include UnivapayClientSdk
|
|
23
|
+
|
|
24
|
+
MERCHANT_ID = '11ec8e24-0ecf-2c5a-923c-331b915dc311'.freeze
|
|
25
|
+
STORE_ID = '11ec8e24-b133-6c68-b54d-971717202e9b'.freeze
|
|
26
|
+
|
|
27
|
+
# pack('m0') is strict base64 and needs no `base64` gem, which stopped being a
|
|
28
|
+
# default gem in Ruby 3.4 -- the same reason the SDK itself avoids it.
|
|
29
|
+
def base64url(bytes, padded: false)
|
|
30
|
+
encoded = [bytes].pack('m0').tr('+/', '-_')
|
|
31
|
+
padded ? encoded : encoded.delete('=')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Builds a JWT carrying `claims`. Header and signature are inert.
|
|
35
|
+
def jwt(claims, padded: false)
|
|
36
|
+
header = base64url('{"alg":"HS256","typ":"JWT"}')
|
|
37
|
+
"#{header}.#{base64url(JSON.generate(claims), padded: padded)}.c2ln"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Builds a JWT whose payload segment is `payload`, base64url-encoded.
|
|
41
|
+
def raw_jwt(payload)
|
|
42
|
+
"aGRy.#{base64url(payload)}.c2ln"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def client_with(jwt_token)
|
|
46
|
+
Client.new(
|
|
47
|
+
bearer_auth_credentials: BearerAuthCredentials.new(
|
|
48
|
+
secret_key: 'not-a-real-secret', jwt_token: jwt_token
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_reads_both_ids_from_store_level_token
|
|
54
|
+
client = client_with(jwt({ 'merchant_id' => MERCHANT_ID, 'store_id' => STORE_ID }))
|
|
55
|
+
|
|
56
|
+
assert_equal MERCHANT_ID, client.get_current_merchant_id
|
|
57
|
+
assert_equal STORE_ID, client.get_current_store_id
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_reads_merchant_from_merchant_level_token_and_reports_no_store
|
|
61
|
+
# A merchant-level token carries no store_id claim at all. nil here is the
|
|
62
|
+
# correct answer, not a decoding failure.
|
|
63
|
+
client = client_with(jwt({ 'merchant_id' => MERCHANT_ID }))
|
|
64
|
+
|
|
65
|
+
assert_equal MERCHANT_ID, client.get_current_merchant_id
|
|
66
|
+
assert_nil client.get_current_store_id
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_accepts_payload_segment_that_carries_padding
|
|
70
|
+
# The TypeScript SDK once rejected exactly this, making it the only one of
|
|
71
|
+
# the seven to return nil for a padded -- but still valid -- token.
|
|
72
|
+
client = client_with(jwt({ 'merchant_id' => MERCHANT_ID, 'store_id' => STORE_ID }, padded: true))
|
|
73
|
+
|
|
74
|
+
assert_equal MERCHANT_ID, client.get_current_merchant_id
|
|
75
|
+
assert_equal STORE_ID, client.get_current_store_id
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_returns_nil_never_raises_for_unusable_input
|
|
79
|
+
cases = [
|
|
80
|
+
['a claim that is JSON null', jwt({ 'store_id' => nil })],
|
|
81
|
+
['a claim that is not a string', jwt({ 'store_id' => 42 })],
|
|
82
|
+
['an undashed 32-character UUID', jwt({ 'store_id' => STORE_ID.delete('-') })],
|
|
83
|
+
['a braced UUID', jwt({ 'store_id' => "{#{STORE_ID}}" })],
|
|
84
|
+
['a urn:uuid: prefixed UUID', jwt({ 'store_id' => "urn:uuid:#{STORE_ID}" })],
|
|
85
|
+
['short hex groups (1-1-1-1-1)', jwt({ 'store_id' => '1-1-1-1-1' })],
|
|
86
|
+
['a UUID with a trailing newline', jwt({ 'store_id' => "#{STORE_ID}\n" })],
|
|
87
|
+
['a UUID padded with spaces', jwt({ 'store_id' => " #{STORE_ID} " })],
|
|
88
|
+
['a two-segment token', 'aGRy.c2ln'],
|
|
89
|
+
['a payload that is not base64url', 'aGRy.!!!!.c2ln'],
|
|
90
|
+
['a payload that is a JSON array', raw_jwt('[1,2]')],
|
|
91
|
+
['a payload that is not JSON', raw_jwt('definitely not json')],
|
|
92
|
+
['an empty string', ''],
|
|
93
|
+
# The Authorization header value is {secret}.{jwt} -- four segments once
|
|
94
|
+
# split. Pasting that whole value into the jwt_token field is the mistake
|
|
95
|
+
# the guide warns about, and it must degrade to nil, not to a wrong id.
|
|
96
|
+
['the combined {secret}.{jwt} header value', "c2VjcmV0.#{jwt({ 'store_id' => STORE_ID })}"]
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
failures = cases.filter_map do |label, token|
|
|
100
|
+
begin
|
|
101
|
+
store_id = client_with(token).get_current_store_id
|
|
102
|
+
"#{label} -> expected nil but got #{store_id.inspect}" unless store_id.nil?
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
"#{label} -> raised #{e.class}: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
assert_empty failures, "cases that did not degrade to nil:\n #{failures.join("\n ")}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_returns_nil_when_no_credentials_configured
|
|
112
|
+
client = Client.new
|
|
113
|
+
|
|
114
|
+
assert_nil client.get_current_merchant_id
|
|
115
|
+
assert_nil client.get_current_store_id
|
|
116
|
+
end
|
|
117
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: univapay-client-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Univapay Developers
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: apimatic_core_interfaces
|
|
@@ -340,6 +340,7 @@ files:
|
|
|
340
340
|
- test/framework_integrations/webhooks/test/test_rails_charge_app.rb
|
|
341
341
|
- test/framework_integrations/webhooks/test/test_sinatra_charge_app.rb
|
|
342
342
|
- test/http_response_catcher.rb
|
|
343
|
+
- test/test_app_jwt.rb
|
|
343
344
|
homepage: https://univapay.com
|
|
344
345
|
licenses:
|
|
345
346
|
- MIT
|