didww-otp_verification 1.1.0 → 1.3.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: b8f7f380aca63b49a1e661d0834f16f7d94af7fdacf63e1e54d7ddacac1e8a79
4
- data.tar.gz: e1920bc8b4994f0abe90fc7a93b43339a1e5dc2d492983e93625a24b70f23285
3
+ metadata.gz: 2d642797b509a6a3a07315a163ac283086e3877a7d6304f88fb968995ad16f09
4
+ data.tar.gz: 6656b36bd906929a5397887573d1d1b40ed29fb3cecbe8add71ec5cc2355934d
5
5
  SHA512:
6
- metadata.gz: c8867b191e13078948e4427c2b5321b365dd52f79481f5ff4e81f56eae59466f8f0a006c6979030aebc8e6ac1432c1bd6d11f9ac35d9637b7c1a7a7dd104d918
7
- data.tar.gz: 330d1aea846c2104f48db034df7eed4af4e15bc47c045c086068db419bcbfffa4271d156f1da818303353d0b6f9ae53ead2c56f4df2159502824023990c3b781
6
+ metadata.gz: c994241bc5f386def42788c2bc6d7b6fbad3050784de09a396252046343df7fa16cd8c6066df35a2168cbb101fffec56f2dded64d2a5d7ff82204e5c40e1683a
7
+ data.tar.gz: 8e3313f28b2c2327b5e73d31ba4b24d574981f89a4878089e584924dbc395035e1b86ea24c0a3d7622b68733e2bf1da8f26db6ea50e6c86828bf1f659a46cea0
data/CHANGELOG.md CHANGED
@@ -6,6 +6,82 @@ 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.3.0] — 2026-11
10
+
11
+ ### Added
12
+
13
+ - **`destination_prefix_rate_limited` error code**, raised as `RateLimitedError`
14
+ (429) from `start_verification` when there are too many verifications to a
15
+ number range. `#retry_after` carries the `Retry-After` wait in seconds; do not
16
+ retry it automatically.
17
+ - **`channel_blocked` error code.** A start can be refused with a 422 carrying it
18
+ when the account can't use the requested delivery method. Contact DIDWW
19
+ support.
20
+
21
+ ## [1.2.0] — 2026-10
22
+
23
+ ### Added
24
+
25
+ - **`custom:` on `start_verification`**, up to 4096 characters of free text sent
26
+ as top-level `data.custom` and delivered unchanged to your callback server in
27
+ the `verification_request` body (the key is absent when unset). It is not
28
+ returned in API responses. A longer value is refused with the new
29
+ `custom_too_long` error code.
30
+ - **`sms: {autofill: ...}`** with type `app_hash`, `domain` or `none`.
31
+ `Verification#sms_autofill` reads back the stored marker, `app_hash` or
32
+ `domain`, whose `value` is the hash or the domain. New error codes
33
+ `autofill_invalid` and `sms_domain_not_configured`.
34
+ - **`template_group` in the `sms:` and `callout:` options**, to pick a template
35
+ group instead of the application's default. New readers
36
+ `Verification#sms_template_group`/`#callout_template_group`,
37
+ `#sms_template_group_used`/`#callout_template_group_used` and
38
+ `#sms_template_fallback_reason`/`#callout_template_fallback_reason`; the last
39
+ is an open set (`language_missing`, `version_pending`, `version_rejected`,
40
+ `audio_not_ready`, `group_missing`). An unknown group is refused with the new
41
+ `template_group_not_found` error code.
42
+ - **Secret rotation support in callback verification.** During a secret
43
+ rotation the service also sends an `x-signature-previous` header, signed with
44
+ the previous secret. `CallbackVerifier#valid?` takes an optional
45
+ `previous_signature:` and accepts the callback when either signature matches
46
+ the configured secret. `RailsCallbackVerifier` reads the header itself.
47
+ Upgrade the SDK before rotating a secret.
48
+ - **New 429 error codes:** `rate_limit_per_ip` (too many requests from this IP
49
+ address), `rate_limit_failed_auth` (too many failed authentication attempts
50
+ from this IP address), `rate_limit_per_account` (too many requests for this
51
+ account), `rate_limit_public_per_account` (too many public requests for this
52
+ account) and `volume_limit_reached` (the application reached its daily or
53
+ monthly verification limit; `Retry-After` can be days, so surface it instead
54
+ of retrying). All raise `RateLimitedError`.
55
+ - **New outcome codes:** `application_paused` and `destination_country_disabled`
56
+ (status `denied`).
57
+
58
+ ### Changed
59
+
60
+ - **`destination_not_supported_for_channel` is now an outcome, not a 422.** A
61
+ start for a number with no supported country, no delivery for the requested
62
+ method, or in a blocked range returns `201` with status `failed` and this
63
+ `error_code`. Code that rescued the 422 now gets a failed verification
64
+ instead. The error code stays in the list for compatibility.
65
+
66
+ - **Supported Ruby is now 3.3+** (`required_ruby_version >= 3.3.0`). Ruby 3.1
67
+ went EOL on 2025-03-26 and 3.2 on 2026-03-31. CI tests 3.3, 3.4 and 4.0.
68
+
69
+ ### Deprecated
70
+
71
+ - **The `sms: {app_hash: ...}` option and `Verification#sms_app_hash`.** Use
72
+ `autofill`. The option is sent as `autofill: {type: "app_hash", value: ...}`
73
+ with a `:deprecated` warning; passing it together with a non-nil `autofill`
74
+ raises `ArgumentError`.
75
+ - **The `stale_dispatch` outcome code.** The service no longer sends it and
76
+ reports these verifications as `dispatch_failed`. It stays documented so
77
+ existing matches keep working, and will be dropped from the docs in the next
78
+ major version.
79
+
80
+ ### Docs
81
+
82
+ - A Testing section on the sandbox environment, which sends nothing and gives
83
+ a predefined result only for the reserved test numbers.
84
+
9
85
  ## [1.1.0] — 2026-10
