standard_id 0.56.0 → 0.57.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: 5d21b27586542e7f6f9bc312ace8fed7a75a8b75c4633e470232890b869f802f
4
- data.tar.gz: 46b0df7981fa6785cdbc86a0ea65e52bf30c6c293a6198671d43169dd82fb955
3
+ metadata.gz: c50961cc2c4446bd495efdebca4ad54a871c371fab3c63e0ff1dd6792b44e1ff
4
+ data.tar.gz: be9ebdaedf84eedfed63f98ade1e8fb37bfc90112b7ba4532aa8d179aefb03b9
5
5
  SHA512:
6
- metadata.gz: c54196984851c658850c910a1509ffbc4231911759a0f003d9f3915002459ca5d313a61643abdd43edeac85d332ef8bc1c4173d65e9ca3fe2f747f44aff93e14
7
- data.tar.gz: 44510185f41ecd77b81c1d44342e5e6b2e432c75232c91cd3347ddea25fa3115d370e05d0af2d54a878a2dbf38cc5888bd0bdb82de12d9d1888b9df60474c10a
6
+ metadata.gz: 55f8de3f738e3363e452842bcea0b68fbff7798e59039bdac698de7b0b5d1c1fffdb58fe693e60671aa6c9cc2743059fc3d3269e11bde54a84d7bbb23e64b8f5
7
+ data.tar.gz: 9c0e08fdb33cc59058c5be1be6f65b7308bb497738a8a338de1072e5cbd105f59417cbd65c4902e6f937ff53544bbd755cf7e8b8e349b2bd058583cc75eaa2bc
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.57.0] - 2026-10-28
11
+
12
+ ### Added
13
+
14
+ - **Migration `20261028000000_add_value_lookup_indexes_to_standard_id_tables`.** Adds a
15
+ `[type, value]` index on `standard_id_identifiers`, which serves the `find_by(value:)` lookup behind
16
+ every sign-in, OTP start, social login and password reset (the only index containing `value` led
17
+ with `account_id`, so each lookup was a sequential scan). Also adds a partial
18
+ `client_application_id` index on `standard_id_refresh_tokens` for `PurgeStaleDynamicClientsJob`.
19
+ Both are built `CONCURRENTLY` on PostgreSQL and are idempotent. Run
20
+ `bin/rails standard_id:install:migrations && bin/rails db:migrate`.
21
+
22
+ ## [0.56.1] - 2026-10-27
23
+
24
+ ### Fixed
25
+
26
+ - **`credential.password.created` from the API email + password sign-up now carries a usable
27
+ credential chain.** `EmailPassword::Registration` created the `Credential` row with
28
+ `Credential.create!`, but `PasswordCredential`'s `has_one :credential, touch: true` had already
29
+ read (and cached) the association as `nil` in `after_create`. A subscriber walking
30
+ `event[:credential].credential.identifier.account` got `nil`; it now gets the account, with the
31
+ whole chain loaded in memory, so strict-loading hosts never load it lazily.
32
+ `Web::SignupForm` was unaffected (it builds the chain with nested attributes); it now has the
33
+ same regression spec.
34
+
10
35
  ## [0.56.0] - 2026-10-26
11
36
 
12
37
  ### Added
@@ -0,0 +1,73 @@
1
+ class AddValueLookupIndexesToStandardIdTables < ActiveRecord::Migration[8.0]
2
+ # Indexes for two lookups nothing in the schema serves (deep audit
3
+ # DBP-002/DBP-011, 2026-10-27):
4
+ #
5
+ # - standard_id_identifiers [type, value]: every sign-in, OTP start, social
6
+ # login and password reset resolves its identifier with
7
+ # `EmailIdentifier.find_by(value:)` / `PhoneNumberIdentifier.find_by(value:)`
8
+ # (STI adds `type = ?`). The only index containing `value` leads with
9
+ # account_id, so each lookup is a sequential scan. Not unique: the same
10
+ # value may belong to more than one account.
11
+ # - standard_id_refresh_tokens client_application_id: PurgeStaleDynamicClientsJob
12
+ # reads refresh tokens per client (`client.refresh_tokens`) and by
13
+ # `client_application_id IS NOT NULL`. Partial, because tokens not bound
14
+ # to an application (social sign-in refreshes, rows minted before
15
+ # 20261020000000) leave it NULL.
16
+ #
17
+ # At today's table sizes these scans are cheap (Sentry, 14 days: the
18
+ # identifier lookup is p50 2-4ms, p95 9-15ms in every host); the cost grows
19
+ # with every account. Idempotent (if_not_exists / if_exists), CONCURRENTLY
20
+ # on Postgres, and an INVALID index left by a failed concurrent build is
21
+ # dropped and rebuilt, same conventions as 20260924000000.
22
+ disable_ddl_transaction!
23
+
24
+ INDEXES = [
25
+ {
26
+ table: :standard_id_identifiers,
27
+ columns: [:type, :value],
28
+ name: "index_standard_id_identifiers_on_type_and_value"
29
+ },
30
+ {
31
+ table: :standard_id_refresh_tokens,
32
+ columns: :client_application_id,
33
+ name: "index_standard_id_refresh_tokens_on_client_application_id",
34
+ where: "client_application_id IS NOT NULL"
35
+ }
36
+ ].freeze
37
+
38
+ def up
39
+ INDEXES.each do |index|
40
+ if postgres? && invalid_postgres_index?(index[:name])
41
+ remove_index index[:table], name: index[:name], if_exists: true, **concurrent
42
+ end
43
+
44
+ add_index index[:table], index[:columns],
45
+ name: index[:name], where: index[:where], if_not_exists: true, **concurrent
46
+ end
47
+ end
48
+
49
+ def down
50
+ INDEXES.each do |index|
51
+ remove_index index[:table], name: index[:name], if_exists: true, **concurrent
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def postgres?
58
+ connection.adapter_name.downcase.include?("postgres")
59
+ end
60
+
61
+ def concurrent
62
+ postgres? ? { algorithm: :concurrently } : {}
63
+ end
64
+
65
+ def invalid_postgres_index?(name)
66
+ select_value(<<~SQL.squish).present?
67
+ SELECT 1
68
+ FROM pg_index i
69
+ INNER JOIN pg_class c ON c.oid = i.indexrelid
70
+ WHERE c.relname = #{connection.quote(name)} AND NOT i.indisvalid
71
+ SQL
72
+ end
73
+ end
@@ -105,7 +105,13 @@ module StandardId
105
105
  account: account, value: email, signup_source: EmailPassword::SIGNUP_SOURCE
106
106
  )
107
107
  password_credential = StandardId::PasswordCredential.create!(login: email, password: password)
108
- StandardId::Credential.create!(identifier: identifier, credentialable: password_credential)
108
+ # Created through the association, not Credential.create!: the
109
+ # has_one's touch: true reads password_credential.credential in
110
+ # after_create, caching nil, so a subscriber walking
111
+ # credential.identifier.account would get nil. Going through the
112
+ # association leaves the whole chain loaded in memory, which also
113
+ # keeps strict-loading hosts from lazily loading identifier/account.
114
+ password_credential.create_credential!(identifier: identifier)
109
115
 
110
116
  StandardId::Events.publish(
111
117
  StandardId::Events::ACCOUNT_CREATED,
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.56.0"
2
+ VERSION = "0.57.0"
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.56.0
4
+ version: 0.57.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -249,6 +249,7 @@ files:
249
249
  - db/migrate/20261020000000_add_client_application_id_to_standard_id_refresh_tokens.rb
250
250
  - db/migrate/20261020120000_add_registration_access_token_digest_to_standard_id_client_applications.rb
251
251
  - db/migrate/20261022000000_add_signup_source_to_standard_id_identifiers.rb
252
+ - db/migrate/20261028000000_add_value_lookup_indexes_to_standard_id_tables.rb
252
253
  - lib/generators/standard_id/install/install_generator.rb
253
254
  - lib/generators/standard_id/install/templates/standard_id.rb
254
255
  - lib/generators/standard_id/install/templates/standard_id_reference.rb