didww-otp_verification 1.0.0 → 1.1.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: 95cc4c0b5d246e12eeb76854405106c8ef277a4790dbbfee8620f6984a6074a8
4
- data.tar.gz: 5d157aba75750f9b9a605b1d114dfdd289dbc9ea949b3d1d2a0a5636da12c0a9
3
+ metadata.gz: b8f7f380aca63b49a1e661d0834f16f7d94af7fdacf63e1e54d7ddacac1e8a79
4
+ data.tar.gz: e1920bc8b4994f0abe90fc7a93b43339a1e5dc2d492983e93625a24b70f23285
5
5
  SHA512:
6
- metadata.gz: fa0921c7b444d6efaba56f12c050d68b408c98f956a21fd3656c3672eabc8c73415a8d3fb5e16bc9c32f7bbcfca930856d5cc1c80fabc0f98a758e40bfb22d59
7
- data.tar.gz: aa033d4b455850aa7ed1c5a79861bfb35a303fd8e677387ebf1d77be9ccb78468cbb5bcc4d7fe1438995a3277faa2eae67c07d689068aa5fa531bd54627cab62
6
+ metadata.gz: c8867b191e13078948e4427c2b5321b365dd52f79481f5ff4e81f56eae59466f8f0a006c6979030aebc8e6ac1432c1bd6d11f9ac35d9637b7c1a7a7dd104d918
7
+ data.tar.gz: 330d1aea846c2104f48db034df7eed4af4e15bc47c045c086068db419bcbfffa4271d156f1da818303353d0b6f9ae53ead2c56f4df2159502824023990c3b781
data/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
6
6
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html): from 1.0.0 onwards a breaking change
7
7
  to the public surface requires a major version.
8
8
 
9
+ ## [1.1.0] — 2026-10
10
+
11
+ ### Added
12
+
13
+ - **A User-Agent header.** Every request now sends
14
+ `User-Agent: didww-verification-ruby/<version>`.
15
+ - **`code_length` on the `sms` and `callout` blocks**, readable via
16
+ `Verification#sms_code_length`/`#callout_code_length` — the length of the
17
+ generated code, 4–8 digits, set per application.
18
+ - **`RateLimitedError` (429, `destination_in_cooldown`)**, raised when
19
+ `start_verification` repeats for the same app and destination inside the
20
+ API's cooldown window. `#retry_after` reads the `Retry-After` header in
21
+ seconds (`nil` if missing or unparseable). The SDK never auto-retries it.
22
+
23
+ ### Changed
24
+
25
+ - Corrected README claims that the verification lifetime is a fixed 120 s /
26
+ 2-minute window: it is the per-app `code_expiry` setting (60–600 s, default
27
+ 300), which `sms_interception_timeout` also equals.
28
+ - A 429 now raises `RateLimitedError`, an `APIError` subclass, instead of the
29
+ `APIError` base class. `rescue APIError` still catches it, but a strict
30
+ `e.instance_of?(APIError)` check no longer matches a 429.
31
+
9
32
  ## [1.0.0] — 2026-09
10
33
 
11
34
  First public release.
data/README.md CHANGED
@@ -33,7 +33,8 @@ verification.status # => "pending"
33
33
  verification.pending? # => true
34
34
  verification.to_h # => raw response data Hash with string keys
35
35
 
36
- # Report the code the user entered (counts as an attempt; max 3, expires in 2 min)
36
+ # Report the code the user entered (counts as an attempt; max 3, expires per
37
+ # the app's code lifetime)
37
38
  result = client.report_verification(
38
39
  verification.id, delivery_method: "sms", code: "1234"
39
40
  )
@@ -103,11 +104,13 @@ field or as a whole:
103
104
  ```ruby
104
105
  v.sms_template # => "Your code is {{CODE}}"
105
106
  v.sms_language # => "en-US"
106
- v.sms_interception_timeout # => 120
107
+ v.sms_interception_timeout # => 300
108
+ v.sms_code_length # => 6
107
109
  v.sms_app_hash # => "A1b2C3d4E5f", or nil if none was stored
108
110
  v.sms # => the raw block, or nil on a callout verification
109
111
 
110
112
  v.callout_language # => "de-DE"
113
+ v.callout_code_length # => 6
111
114
  v.callout # => the raw block, or nil on an sms verification
112
115
  ```
113
116
 
@@ -119,8 +122,11 @@ because the two catalogues are separate, a tag honoured on `sms` can still fall
119
122
  back on `callout`.
120
123
 
121
124
  `sms_interception_timeout` is how many seconds a client should keep an on-device
122
- SMS listener armed. It is a fixed budget, not a countdown, and **not** a deadline
123
- for the verification — manual entry keeps working until `expires_at`.
125
+ SMS listener armed. It equals the app's configured code lifetime (60–600 s,
126
+ default 300) and is **not** a deadline for the verification itself — manual
127
+ entry keeps working until `expires_at`.
128
+ `sms_code_length`/`callout_code_length` is the length of this verification's
129
+ code, 4–8 digits, set per application (default 6).
124
130
  `sms_app_hash` is echoed back only when one was stored, so it reflects what was
125
131
  persisted rather than what was requested.
126
132
 
@@ -197,6 +203,8 @@ DIDWW::OTPVerification::Client.new(
197
203
  ) { |conn| conn.proxy = "http://proxy:3128" }
198
204
  ```
199
205
 
206
+ Every request carries `User-Agent: didww-verification-ruby/<version>`.
207
+
200
208
  ## Auth modes
201
209
 
202
210
  | Mode | Header | Secret | Notes |
@@ -328,7 +336,7 @@ carries `error_code` (switch on it) and `error_detail` (display it). These are
328
336
  | `pending` | no | `nil` — on its way, or awaiting a report |
329
337
  | `verified` | yes | `nil` |
330
338
  | `failed` | yes | `too_many_attempts`, `dispatch_failed`, `stale_dispatch`, `superseded`, `application_deleted` |
331
- | `expired` | yes | `expired` — past the 2-minute window |
339
+ | `expired` | yes | `expired` — past the application's code lifetime (60–600 s, default 300) |
332
340
  | `denied` | yes | `denied_by_callback`, `denied_missing_callback_url`, `denied_invalid_callback_response` |
333
341
 
334
342
  `superseded` means a newer `start_verification` for the same number retired this
@@ -362,6 +370,7 @@ Non-2xx responses raise a typed error under `DIDWW::OTPVerification::Error`:
362
370
  | `BalanceInsufficientError` | 402 | `balance_insufficient` |
363
371
  | `NotFoundError` | 404 | `not_found` |
364
372
  | `ValidationError` | 400 / 422 | `parameter_missing` / per-field codes |
373
+ | `RateLimitedError` | 429 | `destination_in_cooldown` |
365
374
  | `ServerError` | 5xx | `internal_error` |
366
375
 
367
376
  Every error carries the API's coded envelope: `#errors` is an array of
@@ -390,6 +399,12 @@ end
390
399
  The SDK does **not** auto-retry. If you add `faraday-retry`, exclude `POST`
391
400
  (double-charges) and `PATCH` (each report counts against the 3-attempt limit).
392
401
 
402
+ A `start_verification` repeated too soon for the same app and destination
403
+ raises `RateLimitedError` (429, `destination_in_cooldown`) — a short per-number
404
+ cooldown (currently 30 s). `#retry_after` gives the wait in seconds (`nil` if
405
+ the `Retry-After` header is missing or unparseable) — wait and call again
406
+ yourself; never auto-retry it.
407
+
393
408
  ## Development
394
409
 
395
410
  Development is pinned to the Ruby in `.ruby-version`; the gem itself supports
@@ -21,6 +21,7 @@ module DIDWW
21
21
  # Unspecified arguments fall back to DIDWW::OTPVerification.configuration.
22
22
  class Client
23
23
  API_PREFIX = "/api/v1".freeze
24
+ USER_AGENT = "didww-verification-ruby/#{VERSION}".freeze
24
25
 
25
26
  attr_reader :key, :auth_mode, :base_url
26
27
 