10
86
 
11
87
  ### Added
data/README.md CHANGED
@@ -65,15 +65,23 @@ reads only the block matching `delivery_method` and ignores the others.
65
65
  | Field | Description |
66
66
  | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67
67
  | `languages` | Preferred template languages as BCP 47 tags, most preferred first. Matched exactly, so the region subtag is required — `"pl"` does not match `pl-PL`. Unmatched tags fall back to `en-US`. |
68
- | `app_hash` | Android SMS Retriever hash: exactly 11 characters of `[A-Za-z0-9+/]`. The delivered message is then prefixed with `<#> ` and the hash appended as its last token, so the handset can auto-fill the code. Omit it on every other platform. |
68
+ | `autofill` | Auto-fill marker for the delivered message. `{type: "app_hash", value: "..."}` is the Android SMS Retriever hash: exactly 11 characters of `[A-Za-z0-9+/]`. The message is then prefixed with `<#> ` and the hash appended as its last token, so the handset can auto-fill the code. `{type: "domain"}` adds a domain-bound line for the application's configured SMS domain. `{type: "none"}` explicitly asks for no marker. Omit it to use the application default. |
69
+ | `template_group` | Name of one of your template groups, used instead of the application's default. |
69
70
 
70
- `app_hash` is here because a Ruby backend often starts the verification on behalf
71
+ `autofill` is here because a Ruby backend often starts the verification on behalf
71
72
  of an Android app — the hash identifies that app, so only the app can compute it.
