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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67443648e1379446bcc69932c2cbf51c849dfbee244d15af946107a31003c4e7
4
- data.tar.gz: 3e1b5d57cb796ed3b22c9e1711f7e0785a3e8c9ebf9ef3cd953055e39ea08847
3
+ metadata.gz: 1342c0449b6ee293db45dd5b397e5bc1ea581b98f57e4dd8b7f3790a05d191da
4
+ data.tar.gz: ca0a24c38e04b189c162e280f2ec33be5f808bdd3c15cfa11a375d490be422cc
5
5
  SHA512:
6
- metadata.gz: 02f9e23da1f446c6efbfabea6ae225424ac388abba1e3ec6c46b1db81c5fa612ac3b2618e016dc25901fceac7bf277a0725443cf8783010b4a79655fe0d11611
7
- data.tar.gz: bb65fb4d0322dce22c8145b81d7eee60ee3661ff3f8535e646a602398fc4a5d32d49f3b7c244c1ee2cf3ac08d7b17545c113bff6260f48141b222cbbfef1ca6c
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.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.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.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.0 (OS: {os-info}, Engine: {engine}/{engine-version})'
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.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-04 00:00:00.000000000 Z
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