@@ -110,11 +111,9 @@ module DIDWW
110
111
  end
111
112
 
112
113
  errors = parse_errors(body)
113
- raise error_class(response.status).new(
114
- status: response.status,
115
- errors: errors,
116
- response: response
117
- )
114
+ klass = error_class(response.status)
115
+ extra = (klass == RateLimitedError) ? {retry_after: parse_retry_after(response)} : {}
116
+ raise klass.new(status: response.status, errors: errors, response: response, **extra)
118
117
  end
119
118
 
120
119
  # Map the {"errors": [{"code", "detail"}]} envelope to ErrorItem objects.
@@ -131,12 +130,20 @@ module DIDWW
131
130
  end
132
131
  end
133
132
 
133
+ # @return [Integer, nil] the Retry-After header in seconds, nil if
134
+ # missing or not a plain non-negative integer (e.g. an HTTP-date).
135
+ def parse_retry_after(response)
136
+ value = response.headers["Retry-After"].to_s
137
+ Integer(value, 10) if value.match?(/\A\d+\z/)
138
+ end
139
+
134
140
  def error_class(status)
135
141
  case status
136
142
  when 401 then UnauthorizedError
137
143
  when 402 then BalanceInsufficientError
138
144
  when 404 then NotFoundError
139
145
  when 400, 422 then ValidationError
146
+ when 429 then RateLimitedError
140
147
  when 500..599 then ServerError
141
148
  else APIError
142
149
  end
@@ -147,6 +154,8 @@ module DIDWW
147
154
  conn.request :json
148
155
  conn.response :json, content_type: /\bjson$/
149
156
  @faraday_block&.call(conn)
157
+ # After the caller's block, so the block cannot replace it.
158
+ conn.headers["User-Agent"] = USER_AGENT
150
159
  # Auth (signing) must be installed last so it sees the final request
151
160
  # bytes/headers/path, after any user middleware from @faraday_block.
152
161
  apply_auth(conn)
@@ -48,6 +48,20 @@ module DIDWW
48
48
  # 400 Bad Request / 422 Unprocessable Content (validation errors in +errors+).
49
49
  class ValidationError < APIError; end
50
50
 
51
+ # 429 Too Many Requests (+destination_in_cooldown+ code): a start_verification
52
+ # for the same app and destination landed within the API's cooldown window.
53
+ # Never auto-retry this.
54
+ class RateLimitedError < APIError
55
+ # @return [Integer, nil] seconds to wait, from the Retry-After header.
56
+ # Nil if the header is missing or not a plain integer.
57
+ attr_reader :retry_after
58
+
59
+ def initialize(message = nil, status:, errors: [], response: nil, retry_after: nil)
60
+ @retry_after = retry_after
61
+ super(message, status: status, errors: errors, response: response)
62
+ end
63
+ end
64
+
51
65
  # 5xx Server Error (+internal_error+ code).
52
66
  class ServerError < APIError; end
53
67
  end
@@ -55,6 +55,12 @@ module DIDWW
55
55
  @sms && @sms["interception_timeout"]
56
56
  end
57
57
 
58
+ # @return [Integer, nil] the length of this verification's code, 4..8
59
+ # (set per application). Only present for the +sms+ method.
60
+ def sms_code_length
61
+ @sms && @sms["code_length"]
62
+ end
63
+
58
64
  # @return [String, nil] SMS Retriever hash, echoed back only when one was
59
65
  # stored on this verification.
60
66
  def sms_app_hash
@@ -71,6 +77,12 @@ module DIDWW
71
77
  @callout && @callout["language"]
72
78
  end
73
79
 
80
+ # @return [Integer, nil] the length of this verification's code, 4..8
81
+ # (set per application). Only present for the +callout+ method.
82
+ def callout_code_length
83
+ @callout && @callout["code_length"]
84
+ end
85
+
74
86
  def pending? = status == "pending"
75
87
  def verified? = status == "verified"
76
88
  def failed? = status == "failed"
@@ -1,5 +1,5 @@
1
1
  module DIDWW
2
2
  module OTPVerification
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: didww-otp_verification
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DIDWW