73
+ `{type: "domain"}` takes no `value`; on an application with no SMS domain it is
74
+ refused with `sms_domain_not_configured`. A malformed `autofill` is refused with
75
+ `autofill_invalid`.
76
+ The former `app_hash:` key still works as a deprecated alias for
77
+ `autofill: {type: "app_hash", value: ...}` (it prints a deprecation warning
78
+ when Ruby's `:deprecated` warnings are enabled). Passing both with non-nil values
79
+ raises `ArgumentError`; `autofill: nil` counts as absent.
72
80
 
73
81
  ```ruby
74
82
  client.start_verification(
75
83
  destination: "+4915112345678", delivery_method: "sms",
76
- sms: {languages: ["en-US"], app_hash: "A1b2C3d4E5f"}
84
+ sms: {languages: ["en-US"], autofill: {type: "app_hash", value: "A1b2C3d4E5f"}}
77
85
  )
78
86
  ```
79
87
 
@@ -82,6 +90,10 @@ client.start_verification(
82
90
  | Field | Description |
83
91
  | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
92
  | `languages` | Preferred announcement languages — the same tags, order and matching rules as the `sms:` ones, so the same list is valid for either method. It is not shared between them, though: each method reads only its own block, so the list goes under `callout:` here and under `sms:` there. The catalogues are separate too, so a tag that has a message template but no recording is accepted and falls back to `en-US`. |
93
+ | `template_group` | Name of one of your template groups, as for `sms:`. |
94
+
95
+ An unknown `template_group` on either method is refused with
96
+ `template_group_not_found`.
85
97
 
86
98
  ```ruby
87
99
  client.start_verification(
@@ -96,6 +108,20 @@ API ships it. The cost of that is no typo protection: an unrecognized option is
96
108
  ignored rather than rejected, and a misspelled key returns `201` with the
97
109
  defaults applied, not an error.
98
110
 
111
+ ### `custom:`
112
+
113
+ Optional free text, up to 4096 characters, sent as top-level `data.custom` (an
114
+ empty string counts as absent). It is not returned by the API; it reaches your
115
+ callback server as `data.custom` in the `verification_request` body; the key is
116
+ absent when unset. A longer value is refused with `custom_too_long`.
117
+
118
+ ```ruby
119
+ client.start_verification(
120
+ destination: "+4915112345678", delivery_method: "sms",
121
+ custom: "order-42"
122
+ )
123
+ ```
124
+
99
125
  ### Reading a method block back
100
126
 
101
127
  The response carries the block for the method that was used, readable field by
@@ -106,11 +132,17 @@ v.sms_template # => "Your code is {{CODE}}"
106
132
  v.sms_language # => "en-US"
107
133
  v.sms_interception_timeout # => 300
108
134
  v.sms_code_length # => 6
109
- v.sms_app_hash # => "A1b2C3d4E5f", or nil if none was stored
135
+ v.sms_autofill # => {"type" => "app_hash", "value" => "A1b2C3d4E5f"}, or nil
136
+ v.sms_template_group # => "checkout", or nil if none applied
137
+ v.sms_template_group_used # => true
138
+ v.sms_template_fallback_reason # => nil, or e.g. "language_missing"
110
139
  v.sms # => the raw block, or nil on a callout verification
111
140
 
112
141
  v.callout_language # => "de-DE"
113
142
  v.callout_code_length # => 6
143
+ v.callout_template_group # => "checkout", or nil if none applied
144
+ v.callout_template_group_used # => true
145
+ v.callout_template_fallback_reason # => nil, or e.g. "audio_not_ready"
114
146
  v.callout # => the raw block, or nil on an sms verification
115
147
  ```
116
148
 
@@ -127,8 +159,16 @@ default 300) and is **not** a deadline for the verification itself — manual
127
159
  entry keeps working until `expires_at`.
128
160
  `sms_code_length`/`callout_code_length` is the length of this verification's
129
161
  code, 4–8 digits, set per application (default 6).
130
- `sms_app_hash` is echoed back only when one was stored, so it reflects what was
131
- persisted rather than what was requested.
162
+ `sms_autofill` is echoed back only when a hash or domain marker was stored, so it
163
+ reflects what was persisted rather than what was requested; for `domain`, `value`
164
+ is the domain. `sms_app_hash` is a deprecated reader derived from it: the `value`
165
+ when the type is `app_hash`, otherwise `nil`.
166
+ `*_template_group_used` is true when the group's template was used. When it is
167
+ false, `*_template_fallback_reason` says why: `language_missing`,
168
+ `version_pending`, `version_rejected`, `audio_not_ready` or `group_missing` (the
169
+ group no longer exists; `*_template_group` is then `nil`). Both are false/`nil`
170
+ when no template group applies at all. Like error codes, the reasons are an open
171
+ set of plain strings.
132
172
 
133
173
  ### Address by phone number
134
174
 
@@ -167,6 +207,15 @@ DIDWW::OTPVerification.configure do |c|
167
207
  end
168
208
  ```
169
209
 
210
+ ## Testing
211
+
212
+ The sandbox sends no real SMS and places no real calls. Only the reserved test
213
+ numbers give a predefined result, such as delivered, failed or expired; a
214
+ delivered one has a fixed code. Any other number is never delivered to, and its
215
+ verification ends as failed. The test numbers, their outcomes and the code to
216
+ submit are listed in
217
+ [Testing on sandbox](https://doc.didww.com/otp-verification/testing.html).
218
+
170
219
  ## Configuration
171
220
 
172
221
  Set global defaults; every field is overridable per `Client`.
@@ -273,15 +322,35 @@ ok = verifier.valid?(
273
322
  content_type: request.content_type,
274
323
  body: request.raw_post, # RAW body bytes — do not re-serialize
275
324
  timestamp: request.headers["x-timestamp"],
276
- signature: signature
325
+ signature: signature,
326
+ previous_signature: request.headers["x-signature-previous"] # optional
277
327
  )
278
328
 
279
329
  render json: { action: ok ? "allow" : "deny" }
280
330
  ```
281
331
 
332
+ The `verification_request` body carries the `custom` value you passed to
333
+ `start_verification` as `data.custom` (absent when unset). When the verification
334
+ is started from a mobile app, that value is client-controlled: validate it
335
+ against your own records rather than trusting it.
336
+
282
337
  The verifier also enforces a 5-minute timestamp window (configurable via
283
338
  `tolerance:`) to reject replays.
284
339
 
340
+ ### Rotating the application secret
341
+
342
+ While a rotated-out secret is still valid, DIDWW also sends an
343
+ `x-signature-previous` header: the same signature computed with the previous
344
+ secret. The verifier accepts a callback when either the `Authorization`
345
+ signature or `previous_signature` matches the secret you configured. A receiver
346
+ can therefore switch from the old secret to the new one at any point during the
347
+ grace period, with no failed callbacks. Without the header the verifier behaves
348
+ as before.
349
+
350
+ Upgrade the SDK before you rotate a secret, because an older version ignores
351
+ `x-signature-previous` and rejects callbacks signed with the new secret while
352
+ the old one is configured.
353
+
285
354
  ### Rails
286
355
 
287
356
  `RailsCallbackVerifier` pulls every signed field off an `ActionDispatch`
@@ -335,13 +404,23 @@ carries `error_code` (switch on it) and `error_detail` (display it). These are
335
404
  | ---------- | -------- | -------------------------------------------------------------------------------------------- |
336
405
  | `pending` | no | `nil` — on its way, or awaiting a report |
337
406
  | `verified` | yes | `nil` |
338
- | `failed` | yes | `too_many_attempts`, `dispatch_failed`, `stale_dispatch`, `superseded`, `application_deleted` |
407
+ | `failed` | yes | `too_many_attempts`, `dispatch_failed`, `superseded`, `application_deleted`, `destination_not_supported_for_channel`, `stale_dispatch` (deprecated) |
339
408
  | `expired` | yes | `expired` — past the application's code lifetime (60–600 s, default 300) |
340
- | `denied` | yes | `denied_by_callback`, `denied_missing_callback_url`, `denied_invalid_callback_response` |
409
+ | `denied` | yes | `denied_by_callback`, `denied_missing_callback_url`, `denied_invalid_callback_response`, `application_paused`, `destination_country_disabled` |
341
410
 
342
411
  `superseded` means a newer `start_verification` for the same number retired this
343
412
  one — only the newest verification per number stays active.
344
413
 
414
+ Some starts are refused as an outcome rather than an error: a `201` whose status
415
+ is `denied` (`application_paused`, `destination_country_disabled`) or `failed`
416
+ (`destination_not_supported_for_channel`: the number has no supported country,
417
+ can't be reached by this delivery method, or is in a blocked range). Earlier
418
+ versions returned the last one as a `422`; that code stays in the error list for
419
+ compatibility only.
420
+
421
+ `stale_dispatch` is deprecated: the service no longer sends it and reports these
422
+ verifications as `dispatch_failed`.
423
+
345
424
  ```ruby
346
425
  v = client.get_verification(id)
347
426
 
@@ -370,7 +449,7 @@ Non-2xx responses raise a typed error under `DIDWW::OTPVerification::Error`:
370
449
  | `BalanceInsufficientError` | 402 | `balance_insufficient` |
371
450
  | `NotFoundError` | 404 | `not_found` |
372
451
  | `ValidationError` | 400 / 422 | `parameter_missing` / per-field codes |
373
- | `RateLimitedError` | 429 | `destination_in_cooldown` |
452
+ | `RateLimitedError` | 429 | `destination_in_cooldown`, `rate_limit_*`, `volume_limit_reached`, `destination_prefix_rate_limited` |
374
453
  | `ServerError` | 5xx | `internal_error` |
375
454
 
376
455
  Every error carries the API's coded envelope: `#errors` is an array of
@@ -394,21 +473,38 @@ rescue DIDWW::OTPVerification::ValidationError => e
394
473
  end
395
474
  ```
396
475
 
476
+ `channel_blocked` (422): the account can't use the requested delivery method.
477
+ No verification is created; contact DIDWW support.
478
+
397
479
  ## Retries
398
480
 
399
481
  The SDK does **not** auto-retry. If you add `faraday-retry`, exclude `POST`
400
482
  (double-charges) and `PATCH` (each report counts against the 3-attempt limit).
401
483
 
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.
484
+ A 429 raises `RateLimitedError`, and its code says which limit was hit:
485
+
486
+ - `destination_in_cooldown`: a verification for this destination was requested
487
+ too recently.
488
+ - `rate_limit_per_ip`: too many requests from this IP address.
489
+ - `rate_limit_failed_auth`: too many failed authentication attempts from this
490
+ IP address.
491
+ - `rate_limit_per_account`: too many requests for this account.
492
+ - `rate_limit_public_per_account`: too many public requests for this account.
493
+ - `volume_limit_reached`: the application reached its daily or monthly
494
+ verification limit. `Retry-After` can be long (days) — surface it to the
495
+ user instead of retrying. Contact support to raise the limit.
496
+ - `destination_prefix_rate_limited`: too many verifications to this number range;
497
+ try again later. Wait `Retry-After` before starting another one, and show the
498
+ user "Too many verification requests right now. Please try again later."
499
+
500
+ `#retry_after` gives the wait in seconds (`nil` if the `Retry-After` header is
501
+ missing or unparseable) — wait that long before trying again yourself; never
502
+ auto-retry it.
407
503
 
408
504
  ## Development
409
505
 
410
506
  Development is pinned to the Ruby in `.ruby-version`; the gem itself supports
411
- 3.1+.
507
+ 3.3+.
412
508
 
413
509
  ```sh
414
510
  bundle install
@@ -19,7 +19,8 @@ module DIDWW
19
19
  # verifier.valid?(
20
20
  # method: "POST", path: "/callbacks/didww",
21
21
  # content_type: "application/json", body: raw_body,
22
- # timestamp: request.headers["x-timestamp"], signature: signature
22
+ # timestamp: request.headers["x-timestamp"], signature: signature,
23
+ # previous_signature: request.headers["x-signature-previous"]
23
24
  # )
24
25
  class CallbackVerifier
25
26
  # @param tolerance [Integer] max allowed clock skew, in seconds (server uses 5 minutes).
@@ -41,16 +42,21 @@ module DIDWW
41
42
  [key, signature]
42
43
  end
43
44
 
44
- # @return [Boolean] true when both the timestamp is fresh and the
45
- # signature matches (constant-time comparison).
46
- def valid?(method:, path:, content_type:, body:, timestamp:, signature:)
45
+ # @param previous_signature [String, nil] the `x-signature-previous` header, sent
46
+ # during a secret rotation and signed with the previous secret. Ignored when absent.
47
+ # @return [Boolean] true when the timestamp is fresh and the signature, or the
48
+ # previous signature, matches (constant-time comparison).
49
+ def valid?(method:, path:, content_type:, body:, timestamp:, signature:, previous_signature: nil)
47
50
  return false if Util.blank?(signature)
48
51
  return false unless fresh?(timestamp)
49
52
 
50
53
  expected = @signer.sign(
51
54
  method:, path:, content_type:, body:, timestamp: timestamp
52
55
  )
53
- secure_compare(expected, signature)
56
+ # Both comparisons always run, so timing does not reveal which header matched.
57
+ current = secure_compare(expected, signature)
58
+ previous = secure_compare(expected, previous_signature)
59
+ current || previous
54
60
  end
55
61
 
56
62
  private
@@ -63,6 +69,7 @@ module DIDWW
63
69
 
64
70
  # Constant-time string comparison via the audited stdlib implementation.
65
71
  def secure_compare(a, b)
72
+ return false if b.nil?
66
73
  return false unless a.bytesize == b.bytesize
67
74
 
68
75
  OpenSSL.fixed_length_secure_compare(a, b)
@@ -50,10 +50,21 @@ module DIDWW
50
50
  # reads only the block matching +delivery_method+ and ignores the others.
51
51
  # An unrecognized option is ignored rather than rejected, so a typo there
52
52
  # fails silently. Methods with no options of their own take no keyword.
53
- def start_verification(destination:, delivery_method:, sms: nil, callout: nil)
53
+ #
54
+ # +custom+ is free text (up to 4096 characters) delivered to your
55
+ # callback server as +data.custom+; it is not part of the response.
56
+ #
57
+ # The +sms+ key +app_hash+ is deprecated: it is sent as
58
+ # <tt>autofill: {type: "app_hash", value: ...}</tt> with a deprecation
59
+ # warning. Passing it together with +autofill+ raises ArgumentError,
60
+ # because the API rejects that combination. Both apply only when
61
+ # +delivery_method+ is +sms+, the only method that reads +autofill+.
62
+ def start_verification(destination:, delivery_method:, sms: nil, callout: nil, custom: nil)
54
63
  data = {destination:, delivery_method:}
64
+ data[:custom] = custom unless custom.nil?
55
65
  data[:sms] = sms unless sms.nil?
56
66
  data[:callout] = callout unless callout.nil?
67
+ data[:sms] = migrate_app_hash(data[:sms]) if delivery_method.to_s == "sms" && data[:sms].is_a?(Hash)
57
68
  request(:post, "#{API_PREFIX}/verifications", data)
58
69
  end
59
70
 
@@ -85,6 +96,26 @@ module DIDWW
85
96
 
86
97
  private
87
98
 
99
+ # Maps the deprecated sms +app_hash+ onto +autofill+ on a copy, so the
100
+ # caller's hash is left alone.
101
+ def migrate_app_hash(sms)
102
+ keys = [:app_hash, "app_hash"].select { |k| sms.key?(k) }
103
+ return sms if keys.empty?
104
+
105
+ # A nil hash counts as absent, as the API always treated it.
106
+ key = keys.find { |k| !sms[k].nil? }
107
+ return sms.except(*keys) unless key
108
+ # A nil autofill counts as absent too, as the API treats it.
109
+ autofill_keys = [:autofill, "autofill"].select { |k| sms.key?(k) }
110
+ if autofill_keys.any? { |k| !sms[k].nil? }
111
+ raise ArgumentError, "sms: pass either autofill or the deprecated app_hash, not both"
112
+ end
113
+
114
+ warn "DIDWW::OTPVerification: sms app_hash is deprecated; " \
115
+ "use sms: {autofill: {type: \"app_hash\", value: ...}}", category: :deprecated, uplevel: 2
116
+ sms.except(*keys, *autofill_keys).merge(autofill: {type: "app_hash", value: sms[key]})
117
+ end
118
+
88
119
  # Percent-encode a value for use as a single URL path segment. Notably
89
120
  # turns a leading "+" into "%2B" so it survives proxies that would
90
121
  # otherwise decode "+" to a space in the path.
@@ -48,8 +48,11 @@ 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.
51
+ # 429 Too Many Requests. +code+ says which limit was hit:
52
+ # +destination_in_cooldown+ (this destination was requested too recently),
53
+ # a +rate_limit_*+ request limit, +volume_limit_reached+ (a daily or
54
+ # monthly limit; +retry_after+ can be days), or +destination_prefix_rate_limited+
55
+ # (too many verifications to this number range; wait +retry_after+).
53
56
  # Never auto-retry this.
54
57
  class RateLimitedError < APIError
55
58
  # @return [Integer, nil] seconds to wait, from the Retry-After header.
@@ -47,7 +47,8 @@ module DIDWW
47
47
  content_type: request.content_type,
48
48
  body: request.raw_post,
49
49
  timestamp: request.headers["x-timestamp"],
50
- signature: signature
50
+ signature: signature,
51
+ previous_signature: request.headers["x-signature-previous"]
51
52
  )
52
53
  end
53
54
  end
@@ -61,10 +61,40 @@ module DIDWW
61
61
  @sms && @sms["code_length"]
62
62
  end
63
63
 
64
- # @return [String, nil] SMS Retriever hash, echoed back only when one was
65
- # stored on this verification.
64
+ # @return [Hash, nil] the stored autofill marker, +{"type", "value"}+ with
65
+ # type +app_hash+ (value: the hash) or +domain+ (value: the domain);
66
+ # +nil+ unless one was stored on this verification.
67
+ def sms_autofill
68
+ @sms && @sms["autofill"]
69
+ end
70
+
71
+ # @deprecated Use {#sms_autofill}.
72
+ # @return [String, nil] the SMS Retriever hash, when the stored autofill
73
+ # is of type +app_hash+.
66
74
  def sms_app_hash
67
- @sms && @sms["app_hash"]
75
+ autofill = sms_autofill
76
+ return @sms && @sms["app_hash"] unless autofill
77
+
78
+ autofill["value"] if autofill["type"] == "app_hash"
79
+ end
80
+
81
+ # @return [String, nil] the template group applied; nil when none applies
82
+ # or it no longer exists. Only present for the +sms+ method.
83
+ def sms_template_group
84
+ @sms && @sms["template_group"]
85
+ end
86
+
87
+ # @return [Boolean, nil] true when the group's template was used. Only
88
+ # present for the +sms+ method.
89
+ def sms_template_group_used
90
+ @sms && @sms["template_group_used"]
91
+ end
92
+
93
+ # @return [String, nil] why the group's template was not used, e.g.
94
+ # +language_missing+ (an open set); nil when it was or when no group
95
+ # applies. Only present for the +sms+ method.
96
+ def sms_template_fallback_reason
97
+ @sms && @sms["template_fallback_reason"]
68
98
  end
69
99
 
70
100
  # The tag the announcement is played in: the first requested language the
@@ -83,6 +113,25 @@ module DIDWW
83
113
  @callout && @callout["code_length"]
84
114
  end
85
115
 
116
+ # @return [String, nil] the template group applied; nil when none applies
117
+ # or it no longer exists. Only present for the +callout+ method.
118
+ def callout_template_group
119
+ @callout && @callout["template_group"]
120
+ end
121
+
122
+ # @return [Boolean, nil] true when the group's template was used. Only
123
+ # present for the +callout+ method.
124
+ def callout_template_group_used
125
+ @callout && @callout["template_group_used"]
126
+ end
127
+
128
+ # @return [String, nil] why the group's template was not used, e.g.
129
+ # +audio_not_ready+ (an open set); nil when it was or when no group
130
+ # applies. Only present for the +callout+ method.
131
+ def callout_template_fallback_reason
132
+ @callout && @callout["template_fallback_reason"]
133
+ end
134
+
86
135
  def pending? = status == "pending"
87
136
  def verified? = status == "verified"
88
137
  def failed? = status == "failed"
@@ -1,5 +1,5 @@
1
1
  module DIDWW
2
2
  module OTPVerification
3
- VERSION = "1.1.0"
3
+ VERSION = "1.3.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.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DIDWW
@@ -85,7 +85,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: 3.1.0
88
+ version: 3.3.0
89
89
  required_rubygems_version: !ruby/object:Gem::Requirement
90
90
  requirements:
91
91
  - - ">="