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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +6 -1
- data/lib/standard_id/errors.rb +15 -0
- data/lib/standard_id/otp.rb +9 -1
- data/lib/standard_id/passwordless/base_strategy.rb +4 -2
- data/lib/standard_id/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdf22579e98c4ceb592cb20766d510d937d0e5d2f7f70c41074f984ddc3b0393
|
|
4
|
+
data.tar.gz: 35fbb21a24d40a307bd55279a86fa0d91549641e30f30d4101d667d9c5d7c793
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
data/lib/standard_id/errors.rb
CHANGED
|
@@ -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
|
data/lib/standard_id/otp.rb
CHANGED
|
@@ -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::
|
|
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: {})
|
data/lib/standard_id/version.rb
CHANGED