standard_id 0.39.0 → 0.40.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 +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/standard_id/current_attributes.rb +4 -0
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id/web/session_manager.rb +75 -6
- 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: 89d297a5e9c69566991b97cca501fd5a714b2d2e1200d2e740b3e45684981aab
|
|
4
|
+
data.tar.gz: d93fc7206735373f14ac2cd51c4612aaf7b1d19244022d884f0c84757d8f78c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ce536a136af167e65ec9d01e370a187d71f77a2f60f0c5af47ee5c23a9b58cb87444b95191a019fa06689902765f51eaff20656aeedcdef288e34f577c99c91
|
|
7
|
+
data.tar.gz: f825db9970c937a76592c421fb71a9607f3ea0667dfafa105ff2d6ce78bbd754f5ca01f873cf3321962876c3ab0f8c7f7314ec95604cf5900d56291cede6ec57
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.40.0] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`sign_in_account` now supersedes a nil memoised earlier in the same request.** With the `session_resolved` / `account_resolved` memo introduced in the entry below, a guard or shared prop that read `current_account` before the sign-in action ran would have kept returning nil for the rest of that request. Sign-in now memoises the new session and resets the account memo, so the next `current_account` re-derives the account through `load_current_account` — `config.account_scope` and `strict_loading!(false)` apply exactly as on an ordinary authenticated request (caught in review of #326 and #327).
|
|
15
|
+
|
|
16
|
+
- **An anonymous request no longer queries the session table on every `current_account` call, and no longer writes an empty Rails session.** `Web::SessionManager#current_session` memoised with `Current.session ||= …`, which never memoises a nil answer, so a visitor with no session paid the `BrowserSession.by_token(nil)` lookup on every read — shared props, guards, locale selection, nav helpers — five or six times per page. On one consumer's marketing homepage that was ~6 session-table queries per anonymous request and the app's single largest query by total time (fundbright/delivery-ops#598). Worse, the "no session" branch called `clear_session!`, whose `session.delete` loads the Rack session, and Rack persists every loaded session: a new empty session row and a `Set-Cookie` on every anonymous response, which is what stops a CDN from caching a public page.
|
|
17
|
+
|
|
18
|
+
Two new `Current` attributes, `session_resolved` and `account_resolved`, record that the question was answered (nil included) for the rest of the request. `load_session_from_session_token` returns without a query when there is no token, and only falls back to `session[:session_token]` when the Rails session is already loaded or its cookie is actually present on the request (`request.session_options[:key]`) — so a bare GET never allocates one. `load_session_from_remember_token` returns early without a `remember_token` cookie. `clear_session!` only touches the Rails session when it is loaded, and the no-session branch only clears at all when there is stale state to clear (a `session_token`/`remember_token` cookie, or session keys). Hosts that include `StandardId::CurrentAttributes` get the new attributes automatically; a host that hand-rolls `Current` must add them.
|
|
19
|
+
|
|
10
20
|
## [0.39.0] - 2026-08-10
|
|
11
21
|
|
|
12
22
|
### Added
|
|
@@ -4,6 +4,10 @@ module StandardId
|
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
6
|
attribute :session, :account, :request_id, :ip_address, :user_agent, :scope
|
|
7
|
+
# Set once Web::SessionManager has answered "which session / account is
|
|
8
|
+
# this request?" — including when the answer is nil — so the lookup is
|
|
9
|
+
# never repeated within a request. Reset with the rest of Current.
|
|
10
|
+
attribute :session_resolved, :account_resolved
|
|
7
11
|
end
|
|
8
12
|
end
|
|
9
13
|
end
|
data/lib/standard_id/version.rb
CHANGED
|
@@ -11,12 +11,25 @@ module StandardId
|
|
|
11
11
|
@reset_session = reset_session
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Both readers memoise their answer for the rest of the request — including
|
|
15
|
+
# a nil answer. `Current.session ||= …` never did: an anonymous visitor has
|
|
16
|
+
# no session, so every `current_account` call (shared props, before_actions,
|
|
17
|
+
# locale selection, nav helpers — five or six per page) re-ran the lookup.
|
|
18
|
+
# On one consumer's marketing homepage that was ~6 session-table queries
|
|
19
|
+
# per anonymous request, the app's single largest query by total time
|
|
20
|
+
# (fundbright/delivery-ops#598).
|
|
14
21
|
def current_session
|
|
15
|
-
Current.session
|
|
22
|
+
return Current.session if Current.session_resolved
|
|
23
|
+
|
|
24
|
+
Current.session_resolved = true
|
|
25
|
+
load_current_session
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
def current_account
|
|
19
|
-
Current.account
|
|
29
|
+
return Current.account if Current.account_resolved
|
|
30
|
+
|
|
31
|
+
Current.account_resolved = true
|
|
32
|
+
Current.account = load_current_account
|
|
20
33
|
end
|
|
21
34
|
|
|
22
35
|
def sign_in_account(account, scope_name: nil)
|
|
@@ -41,7 +54,18 @@ module StandardId
|
|
|
41
54
|
scopes << scope_name.to_s unless scopes.include?(scope_name.to_s)
|
|
42
55
|
session[:standard_id_scopes] = scopes
|
|
43
56
|
end
|
|
57
|
+
# Sign-in supersedes whatever this request already resolved: a guard
|
|
58
|
+
# or shared prop that asked `current_account` before the sign-in
|
|
59
|
+
# action ran memoised nil, and that memo must not outlive the sign-in.
|
|
60
|
+
# The session is known here, so memoise it; the account is NOT
|
|
61
|
+
# assigned directly — the memo is reset so the next `current_account`
|
|
62
|
+
# re-derives it through load_current_account, which applies
|
|
63
|
+
# `config.account_scope` and `strict_loading!(false)` exactly as an
|
|
64
|
+
# ordinary authenticated request would (review of #327).
|
|
44
65
|
Current.session = browser_session
|
|
66
|
+
Current.session_resolved = true
|
|
67
|
+
Current.account = nil
|
|
68
|
+
Current.account_resolved = false
|
|
45
69
|
emit_session_created(browser_session, account, "browser")
|
|
46
70
|
end
|
|
47
71
|
end
|
|
@@ -61,8 +85,18 @@ module StandardId
|
|
|
61
85
|
|
|
62
86
|
def clear_session!
|
|
63
87
|
# TODO: make token key names configurable
|
|
64
|
-
|
|
65
|
-
session.
|
|
88
|
+
#
|
|
89
|
+
# Only touch the Rails session when it is already loaded. Reading or
|
|
90
|
+
# deleting a key on an unloaded Rack session loads it, and Rack commits
|
|
91
|
+
# every loaded session on the way out — so on an anonymous request this
|
|
92
|
+
# used to write a brand-new empty session to the store and stamp a
|
|
93
|
+
# session cookie on the response, which is what stops any CDN from
|
|
94
|
+
# caching a public page. If the session was never loaded there is nothing
|
|
95
|
+
# in it to clear.
|
|
96
|
+
if rails_session_loaded?
|
|
97
|
+
session.delete(:session_token)
|
|
98
|
+
session.delete(:standard_id_scopes)
|
|
99
|
+
end
|
|
66
100
|
# Delete the cookie outright. Assigning `cookies.encrypted[:session_token] = nil`
|
|
67
101
|
# writes a fresh encrypted blob through the jar's default options (no httponly)
|
|
68
102
|
# on every unauthenticated request, leaving a confusing non-HttpOnly
|
|
@@ -72,10 +106,32 @@ module StandardId
|
|
|
72
106
|
cookies.delete(:remember_token)
|
|
73
107
|
|
|
74
108
|
Current.session = nil
|
|
109
|
+
Current.account = nil
|
|
110
|
+
Current.session_resolved = true
|
|
111
|
+
Current.account_resolved = true
|
|
75
112
|
end
|
|
76
113
|
|
|
77
114
|
private
|
|
78
115
|
|
|
116
|
+
# A Rack session reports `loaded?`; the plain Hash the specs (and some
|
|
117
|
+
# hosts' test doubles) hand in does not, and a Hash is always "loaded".
|
|
118
|
+
def rails_session_loaded?
|
|
119
|
+
!session.respond_to?(:loaded?) || session.loaded?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Whether the request carries the host app's Rails session cookie at all.
|
|
123
|
+
# Reading `session[...]` when it does not would make Rack allocate and
|
|
124
|
+
# then persist an empty session (see clear_session!). Without a request
|
|
125
|
+
# that can answer, assume it does — the old behaviour.
|
|
126
|
+
def rails_session_cookie_present?
|
|
127
|
+
return true unless request.respond_to?(:session_options) && request.respond_to?(:cookies)
|
|
128
|
+
|
|
129
|
+
key = request.session_options[:key]
|
|
130
|
+
return true if key.blank?
|
|
131
|
+
|
|
132
|
+
request.cookies.key?(key.to_s)
|
|
133
|
+
end
|
|
134
|
+
|
|
79
135
|
# Persist the session token in an encrypted cookie whose lifetime matches
|
|
80
136
|
# the DB session's expires_at, so an authenticated session survives a full
|
|
81
137
|
# browser restart (a bare session cookie would be cleared on browser close,
|
|
@@ -117,19 +173,32 @@ module StandardId
|
|
|
117
173
|
clear_session!
|
|
118
174
|
end
|
|
119
175
|
else
|
|
120
|
-
|
|
176
|
+
# Nothing identified a session. Clear stale state only when there is
|
|
177
|
+
# some — a bare anonymous GET has no cookies to delete and no session
|
|
178
|
+
# to touch, and touching it is what makes the response uncacheable.
|
|
179
|
+
clear_session! if stale_session_state?
|
|
121
180
|
end
|
|
122
181
|
|
|
123
182
|
Current.session
|
|
124
183
|
end
|
|
125
184
|
|
|
185
|
+
def stale_session_state?
|
|
186
|
+
cookies.encrypted[:session_token].present? || cookies[:session_token].present? || cookies[:remember_token].present? ||
|
|
187
|
+
(rails_session_loaded? && (session[:session_token].present? || session[:standard_id_scopes].present?))
|
|
188
|
+
end
|
|
189
|
+
|
|
126
190
|
def load_session_from_session_token
|
|
127
191
|
# Try encrypted cookie first (for Action Cable), then fall back to session (for backward compatibility)
|
|
128
|
-
session_token = cookies.encrypted[:session_token]
|
|
192
|
+
session_token = cookies.encrypted[:session_token]
|
|
193
|
+
session_token ||= session[:session_token] if rails_session_loaded? || rails_session_cookie_present?
|
|
194
|
+
return if session_token.blank?
|
|
195
|
+
|
|
129
196
|
StandardId::BrowserSession.eager_load(:account).by_token(session_token).first
|
|
130
197
|
end
|
|
131
198
|
|
|
132
199
|
def load_session_from_remember_token
|
|
200
|
+
return if cookies[:remember_token].blank?
|
|
201
|
+
|
|
133
202
|
password_credential = StandardId::PasswordCredential.find_by_token_for(:remember_me, cookies[:remember_token])
|
|
134
203
|
return if password_credential.blank?
|
|
135
204
|
|