standard_id 0.43.0 → 0.43.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: 8f9798d4a8c4db5cbc69cc0c4f569cc41e825b5df159124fab15f39923ca602f
4
- data.tar.gz: 1b06dc3eee0795f25633df9dc93dae5376c0bc5f6e1b0cd97ef3ce37fca6fc02
3
+ metadata.gz: 8003e8ad18d3a88d9684bb3877d88bbc1d9d207a897776e6ef3a6cd01b0f1191
4
+ data.tar.gz: 3fb388482c096695f36d79ee02e122e1cbede9fc1d11b14c6236a4d423a82a0b
5
5
  SHA512:
6
- metadata.gz: 10594cb5f3206999becca9fb582bfcc714f8371713160192ddf72fdeb39fe4f9552e9d551991fcf294af2eca71fcd8266f8c1f0a27e94c069a7f597581a54d8e
7
- data.tar.gz: 1ababa07f26cea7f6240666372f25ada38a7c3672313306c1ce7eddedded43efaff08b27df71d50c1b78f75e75d6f909f421236c455d1ac9c1658015752bdc4c
6
+ metadata.gz: 2aade7f1bba3cb85827120d4ac19e55b0a2a039955097cd43af6ef75a84f1516080c4e6c0fc36a87d15731bb332603eab987c93130e69e2e2f5701a5ac929053
7
+ data.tar.gz: c0a8bc153ac8db1de5f5186fb014e4ed89e0b5db06ef7e295ee276f032fc59ec39a591eb6e54e440263a22d43faf8b426f6e1acf4ab09fed72870e96c4e17f85
data/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.43.1] - 2026-09-24
11
+
12
+ Fixes found while shipping 0.43.0. **Take this instead of 0.43.0** — hosts on `c.passwordless.delivery = :built_in` with the WebEngine mounted (jumpdrive-web, nutripod-web) would otherwise email email-verification codes as "your sign-in code".
13
+
14
+ ### Fixed
15
+
16
+ - **Verification codes no longer use the sign-in email.** The built-in `PasswordlessDeliverySubscriber` sent `PasswordlessMailer#otp_email` ("Your sign-in code" / "Use the following code to sign in") for every realm, which 0.43.0 made reachable from WebEngine `verify_email/start` (realm `"verification"`). Only the `"authentication"` realm gets `otp_email` now. Every other realm — `verify_email`, `Otp.issue` contact verification, step-up — gets the new **`PasswordlessMailer#verification_email`** ("Your verification code" / "Use the following code to complete your verification"). Both emails state the challenge's actual expiry (the new `expires_in_minutes` param) instead of assuming `code_ttl`. There is still no built-in SMS delivery.
17
+ - **The WebEngine `verify_email` / `verify_phone` start 422 now gives the real reason.** The response body used to be a fixed `"invalid email"` / `"invalid phone"`; it now matches the flash: the format error, the host `username_validator`'s message, or the retry cooldown ("Please wait N seconds before requesting another code").
18
+ - **`PASSWORDLESS_CODE_SENT` no longer claims `delivery_status: "sent"` for codes the engine never delivered.** It was emitted as `"sent"` after every non-manual issue, including when delivery belonged to a host subscriber, or to none at all (`Otp.issue(delivery: :custom)` with no subscriber). It is still emitted in the same cases (so audit trails that record `standard_id.passwordless.*` keep their row), but `delivery_status` is now:
19
+ - `"sent"` — the built-in mailer enqueued the email (`deliver_later` returned a job);
20
+ - `"failed"` — the built-in mailer was responsible but did not enqueue it: the enqueue raised (the subscriber logs the error) or Active Job refused it (`deliver_later` returned `false`);
21
+ - `"delegated"` — a host `PASSWORDLESS_CODE_GENERATED` subscriber is responsible (global `:custom`, `Otp.issue(delivery: :custom)`, or SMS), so the engine cannot confirm a send.
22
+
23
+ Never emitted for `Otp.issue(delivery: :manual)`, as before. None of the five apps subscribes to `PASSWORDLESS_CODE_SENT` or reads `delivery_status`. fundbright-web, luminality-web and nutripod-web audit `standard_id.passwordless.*`, and jumpdrive-web audits all `standard_id.*`, so their stored `passwordless.code.sent` rows will now carry `"delegated"` (fundbright-web, luminality-web) or `"sent"` / `"failed"` (jumpdrive-web, nutripod-web) instead of a blanket `"sent"`.
24
+
25
+ ### Added
26
+
27
+ - **i18n for the built-in OTP emails** (`config/locales/en.yml`): `standard_id.passwordless_mailer.{otp_email,verification_email}.{subject,greeting,intro,expiry,ignore,footer}`. Override any key in the host's locale files. The gem ships only `en`; in any other `I18n.locale` a key the host has not translated falls back to the English copy (no "translation missing", with or without I18n fallbacks enabled). An **assigned** `c.passwordless.mailer_subject` still wins for the sign-in subject (jumpdrive-web sets one), and its unassigned default equals the i18n default, so nothing changes for a host that overrides neither.
28
+ - `PasswordlessDeliverySubscriber.handles?(payload)` — whether the built-in mailer delivers a given `PASSWORDLESS_CODE_GENERATED` payload.
29
+ - `CodeChallenge#built_in_delivered` — a transient flag (not persisted) the built-in subscriber sets once it has enqueued the email.
30
+
31
+ ### Upgrade notes
32
+
33
+ Nothing is required. A host that overrides `app/views/standard_id/passwordless_mailer/otp_email.*` (luminality-web, via its React email build) keeps its sign-in template. With `:built_in` delivery, verification-realm codes now render the gem's `verification_email` template unless the host overrides that one too. luminality-web calls `PasswordlessMailer.with(...).otp_email` directly for its own verification flow; that call is unchanged.
34
+
10
35
  ## [0.43.0] - 2026-09-24
11
36
 
12
37
  **Breaking minor.** Removes everything 0.42 deprecated. Every consumer cleared those warnings in its Phase 3 adoption, so for the five apps the upgrade is a version bump — see **Upgrade notes**.
data/README.md CHANGED
@@ -554,7 +554,14 @@ end
554
554
 
555
555
  Set `c.passwordless.delivery = :custom` (the default) so the engine's built-in
556
556
  `PasswordlessMailer` stays out of the way; with `:built_in` the engine emails
557
- the code itself. The subscriber runs synchronously inside the request, so
557
+ the code itself — `otp_email` ("Your sign-in code") for the `"authentication"`
558
+ realm and `verification_email` ("Your verification code") for every other realm
559
+ (WebEngine `verify_email`, `Otp.issue` contact verification / step-up). There is
560
+ no built-in SMS delivery. The copy is in `config/locales/en.yml` under
561
+ `standard_id.passwordless_mailer.{otp_email,verification_email}.*` (`subject`,
562
+ `greeting`, `intro`, `expiry`, `ignore`, `footer`); override any key in your own
563
+ locale files, or the templates in `app/views/standard_id/passwordless_mailer/`.
564
+ An assigned `c.passwordless.mailer_subject` still sets the sign-in subject. The subscriber runs synchronously inside the request, so
558
565
  `I18n.locale`, `Current.*` and the like are still available.
559
566
 
560
567
  Event payload includes:
@@ -693,7 +700,7 @@ Every StandardId event automatically carries tracing metadata (`event_id`, `time
693
700
  | | `oauth.code.consumed` | `authorization_code`, `client_id`, `account` | After an authorization code is exchanged |
694
701
  | Passwordless | `passwordless.code.requested` | `identifier`, `channel` (email/sms) | Before generating an OTP |
695
702
  | | `passwordless.code.generated` | `code_challenge`, `identifier`, `channel`, `expires_at` | After an OTP is created |
696
- | | `passwordless.code.sent` | `identifier`, `channel`, `delivery_status` | After an OTP is delivered |
703
+ | | `passwordless.code.sent` | `identifier`, `channel`, `realm`, `delivery_status` | After an OTP is handed to delivery (not for `Otp.issue(delivery: :manual)`). `delivery_status`: `"sent"` (built-in mailer enqueued it), `"failed"` (built-in mailer was responsible but did not enqueue), `"delegated"` (a host `PASSWORDLESS_CODE_GENERATED` subscriber delivers — the engine can't confirm it) |
697
704
  | | `passwordless.code.verified` | `code_challenge`, `account`, `channel` | After OTP verification succeeds |
698
705
  | | `passwordless.code.failed` | `identifier`, `channel`, `attempts` | After OTP verification fails |
699
706
  | | `passwordless.account.created` | `account`, `channel`, `identifier` | When an account is created via passwordless flow |
@@ -32,7 +32,7 @@ module StandardId
32
32
  email = params[:email].to_s.strip.downcase
33
33
  if email.blank?
34
34
  flash[:alert] = "Please enter your email address"
35
- render plain: "missing email", status: :unprocessable_content and return
35
+ render plain: flash[:alert], status: :unprocessable_content and return
36
36
  end
37
37
 
38
38
  # Issued through the passwordless strategy (validation, retry delay,
@@ -47,8 +47,11 @@ module StandardId
47
47
  expires_in: 10.minutes
48
48
  )
49
49
  unless result.success?
50
+ # The body carries the same reason as the flash: a malformed
51
+ # target, the host's username_validator message, or the retry
52
+ # cooldown ("Please wait N seconds ...").
50
53
  flash[:alert] = result.error_message
51
- render plain: "invalid email", status: :unprocessable_content and return
54
+ render plain: result.error_message, status: :unprocessable_content and return
52
55
  end
53
56
 
54
57
  redirect_to standard_id_web.login_path, notice: "Verification code sent to your email", status: :see_other
@@ -32,7 +32,7 @@ module StandardId
32
32
  phone = params[:phone_number].to_s.strip
33
33
  if phone.blank? || !(phone.match?(/\A\+?[1-9]\d{1,14}\z/))
34
34
  flash[:alert] = "Please enter a valid phone number"
35
- render plain: "invalid phone", status: :unprocessable_content and return
35
+ render plain: flash[:alert], status: :unprocessable_content and return
36
36
  end
37
37
 
38
38
  # Issued through the passwordless strategy (validation, retry delay,
@@ -47,8 +47,11 @@ module StandardId
47
47
  expires_in: 10.minutes
48
48
  )
49
49
  unless result.success?
50
+ # The body carries the same reason as the flash: a malformed
51
+ # target, the host's username_validator message, or the retry
52
+ # cooldown ("Please wait N seconds ...").
50
53
  flash[:alert] = result.error_message
51
- render plain: "invalid phone", status: :unprocessable_content and return
54
+ render plain: result.error_message, status: :unprocessable_content and return
52
55
  end
53
56
 
54
57
  redirect_to standard_id_web.login_path, notice: "Verification code sent via SMS", status: :see_other
@@ -1,16 +1,72 @@
1
1
  module StandardId
2
+ # Built-in OTP emails, sent by Events::Subscribers::PasswordlessDeliverySubscriber
3
+ # when `c.passwordless.delivery = :built_in`.
4
+ #
5
+ # * #otp_email — the sign-in code (realm "authentication").
6
+ # * #verification_email — every other realm: WebEngine verify_email/start
7
+ # (realm "verification") and `Otp.issue(realm: ...)` for contact
8
+ # verification, step-up and the like. It must not tell the recipient the
9
+ # code is for signing in.
10
+ #
11
+ # Copy lives under `standard_id.passwordless_mailer.<action>.*` in
12
+ # config/locales/en.yml; override any key in the host's locale files, or
13
+ # override the templates in app/views/standard_id/passwordless_mailer/.
14
+ # `c.passwordless.mailer_subject`, when assigned, still sets the sign-in
15
+ # subject (it wins over the i18n key).
2
16
  class PasswordlessMailer < ApplicationMailer
3
17
  layout false
4
18
 
19
+ helper_method :otp_copy
20
+
21
+ # @param email [String]
22
+ # @param otp_code [String]
23
+ # @param expires_in_minutes [Integer, nil] defaults to passwordless.code_ttl
5
24
  def otp_email
6
- @otp_code = params[:otp_code]
7
- @email = params[:email]
25
+ assign_otp_params
26
+ subject = if StandardId.config.passwordless.assigned?(:mailer_subject)
27
+ StandardId.config.passwordless.mailer_subject
28
+ else
29
+ otp_copy(:subject, default: StandardId.config.passwordless.mailer_subject)
30
+ end
31
+
32
+ mail(to: @email, from: StandardId.config.passwordless.mailer_from, subject: subject)
33
+ end
34
+
35
+ # @param email [String]
36
+ # @param otp_code [String]
37
+ # @param realm [String, nil] the OTP realm, available to overriding templates
38
+ # @param expires_in_minutes [Integer, nil] defaults to passwordless.code_ttl
39
+ def verification_email
40
+ assign_otp_params
41
+ @realm = params[:realm]
8
42
 
9
43
  mail(
10
44
  to: @email,
11
45
  from: StandardId.config.passwordless.mailer_from,
12
- subject: StandardId.config.passwordless.mailer_subject
46
+ subject: otp_copy(:subject)
13
47
  )
14
48
  end
49
+
50
+ # Copy for the current action from
51
+ # standard_id.passwordless_mailer.<action>.<key>, in I18n.locale. Falls
52
+ # back to the gem's English copy when the host has no translation for
53
+ # that locale (the gem ships only `en`, and a host need not enable
54
+ # I18n fallbacks), so a zh-SG request never renders "translation missing".
55
+ #
56
+ # @param key [Symbol, String]
57
+ # @param default [String, nil] final fallback when even `en` has no key
58
+ def otp_copy(key, default: nil, **options)
59
+ scope = "standard_id.passwordless_mailer.#{action_name}"
60
+ english = I18n.t(key, scope: scope, locale: :en, default: default, **options)
61
+ I18n.t(key, scope: scope, default: english, **options)
62
+ end
63
+
64
+ private
65
+
66
+ def assign_otp_params
67
+ @otp_code = params[:otp_code]
68
+ @email = params[:email]
69
+ @expires_in_minutes = params[:expires_in_minutes] || (StandardId.config.passwordless.code_ttl / 60)
70
+ end
15
71
  end
16
72
  end
@@ -1,5 +1,9 @@
1
1
  module StandardId
2
2
  class CodeChallenge < ApplicationRecord
3
+ # Transient (not persisted): set by the built-in PasswordlessDeliverySubscriber
4
+ # once it has enqueued the email, so the passwordless strategy can report
5
+ # PASSWORDLESS_CODE_SENT truthfully.
6
+ attr_accessor :built_in_delivered
3
7
  self.table_name = "standard_id_code_challenges"
4
8
 
5
9
  # Well-known realms used by the engine itself. Host apps may create
@@ -11,13 +11,13 @@
11
11
  </head>
12
12
  <body>
13
13
  <div class="container">
14
- <p>Hi,</p>
15
- <p>Use the following code to sign in:</p>
14
+ <p><%= otp_copy(:greeting) %></p>
15
+ <p><%= otp_copy(:intro) %></p>
16
16
  <div class="code"><%= @otp_code %></div>
17
- <p>This code will expire in <%= StandardId.config.passwordless.code_ttl / 60 %> minutes.</p>
18
- <p>If you did not request this code, you can safely ignore this email.</p>
17
+ <p><%= otp_copy(:expiry, count: @expires_in_minutes) %></p>
18
+ <p><%= otp_copy(:ignore) %></p>
19
19
  <div class="footer">
20
- <p>This is an automated message. Please do not reply.</p>
20
+ <p><%= otp_copy(:footer) %></p>
21
21
  </div>
22
22
  </div>
23
23
  </body>
@@ -1,12 +1,12 @@
1
- Hi,
1
+ <%= otp_copy(:greeting) %>
2
2
 
3
- Use the following code to sign in:
3
+ <%= otp_copy(:intro) %>
4
4
 
5
5
  <%= @otp_code %>
6
6
 
7
- This code will expire in <%= StandardId.config.passwordless.code_ttl / 60 %> minutes.
7
+ <%= otp_copy(:expiry, count: @expires_in_minutes) %>
8
8
 
9
- If you did not request this code, you can safely ignore this email.
9
+ <%= otp_copy(:ignore) %>
10
10
 
11
11
  --
12
- This is an automated message. Please do not reply.
12
+ <%= otp_copy(:footer) %>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 0; }
7
+ .container { max-width: 480px; margin: 40px auto; background-color: #ffffff; border-radius: 8px; padding: 40px; }
8
+ .code { font-size: 32px; font-weight: bold; letter-spacing: 8px; text-align: center; padding: 20px; background-color: #f0f0f0; border-radius: 6px; margin: 24px 0; font-family: monospace; }
9
+ .footer { margin-top: 32px; font-size: 13px; color: #888888; text-align: center; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div class="container">
14
+ <p><%= otp_copy(:greeting) %></p>
15
+ <p><%= otp_copy(:intro) %></p>
16
+ <div class="code"><%= @otp_code %></div>
17
+ <p><%= otp_copy(:expiry, count: @expires_in_minutes) %></p>
18
+ <p><%= otp_copy(:ignore) %></p>
19
+ <div class="footer">
20
+ <p><%= otp_copy(:footer) %></p>
21
+ </div>
22
+ </div>
23
+ </body>
24
+ </html>
@@ -0,0 +1,12 @@
1
+ <%= otp_copy(:greeting) %>
2
+
3
+ <%= otp_copy(:intro) %>
4
+
5
+ <%= @otp_code %>
6
+
7
+ <%= otp_copy(:expiry, count: @expires_in_minutes) %>
8
+
9
+ <%= otp_copy(:ignore) %>
10
+
11
+ --
12
+ <%= otp_copy(:footer) %>
@@ -0,0 +1,22 @@
1
+ en:
2
+ standard_id:
3
+ passwordless_mailer:
4
+ otp_email:
5
+ # Used only when c.passwordless.mailer_subject is not assigned.
6
+ subject: "Your sign-in code"
7
+ greeting: "Hi,"
8
+ intro: "Use the following code to sign in:"
9
+ expiry:
10
+ one: "This code will expire in %{count} minute."
11
+ other: "This code will expire in %{count} minutes."
12
+ ignore: "If you did not request this code, you can safely ignore this email."
13
+ footer: "This is an automated message. Please do not reply."
14
+ verification_email:
15
+ subject: "Your verification code"
16
+ greeting: "Hi,"
17
+ intro: "Use the following code to complete your verification:"
18
+ expiry:
19
+ one: "This code will expire in %{count} minute."
20
+ other: "This code will expire in %{count} minutes."
21
+ ignore: "If you did not request this code, you can safely ignore this email."
22
+ footer: "This is an automated message. Please do not reply."
@@ -1,31 +1,56 @@
1
1
  module StandardId
2
2
  module Events
3
3
  module Subscribers
4
+ # Built-in OTP email delivery (`c.passwordless.delivery = :built_in`).
5
+ #
6
+ # Sign-in codes (realm "authentication") get PasswordlessMailer#otp_email;
7
+ # every other realm — WebEngine email verification, Otp.issue step-up /
8
+ # contact verification — gets #verification_email, so a verification code
9
+ # never arrives as "your sign-in code". There is no built-in SMS delivery.
10
+ #
11
+ # On a successful enqueue it marks the code challenge
12
+ # (CodeChallenge#built_in_delivered), which is how the strategy knows to
13
+ # report PASSWORDLESS_CODE_SENT as "sent" rather than "failed".
4
14
  class PasswordlessDeliverySubscriber < Base
5
15
  subscribe_to StandardId::Events::PASSWORDLESS_CODE_GENERATED
6
16
 
17
+ # Whether the built-in mailer is responsible for delivering this
18
+ # PASSWORDLESS_CODE_GENERATED payload.
19
+ #
20
+ # @param payload [Hash] event payload (symbol or string keys)
21
+ # @return [Boolean]
22
+ def self.handles?(payload)
23
+ payload = payload.with_indifferent_access
24
+ # Per-call manual delivery (Otp.issue(delivery: :manual)) and
25
+ # Otp.issue(delivery: :custom) leave delivery to the caller / host
26
+ # subscriber, even when the engine mailer is the global default.
27
+ return false if payload[:skip_sender]
28
+ return false if payload[:delivery]&.to_sym == :custom
29
+
30
+ StandardId.config.passwordless.delivery == :built_in && payload[:channel].to_s == "email"
31
+ end
32
+
7
33
  def call(event)
8
- # Per-call manual-delivery request takes precedence over the global
9
- # delivery config. Honors Otp.issue(delivery: :manual) callers who
10
- # deliver the code themselves (custom widget flows, step-up
11
- # challenges, etc.) and would otherwise receive a duplicate email
12
- # from this subscriber when c.passwordless.delivery == :built_in.
13
- return if event[:skip_sender]
14
- # Otp.issue(delivery: :custom): the host's own subscriber delivers
15
- # this code, even where the engine mailer is the global default.
16
- return if event[:delivery]&.to_sym == :custom
17
- return unless built_in_delivery?
18
- return unless event[:channel] == "email"
34
+ return unless self.class.handles?(event.payload)
19
35
 
20
36
  identifier = event[:identifier]
21
- code = event[:code_challenge]&.code
37
+ challenge = event[:code_challenge]
38
+ code = challenge&.code
22
39
 
23
40
  return if identifier.blank? || code.blank?
24
41
 
25
- StandardId::PasswordlessMailer.with(
42
+ enqueued = StandardId::PasswordlessMailer.with(
26
43
  email: identifier,
27
- otp_code: code
28
- ).otp_email.deliver_later
44
+ otp_code: code,
45
+ realm: event[:realm],
46
+ expires_in_minutes: expires_in_minutes(event[:expires_at])
47
+ ).public_send(mailer_action(event[:realm])).deliver_later
48
+
49
+ # deliver_later returns false (without raising) when Active Job
50
+ # refuses the enqueue, e.g. an aborting before_enqueue callback.
51
+ if enqueued && challenge.respond_to?(:built_in_delivered=)
52
+ challenge.built_in_delivered = true
53
+ end
29
54
  end
30
55
 
31
56
  def handle_error(error, event)
@@ -37,8 +62,17 @@ module StandardId
37
62
 
38
63
  private
39
64
 
40
- def built_in_delivery?
41
- StandardId.config.passwordless.delivery == :built_in
65
+ def mailer_action(realm)
66
+ realm.blank? || realm.to_s == StandardId::Passwordless::DEFAULT_REALM ? :otp_email : :verification_email
67
+ end
68
+
69
+ def expires_in_minutes(expires_at)
70
+ return nil if expires_at.blank?
71
+
72
+ expires_at = Time.zone.parse(expires_at) if expires_at.is_a?(String)
73
+ [((expires_at - Time.current) / 60.0).ceil, 1].max
74
+ rescue ArgumentError, TypeError, NoMethodError
75
+ nil
42
76
  end
43
77
  end
44
78
  end
@@ -45,8 +45,8 @@ module StandardId
45
45
  # can honor a per-call manual-delivery request. Without this,
46
46
  # Otp.issue(delivery: :manual) silently double-delivers when
47
47
  # c.passwordless.delivery == :built_in.
48
- emit_code_generated(challenge, username, skip_sender: skip_sender, delivery: delivery)
49
- emit_code_sent(username) unless skip_sender
48
+ generated_payload = emit_code_generated(challenge, username, skip_sender: skip_sender, delivery: delivery)
49
+ emit_code_sent(username, delivery_status: delivery_status_for(challenge, generated_payload)) unless skip_sender
50
50
  challenge
51
51
  end
52
52
 
@@ -190,9 +190,9 @@ module StandardId
190
190
  )
191
191
  end
192
192
 
193
+ # @return [Hash] the published payload (used to decide the delivery status)
193
194
  def emit_code_generated(challenge, username, skip_sender: false, delivery: nil)
194
- StandardId::Events.publish(
195
- StandardId::Events::PASSWORDLESS_CODE_GENERATED,
195
+ payload = {
196
196
  code_challenge: challenge,
197
197
  identifier: username,
198
198
  channel: connection_type,
@@ -200,16 +200,31 @@ module StandardId
200
200
  expires_at: challenge.expires_at,
201
201
  skip_sender: skip_sender,
202
202
  delivery: delivery
203
- )
203
+ }
204
+ StandardId::Events.publish(StandardId::Events::PASSWORDLESS_CODE_GENERATED, payload)
205
+ payload
206
+ end
207
+
208
+ # delivery_status of PASSWORDLESS_CODE_SENT:
209
+ # - "sent" — the built-in mailer (delivery :built_in) enqueued the email.
210
+ # - "failed" — the built-in mailer was responsible but did not enqueue
211
+ # it (its error was logged by the subscriber).
212
+ # - "delegated" — delivery belongs to the host's PASSWORDLESS_CODE_GENERATED
213
+ # subscriber (delivery :custom, or SMS); the engine cannot
214
+ # know whether it sent anything.
215
+ def delivery_status_for(challenge, generated_payload)
216
+ return "delegated" unless StandardId::Events::Subscribers::PasswordlessDeliverySubscriber.handles?(generated_payload)
217
+
218
+ challenge.built_in_delivered ? "sent" : "failed"
204
219
  end
205
220
 
206
- def emit_code_sent(username)
221
+ def emit_code_sent(username, delivery_status:)
207
222
  StandardId::Events.publish(
208
223
  StandardId::Events::PASSWORDLESS_CODE_SENT,
209
224
  identifier: username,
210
225
  channel: connection_type,
211
226
  realm: @realm,
212
- delivery_status: "sent"
227
+ delivery_status: delivery_status
213
228
  )
214
229
  end
215
230
  end
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.43.0"
2
+ VERSION = "0.43.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.43.0
4
+ version: 0.43.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -200,6 +200,8 @@ files:
200
200
  - app/views/standard_id/password_reset_mailer/reset_email.text.erb
201
201
  - app/views/standard_id/passwordless_mailer/otp_email.html.erb
202
202
  - app/views/standard_id/passwordless_mailer/otp_email.text.erb
203
+ - app/views/standard_id/passwordless_mailer/verification_email.html.erb
204
+ - app/views/standard_id/passwordless_mailer/verification_email.text.erb
203
205
  - app/views/standard_id/web/account/edit.html.erb
204
206
  - app/views/standard_id/web/account/show.html.erb
205
207
  - app/views/standard_id/web/auth/callback/providers/mobile_callback.html.erb
@@ -214,6 +216,7 @@ files:
214
216
  - app/views/standard_id/web/signup/show.html.erb
215
217
  - config/brakeman.ignore
216
218
  - config/initializers/generators.rb
219
+ - config/locales/en.yml
217
220
  - config/routes/api.rb
218
221
  - config/routes/web.rb
219
222
  - db/migrate/20250830000000_create_standard_id_client_applications.rb