usesmileid 12.0.0 → 13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b3aa92d6b398bde9e998a5e5b128c688a94ac72331b7f5be584df7008286458
4
- data.tar.gz: f44668a9c23b92c59708176ba20153ef729b3f840431a3fd2fd32fab8121efa4
3
+ metadata.gz: f64d6fd177105edbffe5bceac374c675140d49a0da20d8e56041ae156429dda3
4
+ data.tar.gz: 810132bb74281566a6957f2ae76dd647d0e685f1423a71db26780dbf9351076f
5
5
  SHA512:
6
- metadata.gz: 2f3017edcda29325e2b49d5a6824d20f6c649538044e840f2a6b08aea08d44d65fa9e8e07c821544519181726d9a3fa3853ee796c6216808c18169f094d65a5e
7
- data.tar.gz: 146f765cb0b59e9aa473718c614ae3287fd94e064e51bca8f546755c09e1b3f4f60c22816c538041aeb01750c1728da2b9b3b3c65d0238edbe74a3a88106b289
6
+ metadata.gz: 856a7d6b04e054b1084d21dd35822274c2f0fcced663944ad01b6c6446df160e1f58237a5686c9f0824bd64e6cdf9de32b9b131fd1db68d4f34287998a9da0f4
7
+ data.tar.gz: 306285d436a657d8bbafe633b101cebb9f5e6e955b2fe60029d6ff08e3cc0c5665fb58edfa78037323f7f23563897012e4fff02fd7b2d6810cb01c5b2f6bb427
data/CHANGELOG.md CHANGED
@@ -7,41 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [13.0.0] - 2026-11-09
11
+
12
+ Version 12.2.0 was never published. Its changes are in this release, which is a major version because it breaks the 12.1.0 API.
13
+
14
+ ### Breaking changes
15
+
16
+ - `biometric.authenticate` no longer takes `use_enrolled_image:`. Use `biometric_kyc.verify(use_enrolled_image: true, user_id: ...)` instead.
17
+ - `biometric.authenticate` now requires `selfie_image:` and `liveness_images:`. Pass both on every call.
18
+ - `SmileID::TokenManager` has been removed. The client now mints a token for each request, so you do not need to manage tokens.
19
+
10
20
  ### Changed
11
21
 
12
- - Renamed the gem from `smile-identity-core` to `smileid`. The require path
13
- (`require "smileid"`) and the `SmileID` module are unchanged.
14
- - Set the version to 12.0.0, aligning the server SDKs with the V12 mobile
15
- SDKs.
16
- - `base_url` must now be an absolute https URL with no query or fragment,
17
- validated at construction. There is no allow-insecure option.
18
- - `default_callback_url` (at construction) and per-request `callback_url`
19
- values (entry operations and replay) must be https; insecure values raise
20
- a validation error before any request is sent.
21
- - `job_id` and `user_id` path parameters are URL-encoded as single path
22
- segments before interpolation.
23
- - Multipart filenames and content types are sanitized against header
24
- injection on every input path, including caller-supplied content types.
25
- Filenames have control characters stripped and quotes encoded; content
26
- types must be a valid media type.
22
+ - Each request mints its own token, bound to that request's `user_id` and product, instead of reusing one cached token. A 401 is no longer retried with a refreshed token.
23
+ - A `user_id` that is empty or padded with whitespace raises `ValidationError` before any request is sent.
24
+ - `biometric.authenticate` takes `selfie_image:` and `liveness_images:` as required keywords.
27
25
 
28
26
  ### Added
29
27
 
30
- - `SmileID::Errors::UnexpectedResponseError`, raised when a 2xx success
31
- response body is not the expected JSON object.
28
+ - `biometric_kyc.verify(use_enrolled_image: true, user_id: ...)` verifies against the user's enrolled selfie; the grant is bound into the job's token and the selfie and liveness images may be omitted.
29
+
30
+ ### Removed
31
+
32
+ - `use_enrolled_image` from `biometric.authenticate`; selfie and liveness images are now always required.
33
+
34
+ ## [12.1.0] - 2026-10-01
32
35
 
33
36
  ### Added
34
37
 
35
- - Full V3 API coverage: enhanced KYC, document verification, enhanced document
36
- verification, biometric KYC, biometric enrollment, authentication and compare,
37
- verification status with a `wait_until_complete` polling helper, callback
38
- replay, fraud reporting (with `flag_fraud` / `clear_fraud` wrappers), and the
39
- four services endpoints.
40
- - Internal JWT authentication with a thread-safe cache and a single automatic
41
- refresh on 401.
42
- - Typed error hierarchy under `SmileID::Errors`, covering both API error body
43
- shapes.
44
- - Automatic retries with exponential backoff and `Retry-After` support for
45
- idempotent calls only.
46
- - Consent builder and client-side validation for user details and fraud
47
- reports.
38
+ - Residency Document Verification: `documents.verify_residency`.
39
+
40
+ ## [12.0.0] - 2026-08-20
41
+
42
+ First public release.
43
+
44
+ ### Products
45
+
46
+ - Enhanced KYC, Biometric KYC, Document Verification and Enhanced Document
47
+ Verification.
48
+ - SmartSelfie enrollment, authentication and compare.
49
+
50
+ ### Verifications
51
+
52
+ - Job status retrieval, plus a `wait_until_complete` helper that polls until
53
+ a job reaches a terminal decision.
54
+ - Callback replay.
55
+ - User fraud reporting, with `flag_fraud` / `clear_fraud` convenience
56
+ wrappers.
57
+
58
+ ### Client
59
+
60
+ - Sandbox and production environments, with a `base_url` override for other
61
+ hosts. `base_url` and callback URLs must be https, checked before any
62
+ request is sent.
63
+ - Internal JWT authentication, cached and refreshed automatically on 401.
64
+ - Automatic retries with backoff for idempotent calls.
65
+ - Typed errors under `SmileID::Errors`, keyed on HTTP status.
66
+ - Bank codes, supported ID types and supported documents lookups.
67
+
68
+ [Unreleased]: https://github.com/smileidentity/smileid-sdk-ruby/compare/v13.0.0...HEAD
69
+ [13.0.0]: https://github.com/smileidentity/smileid-sdk-ruby/compare/v12.1.0...v13.0.0
70
+ [12.1.0]: https://github.com/smileidentity/smileid-sdk-ruby/compare/v12.0.0...v12.1.0
71
+ [12.0.0]: https://github.com/smileidentity/smileid-sdk-ruby/releases/tag/v12.0.0
data/README.md CHANGED
@@ -40,7 +40,7 @@ smile = SmileID::Client.new(
40
40
 
41
41
  Partner ids are displayed zero-padded (for example 002) but must be passed without leading zeros (2).
42
42
 
43
- Authentication is internal: the SDK fetches a short-lived token from the API, caches it until just before expiry, and refreshes it once automatically if a request returns 401. You never see or pass a token.
43
+ Authentication is internal: before each request the SDK fetches a short-lived token from the API. A job's token is bound to that job's product and, when the job names one, its user; other calls get a token with neither. A 401 raises `AuthenticationError` without a retry. You never see or pass a token.
44
44
 
45
45
  ### Environment selection
46
46
 
@@ -142,6 +142,26 @@ accepted = smile.documents.verify_enhanced(
142
142
  )
143
143
  ```
144
144
 
145
+ ### Residency document verification
146
+
147
+ Same shape as enhanced document verification, plus the `visa` endorsed in the
148
+ passport (JPEG or PNG). `id_type` is optional and defaults to `PASSPORT`, the
149
+ only value accepted. The visa details arrive in
150
+ `additional_documents` on the callback.
151
+
152
+ ```ruby
153
+ accepted = smile.documents.verify_residency(
154
+ selfie_image: "selfie.jpg",
155
+ liveness_images: ["live1.jpg", "live2.jpg", "live3.jpg",
156
+ "live4.jpg", "live5.jpg", "live6.jpg"],
157
+ document: "passport.jpg",
158
+ visa: "visa.jpg",
159
+ country: "ZA",
160
+ user_details: user_details,
161
+ consent: consent
162
+ )
163
+ ```
164
+
145
165
  ### Biometric KYC
146
166
 
147
167
  ```ruby
@@ -157,6 +177,20 @@ accepted = smile.biometric_kyc.verify(
157
177
  )
158
178
  ```
159
179
 
180
+ To verify against a user's enrolled selfie instead of a fresh capture, set `use_enrolled_image: true` and pass the enrolled `user_id`; the selfie and liveness images may then be omitted.
181
+
182
+ ```ruby
183
+ accepted = smile.biometric_kyc.verify(
184
+ use_enrolled_image: true,
185
+ user_id: "user-42",
186
+ country: "NG",
187
+ id_type: "NIN",
188
+ id_number: "12345678901",
189
+ user_details: user_details,
190
+ consent: consent
191
+ )
192
+ ```
193
+
160
194
  ### Biometric enrollment
161
195
 
162
196
  ```ruby
@@ -172,7 +206,7 @@ accepted = smile.biometric.enroll(
172
206
 
173
207
  ### Biometric authentication
174
208
 
175
- `user_id` is required and must match an enrolled user. Images are required unless you set `use_enrolled_image: true`.
209
+ `user_id` is required and must match an enrolled user. The selfie and liveness images are always required.
176
210
 
177
211
  ```ruby
178
212
  accepted = smile.biometric.authenticate(
@@ -276,7 +310,7 @@ Every API failure raises a typed error under `SmileID::Errors`, keyed on the HTT
276
310
  | Error | Raised on |
277
311
  |---|---|
278
312
  | `InvalidRequestError` | 400, 415, and failed client-side validation (`ValidationError` subclass) |
279
- | `AuthenticationError` | 401 after one automatic token refresh |
313
+ | `AuthenticationError` | 401 |
280
314
  | `PaymentRequiredError` | 402 — insufficient wallet balance |
281
315
  | `PermissionError` | 403 |
282
316
  | `NotFoundError` | 404 (except `verifications.retrieve`, which returns a `not_found` status) |
@@ -7,6 +7,19 @@ module SmileID
7
7
  # The Smile ID client (spec section 2.1, 4). Construct once, then call
8
8
  # resource verbs, e.g. client.enhanced_kyc.verify(...).
9
9
  class Client
10
+ # Mirrors the API authorizer: a token bound to a product is accepted only on
11
+ # that product's endpoint. Paths absent here mint a token with no claims.
12
+ PRODUCT_BY_PATH = {
13
+ '/v3/enhanced_kyc' => 'enhanced_kyc',
14
+ '/v3/document_verification' => 'document_verification',
15
+ '/v3/enhanced_document_verification' => 'enhanced_document_verification',
16
+ '/v3/residency_document_verification' => 'residency_document_verification',
17
+ '/v3/biometric_kyc' => 'biometric_kyc',
18
+ '/v3/registration' => 'smart_selfie_registration',
19
+ '/v3/authentication' => 'smart_selfie_authentication',
20
+ '/v3/compare' => 'smart_selfie_compare'
21
+ }.freeze
22
+
10
23
  attr_reader :config
11
24
 
12
25
  def initialize(partner_id:, api_key:, environment: :sandbox,
@@ -18,7 +31,6 @@ module SmileID
18
31
  base_url: base_url, timeout: timeout, max_retries: max_retries, http_client: http_client
19
32
  )
20
33
  @transport = Transport.new(@config)
21
- @token_manager = TokenManager.new(@config, @transport)
22
34
  end
23
35
 
24
36
  def enhanced_kyc
@@ -49,32 +61,25 @@ module SmileID
49
61
  @services ||= Resources::Services.new(self)
50
62
  end
51
63
 
52
- # Execute an operation end to end: build the request, attach auth and
53
- # telemetry, sign if configured, send it, refresh-on-401 once, and either
54
- # return the response or raise a typed error (spec section 2A, 7).
64
+ # Execute an operation end to end: mint its token, build the request, attach
65
+ # auth and telemetry, send it, and either return the response or raise a
66
+ # typed error (spec section 2A, 7). Every authenticated call mints its own
67
+ # token; retries of an idempotent call reuse it.
55
68
  #
56
69
  # @return [SmileID::Response] on a success status for the operation.
57
- def call(op_key, form: nil, path_params: {}, query: {}, user_id_header: nil, timeout: nil)
70
+ def call(op_key, form: nil, path_params: {}, query: {}, user_id_header: nil, timeout: nil,
71
+ use_enrolled_image: false)
58
72
  op = Generated::Operations.fetch(op_key)
59
73
  url = @config.base_url + interpolate(op.path, path_params)
60
74
  content_type, body = serialize(op, form)
61
- refreshed = false
62
-
63
- loop do
64
- headers = build_headers(op, content_type, user_id_header)
65
- response = @transport.send_request(
66
- method: op.http_method, url: url, headers: headers, query: query || {},
67
- body: body, retryable: op.idempotent, timeout: timeout
68
- )
75
+ token = mint_token(op, form, user_id_header, use_enrolled_image, timeout) if op.authenticated
69
76
 
70
- if response.status == 401 && op.authenticated && !refreshed
71
- @token_manager.force_refresh
72
- refreshed = true
73
- next
74
- end
75
-
76
- return handle(op, response)
77
- end
77
+ headers = build_headers(op, content_type, user_id_header, token)
78
+ response = @transport.send_request(
79
+ method: op.http_method, url: url, headers: headers, query: query || {},
80
+ body: body, retryable: op.idempotent, timeout: timeout
81
+ )
82
+ handle(op, response)
78
83
  end
79
84
 
80
85
  private
@@ -88,15 +93,86 @@ module SmileID
88
93
  Helpers::Multipart.build(form)
89
94
  end
90
95
 
91
- def build_headers(op, content_type, user_id_header)
96
+ def build_headers(op, content_type, user_id_header, token)
92
97
  headers = Telemetry.headers
93
98
  headers['Content-Type'] = content_type if content_type
94
- headers['SmileID-Token'] = @token_manager.token if op.authenticated
99
+ headers['SmileID-Token'] = token if token
95
100
  headers['SmileID-Partner-ID'] = @config.partner_id if op.partner_id_header
96
101
  headers['User-ID'] = user_id_header if user_id_header
97
102
  headers
98
103
  end
99
104
 
105
+ # The token is bound to this one request's user and product. A grant
106
+ # (use_enrolled_image) authorises a captureless job for one user, so it must
107
+ # never reach another call's token.
108
+ def mint_token(op, form, user_id_header, use_enrolled_image, timeout)
109
+ product = PRODUCT_BY_PATH[op.path]
110
+ user_id = job_user_id(form, user_id_header) if product
111
+ if use_enrolled_image && user_id.nil?
112
+ raise Errors::ValidationError.new('user_id is required when use_enrolled_image is true')
113
+ end
114
+
115
+ claims = { 'user_id' => user_id, 'product' => product,
116
+ 'use_enrolled_image' => (true if use_enrolled_image) }.compact
117
+ request_token(claims, timeout)
118
+ end
119
+
120
+ def request_token(claims, timeout)
121
+ content_type, body = Helpers::Multipart.build(claims) unless claims.empty?
122
+ response = @transport.send_request(
123
+ method: :post,
124
+ url: "#{@config.base_url}/v3/token",
125
+ headers: token_headers(content_type),
126
+ body: body,
127
+ retryable: true,
128
+ timeout: timeout
129
+ )
130
+
131
+ unless response.status == 200
132
+ raise Errors.from_response(
133
+ status_code: response.status,
134
+ body: response.json,
135
+ raw_body: response.raw_body,
136
+ headers: response.headers
137
+ )
138
+ end
139
+
140
+ jwt = response.json && response.json['token']
141
+ if jwt.nil? || jwt.to_s.empty?
142
+ raise Errors::UnexpectedResponseError.new(
143
+ 'token endpoint returned no token',
144
+ status_code: response.status,
145
+ request_id: Errors.request_id_from(response.headers),
146
+ raw_body: response.raw_body
147
+ )
148
+ end
149
+
150
+ jwt
151
+ end
152
+
153
+ # The API trims the User-ID header but not the token's user_id claim, so a
154
+ # padded id would mint a token for a different user than the job names.
155
+ def job_user_id(form, user_id_header)
156
+ user_id = user_id_header.nil? ? form && form['user_id'] : user_id_header
157
+ return nil if user_id.nil?
158
+
159
+ text = user_id.to_s
160
+ if text.empty? || text != text.strip
161
+ raise Errors::ValidationError.new('user_id must not be empty or padded with whitespace')
162
+ end
163
+
164
+ user_id
165
+ end
166
+
167
+ def token_headers(content_type)
168
+ headers = Telemetry.headers.merge(
169
+ 'smileid-partner-id' => @config.partner_id,
170
+ 'smileid-api-key' => @config.api_key
171
+ )
172
+ headers['Content-Type'] = content_type if content_type
173
+ headers
174
+ end
175
+
100
176
  def handle(op, response)
101
177
  if op.success_statuses.include?(response.status)
102
178
  return response if response.json.is_a?(Hash)
@@ -29,6 +29,13 @@ module SmileID
29
29
  callback_url || @client.config.default_callback_url
30
30
  end
31
31
 
32
+ def require_images!(selfie_image, liveness_images, condition = nil)
33
+ return unless selfie_image.nil? || liveness_images.nil? || Array(liveness_images).empty?
34
+
35
+ message = ['selfie_image and liveness_images are required', condition].compact.join(' ')
36
+ raise Errors::ValidationError.new(message)
37
+ end
38
+
32
39
  def accepted(response)
33
40
  Generated::Models::AcceptedResponse.from(response.json)
34
41
  end
@@ -55,7 +62,8 @@ module SmileID
55
62
  end
56
63
  end
57
64
 
58
- # POST /v3/document_verification and /v3/enhanced_document_verification
65
+ # POST /v3/document_verification, /v3/enhanced_document_verification and
66
+ # /v3/residency_document_verification
59
67
  class Documents < Base
60
68
  def verify(selfie_image:, liveness_images:, document:, consent:, country:, user_details:,
61
69
  document_back: nil, id_type: nil, callback_url: nil,
@@ -87,6 +95,29 @@ module SmileID
87
95
  user_id_header: user_id, timeout: timeout))
88
96
  end
89
97
 
98
+ # POST /v3/residency_document_verification. The document is a passport
99
+ # and the visa endorsed in it is read as well, so id_type defaults to, and must be, PASSPORT.
100
+ def verify_residency(selfie_image:, liveness_images:, document:, visa:, consent:,
101
+ country:, user_details:, id_type: 'PASSPORT', document_back: nil,
102
+ callback_url: nil, partner_params: nil, metadata: nil, user_id: nil, timeout: nil)
103
+ id_type = 'PASSPORT' if id_type.to_s.empty?
104
+ unless id_type.to_s == 'PASSPORT'
105
+ raise Errors::ValidationError.new('id_type must be PASSPORT for residency document verification')
106
+ end
107
+ if visa.nil? || (visa.respond_to?(:empty?) && visa.empty?)
108
+ raise Errors::ValidationError.new('visa is required for residency document verification')
109
+ end
110
+
111
+ form = document_form(
112
+ selfie_image: selfie_image, liveness_images: liveness_images, document: document,
113
+ document_back: document_back, consent: consent, country: country, id_type: id_type,
114
+ user_details: user_details, callback_url: callback_url,
115
+ partner_params: partner_params, metadata: metadata
116
+ ).merge('visa' => visa)
117
+ accepted(@client.call(:residency_document_verification, form: form,
118
+ user_id_header: user_id, timeout: timeout))
119
+ end
120
+
90
121
  private
91
122
 
92
123
  def document_form(selfie_image:, liveness_images:, document:, document_back:, consent:,
@@ -109,9 +140,24 @@ module SmileID
109
140
 
110
141
  # POST /v3/biometric_kyc
111
142
  class BiometricKyc < Base
112
- def verify(selfie_image:, liveness_images:, consent:, country:, id_type:, id_number:,
113
- user_details:, callback_url: nil, sandbox_result: nil,
143
+ # With use_enrolled_image: true the job verifies against user_id's enrolled
144
+ # selfie, so the images may be omitted. The grant is bound into this job's
145
+ # token, never sent in the body.
146
+ def verify(consent:, country:, id_type:, id_number:, user_details:,
147
+ selfie_image: nil, liveness_images: nil, use_enrolled_image: false,
148
+ callback_url: nil, sandbox_result: nil,
114
149
  partner_params: nil, metadata: nil, user_id: nil, timeout: nil)
150
+ unless [true, false].include?(use_enrolled_image)
151
+ raise Errors::ValidationError.new('use_enrolled_image must be true or false')
152
+ end
153
+
154
+ if use_enrolled_image
155
+ if user_id.to_s.strip.empty?
156
+ raise Errors::ValidationError.new('user_id is required when use_enrolled_image is true')
157
+ end
158
+ else
159
+ require_images!(selfie_image, liveness_images, 'unless use_enrolled_image is true')
160
+ end
115
161
  form = {
116
162
  'country' => country,
117
163
  'id_type' => id_type,
@@ -125,7 +171,8 @@ module SmileID
125
171
  'partner_params' => partner_params,
126
172
  'metadata' => metadata
127
173
  }.compact
128
- accepted(@client.call(:biometric_kyc, form: form, user_id_header: user_id, timeout: timeout))
174
+ accepted(@client.call(:biometric_kyc, form: form, user_id_header: user_id, timeout: timeout,
175
+ use_enrolled_image: use_enrolled_image))
129
176
  end
130
177
  end
131
178
 
@@ -149,17 +196,16 @@ module SmileID
149
196
  end
150
197
 
151
198
  # user_id goes in the BODY here (required), not the User-ID header.
152
- def authenticate(user_id:, consent:, user_details:, selfie_image: nil, liveness_images: nil,
153
- use_enrolled_image: nil, callback_url: nil, sandbox_result: nil,
199
+ def authenticate(user_id:, consent:, user_details:, selfie_image:, liveness_images:,
200
+ callback_url: nil, sandbox_result: nil,
154
201
  partner_params: nil, metadata: nil, timeout: nil)
155
- require_images!(use_enrolled_image, selfie_image, liveness_images)
202
+ require_images!(selfie_image, liveness_images)
156
203
  form = {
157
204
  'user_id' => user_id,
158
205
  'selfie_image' => selfie_image,
159
206
  'liveness_images' => liveness_images,
160
207
  'user_details' => coerce_user_details(user_details),
161
208
  'consent' => coerce_consent(consent),
162
- 'use_enrolled_image' => use_enrolled_image,
163
209
  'callback_url' => resolve_callback(callback_url),
164
210
  'sandbox_result' => sandbox_result,
165
211
  'partner_params' => partner_params,
@@ -188,17 +234,6 @@ module SmileID
188
234
  }.compact
189
235
  accepted(@client.call(:compare, form: form, timeout: timeout))
190
236
  end
191
-
192
- private
193
-
194
- def require_images!(use_enrolled_image, selfie_image, liveness_images)
195
- return if use_enrolled_image == true
196
- return unless selfie_image.nil? || liveness_images.nil? || Array(liveness_images).empty?
197
-
198
- raise Errors::ValidationError.new(
199
- 'selfie_image and liveness_images are required unless use_enrolled_image is true'
200
- )
201
- end
202
237
  end
203
238
 
204
239
  # GET /v3/status, POST /v3/replay, plus the wait_until_complete poll helper.
@@ -36,7 +36,7 @@ module SmileID
36
36
  # The single HTTP transport (spec section 2.2, 2.6). Builds the Faraday
37
37
  # request, sends it, retries idempotent operations on transient failures
38
38
  # honouring Retry-After, and normalizes the response. It does not touch auth;
39
- # the client injects tokens and handles the refresh-on-401 dance.
39
+ # the client mints and injects tokens.
40
40
  class Transport
41
41
  RETRY_STATUSES = [408, 429, 500, 502, 503, 504].freeze
42
42
  BACKOFF_BASE = 0.5
@@ -39,7 +39,7 @@ module SmileID
39
39
  # Raised before send when a local validation rule fails (spec section 5.1, 6.11).
40
40
  class ValidationError < InvalidRequestError; end
41
41
 
42
- # 401 — token missing, invalid, or expired after a single refresh attempt.
42
+ # 401 — token missing, invalid or expired.
43
43
  class AuthenticationError < SmileIDError; end
44
44
 
45
45
  # 402 — insufficient wallet balance.
@@ -33,6 +33,11 @@ module SmileID
33
33
  partner_id_header: true, body_kind: :multipart, idempotent: false,
34
34
  success_statuses: [202]
35
35
  ),
36
+ residency_document_verification: Operation.new(
37
+ http_method: :post, path: '/v3/residency_document_verification', authenticated: true,
38
+ partner_id_header: true, body_kind: :multipart, idempotent: false,
39
+ success_statuses: [202]
40
+ ),
36
41
  biometric_kyc: Operation.new(
37
42
  http_method: :post, path: '/v3/biometric_kyc', authenticated: true,
38
43
  partner_id_header: true, body_kind: :multipart, idempotent: false,
@@ -21,7 +21,7 @@ module SmileID
21
21
  # Fields serialized as JSON parts.
22
22
  JSON_PART_FIELDS = %w[consent user_details partner_params metadata].freeze
23
23
  # Single binary fields.
24
- BINARY_FIELDS = %w[selfie_image document document_back comparison_image].freeze
24
+ BINARY_FIELDS = %w[selfie_image document document_back visa comparison_image].freeze
25
25
  # Repeated binary fields.
26
26
  BINARY_ARRAY_FIELDS = %w[liveness_images].freeze
27
27
 
@@ -29,13 +29,14 @@ module SmileID
29
29
  'selfie_image' => 'image/jpeg',
30
30
  'document' => 'image/jpeg',
31
31
  'document_back' => 'image/jpeg',
32
+ 'visa' => 'image/jpeg',
32
33
  'comparison_image' => 'image/jpeg',
33
34
  'liveness_images' => 'image/jpeg'
34
35
  }.freeze
35
36
 
36
- # Only document and document_back may be PNG (spec section 5.3 rule 3);
37
+ # Only document, document_back and visa may be PNG (spec section 5.3 rule 3);
37
38
  # selfie, liveness and comparison images are always image/jpeg.
38
- PNG_CAPABLE_FIELDS = %w[document document_back].freeze
39
+ PNG_CAPABLE_FIELDS = %w[document document_back visa].freeze
39
40
  PNG_MAGIC = "\x89PNG".b.freeze
40
41
 
41
42
  # RFC 6838 type/subtype tokens — anything else is rejected before send.
@@ -45,6 +46,7 @@ module SmileID
45
46
  'selfie_image' => 'selfie.jpg',
46
47
  'document' => 'document.jpg',
47
48
  'document_back' => 'document_back.jpg',
49
+ 'visa' => 'visa.jpg',
48
50
  'comparison_image' => 'comparison.jpg'
49
51
  }.freeze
50
52
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmileID
4
- VERSION = '12.0.0'
4
+ VERSION = '13.0.0'
5
5
  end
data/lib/usesmileid.rb CHANGED
@@ -9,7 +9,6 @@ require 'usesmileid/helpers/user_details'
9
9
  require 'usesmileid/helpers/multipart'
10
10
  require 'usesmileid/client/config'
11
11
  require 'usesmileid/client/transport'
12
- require 'usesmileid/client/auth'
13
12
  require 'usesmileid/client/resources'
14
13
  require 'usesmileid/client/client'
15
14
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: usesmileid
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.0.0
4
+ version: 13.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Smile Identity
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-20 00:00:00.000000000 Z
11
+ date: 2026-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -49,7 +49,6 @@ files:
49
49
  - LICENSE
50
50
  - README.md
51
51
  - lib/usesmileid.rb
52
- - lib/usesmileid/client/auth.rb
53
52
  - lib/usesmileid/client/client.rb
54
53
  - lib/usesmileid/client/config.rb
55
54
  - lib/usesmileid/client/resources.rb
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'base64'
4
- require 'json'
5
-
6
- module SmileID
7
- # Internal JWT lifecycle (spec section 2.3, 2A). Partners never see the token.
8
- #
9
- # The cache is thread-safe: concurrent calls do not stampede the token
10
- # endpoint. The token is cached until its JWT `exp` claim minus a 60s skew; a
11
- # token whose expiry cannot be decoded is treated as single-use (refreshed on
12
- # the next call, and always on a 401).
13
- class TokenManager
14
- EXPIRY_SKEW = 60
15
-
16
- def initialize(config, transport)
17
- @config = config
18
- @transport = transport
19
- @mutex = Mutex.new
20
- @cached = nil
21
- end
22
-
23
- # Return a valid token, fetching one if the cache is empty or expired.
24
- def token
25
- @mutex.synchronize do
26
- return @cached[:jwt] if valid?
27
-
28
- fetch
29
- end
30
- end
31
-
32
- # Force a refresh (used after a 401). Fetches a new token unconditionally.
33
- def force_refresh
34
- @mutex.synchronize do
35
- @cached = nil
36
- fetch
37
- end
38
- end
39
-
40
- private
41
-
42
- def valid?
43
- @cached && Time.now < @cached[:expires_at]
44
- end
45
-
46
- def fetch
47
- response = @transport.send_request(
48
- method: :post,
49
- url: "#{@config.base_url}/v3/token",
50
- headers: Telemetry.headers.merge(
51
- 'smileid-partner-id' => @config.partner_id,
52
- 'smileid-api-key' => @config.api_key
53
- ),
54
- query: {},
55
- body: nil,
56
- retryable: true
57
- )
58
-
59
- unless response.status == 200
60
- raise Errors.from_response(
61
- status_code: response.status,
62
- body: response.json,
63
- raw_body: response.raw_body,
64
- headers: response.headers
65
- )
66
- end
67
-
68
- jwt = response.json && response.json['token']
69
- raise Errors::AuthenticationError.new('token endpoint returned no token') if jwt.nil?
70
-
71
- @cached = { jwt: jwt, expires_at: expires_at(jwt) }
72
- jwt
73
- end
74
-
75
- def expires_at(jwt)
76
- exp = decode_exp(jwt)
77
- exp ? Time.at(exp) - EXPIRY_SKEW : Time.now
78
- end
79
-
80
- # Decode the `exp` claim from a JWT without verifying the signature. Returns
81
- # nil when the token cannot be decoded.
82
- def decode_exp(jwt)
83
- payload_segment = jwt.to_s.split('.')[1]
84
- return nil if payload_segment.nil?
85
-
86
- payload = JSON.parse(Base64.urlsafe_decode64(pad(payload_segment)))
87
- exp = payload['exp']
88
- exp.is_a?(Numeric) ? exp : nil
89
- rescue ArgumentError, JSON::ParserError
90
- nil
91
- end
92
-
93
- def pad(segment)
94
- remainder = segment.length % 4
95
- remainder.zero? ? segment : segment + ('=' * (4 - remainder))
96
- end
97
- end
98
- end