standard_id 0.48.0 → 0.48.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: f909ccb15d7b3f3b09fff0d61bd87db9e48614652ac1c4338a6e2390a7cb056e
4
- data.tar.gz: da699414b0e242675d61fc412d6e0d59f2f05fc0580778fa48e5600a3e48b874
3
+ metadata.gz: cdf22579e98c4ceb592cb20766d510d937d0e5d2f7f70c41074f984ddc3b0393
4
+ data.tar.gz: 35fbb21a24d40a307bd55279a86fa0d91549641e30f30d4101d667d9c5d7c793
5
5
  SHA512:
6
- metadata.gz: ab4a5fd3ae269431574ea2e82ac3c33914a1f6769a7602c1c4618fb20434382a47304ae90fa998f09e9adbabb2f34466d34c1ffca33a2a1b0707316094dd0f79
7
- data.tar.gz: 915c57e92d64784d1a65fd0f91265773fe03c087911f124b896d535d25509f8497358c149194b575cf699e21d70b0e367e3affd80547f1e63c31ec440ab16e6a
6
+ metadata.gz: cd202ffabe7287cb374442e6aeee83d0f2853a17d9cb7654ba816f28f48c4d6970b859b7d4d83581f09ef4a2793689b5fc620b545246bfb515691fda9a4cd1dd
7
+ data.tar.gz: 0e17bfff08af26fc343dedaf0d9500bbef69fe118031c305bdc29f33ede7231c8641c14b2b6156395702c4957c58b605dc6a24b54b4f975fa19618c8f4cade01
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.48.1] - 2026-10-20
11
+
12
+ ### Added
13
+
14
+ - **`StandardId::RetryDelayError`** (an `InvalidRequestError`, with `retry_after` seconds) is what the OTP resend cooldown (`passwordless.retry_delay`) now raises, and **`Otp.issue` reports it as `error_code: :cooldown`** instead of the generic `:invalid_request`. A host that wraps `Otp.issue` can tell "a usable code was issued moments ago" apart from a real failure and keep its own cooldown copy — which is what fundbright-web needed before it could drop its advisory lock around `Otp.issue` now that the cooldown check is atomic (0.48.0). Every existing `rescue StandardId::InvalidRequestError` keeps working.
15
+
10
16
  ## [0.48.0] - 2026-10-20
11
17
 
12
18
  ### Added
data/README.md CHANGED
@@ -1757,7 +1757,12 @@ A tripped limit raises `StandardId::RateLimitExceededError` (an `InvalidRequestE
1757
1757
  error handler renders as `429 rate_limit_exceeded` with `Retry-After`, the web login page shows as a
1758
1758
  flash, and `Otp.issue` returns as a failed result with `error_code: :rate_limited`). The per-target
1759
1759
  default matches the API's `api_passwordless_start_per_target`; if you raise that (or `login_per_email`)
1760
- raise `otp_start_per_target` with it, or it becomes the binding limit. The 30-second resend
1760
+ raise `otp_start_per_target` with it, or it becomes the binding limit.
1761
+
1762
+ The resend cooldown (`passwordless.retry_delay`) is reported separately from those limits:
1763
+ `StandardId::RetryDelayError` (an `InvalidRequestError` carrying `retry_after` seconds), which
1764
+ `Otp.issue` returns as `error_code: :cooldown`, so a host can tell "a code is already on its way"
1765
+ apart from a failure. The 30-second resend
1761
1766
  cooldown (`passwordless.retry_delay`) and the challenge insert run in one transaction, serialised per
1762
1767
  address with a PostgreSQL advisory lock, so two concurrent starts cannot both issue a code.
1763
1768
 
@@ -93,6 +93,21 @@ module StandardId
93
93
  def http_status = :too_many_requests
94
94
  end
95
95
 
96
+ # The OTP resend cooldown (`passwordless.retry_delay`) refused a start: a code
97
+ # for this target was issued less than retry_delay seconds ago and is still
98
+ # usable, so nothing new was sent. A subclass of InvalidRequestError so every
99
+ # existing rescue keeps working; Otp.issue reports it as `error_code:
100
+ # :cooldown` so a host can show its own "a code is already on its way" copy
101
+ # and tell it apart from a send failure. `retry_after` is the seconds left.
102
+ class RetryDelayError < InvalidRequestError
103
+ attr_reader :retry_after
104
+
105
+ def initialize(message = "Please wait before requesting another code", retry_after: nil)
106
+ super(message)
107
+ @retry_after = retry_after
108
+ end
109
+ end
110
+
96
111
  class InvalidClientError < OAuthError
97
112
  def oauth_error_code = :invalid_client
98
113
  def http_status = :unauthorized
@@ -74,7 +74,10 @@ module StandardId
74
74
  # - success?: true/false
75
75
  # - challenge: the created CodeChallenge (nil on failure)
76
76
  # - code: raw OTP code — only populated when delivery: :manual
77
- # - error_code: machine-readable symbol on failure
77
+ # - error_code: machine-readable symbol on failure — :invalid_request
78
+ # (bad target, username_validator rejection), :cooldown (a usable code
79
+ # was issued less than passwordless.retry_delay seconds ago; nothing new
80
+ # was sent), :rate_limited (rate_limits.otp_start_per_* tripped)
78
81
  # - error_message: human-readable message on failure
79
82
  IssueResult = Data.define(:success?, :challenge, :code, :error_code, :error_message) do
80
83
  # Parallel naming with VerificationService::Result, which exposes the
@@ -132,6 +135,11 @@ module StandardId
132
135
  skip_sender: delivery_sym == :manual,
133
136
  delivery: delivery_sym
134
137
  )
138
+ rescue StandardId::RetryDelayError => e
139
+ # The resend cooldown (passwordless.retry_delay): a usable code was
140
+ # issued moments ago and nothing new was sent. Distinct code so a
141
+ # caller can say "it's on its way" rather than report a failure.
142
+ return failure_issue_result(:cooldown, e.message)
135
143
  rescue StandardId::RateLimitExceededError => e
136
144
  # A lib-level start limit (rate_limits.otp_start_per_*) tripped.
137
145
  # Distinct code so a caller can answer 429 rather than 400.
@@ -121,8 +121,10 @@ module StandardId
121
121
  return if elapsed >= delay
122
122
 
123
123
  wait = (delay - elapsed).ceil
124
- raise StandardId::InvalidRequestError,
125
- "Please wait #{wait} #{'second'.pluralize(wait)} before requesting another code"
124
+ raise StandardId::RetryDelayError.new(
125
+ "Please wait #{wait} #{'second'.pluralize(wait)} before requesting another code",
126
+ retry_after: wait
127
+ )
126
128
  end
127
129
 
128
130
  def create_challenge!(username, code_length: nil, expires_in: nil, metadata: {})
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.48.0"
2
+ VERSION = "0.48.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.48.0
4
+ version: 0.48.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim