standard_id 0.36.2 → 0.37.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: e6ea917e3795560901ae1213d21a172e84fb836ecd6885f96f99c138291ffd40
4
- data.tar.gz: e4582d67367ee6026bc058ffb60a17ee729fb23635318c3acec42a153b0d2b19
3
+ metadata.gz: 26e62c374edcaa53ab63756178e869a2d0c54c7ab00d26c2da823b28867fbeb6
4
+ data.tar.gz: 1a9a4e63f3e7f0e603c771c78f9fe503aad2768ea6302f675c360056f9d7c8d7
5
5
  SHA512:
6
- metadata.gz: b00ae747d7337e330bf1187f600d8a979c345ea64b72a438657ee28c37d0e685ef00205f4db5726532be0f8db04c3c6386ef98322420ba290999d63b80f9ff80
7
- data.tar.gz: d7c7f079b61c66c65f7c76d5f214e08f11643359199f2cc8b28192b41eff175431a9d8826c224363f94f71adecbd59ff81ff6dd6594d89c15d5498f558b9aebf
6
+ metadata.gz: d29d16c5b0d02017f574df54ee5a14a2455e4440da51c72d6cb480e89c284de9167fbb5f0dbdf4bb8e659009c2020ec06d80f7e7b56c2e06323969b5356c9db2
7
+ data.tar.gz: 510aaa0c2252ae3a1a1ab548a9f4272fdaa304cef732cb9b87995d0996734340f5673750abfe827af29b875dde7e0b08cf88b345da2e1543b9459879d4072168
data/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.37.0] - 2026-08-05
11
+
12
+ ### Fixed
13
+
14
+ - **`refresh_tokens.session_id` is finally populated, and the cause was statement ordering, not a missing feature.** Every refresh token in every consuming app had a nil `session_id`, so every session→refresh revocation cascade — the gem's own `Session#revoke!`, `Session.revoke_sessions!`, and each app's equivalent — matched **zero rows**, and 0.35.0's parent-session check had no parent to consult. Measured in production: jumpdrive 15 tokens / 0 linked; nutripod 238 / 0, with **177 live at the time of measurement**.
15
+
16
+ `generate_token_response` created the refresh token one statement *before* `maybe_persist_session_for_token!` ran, inside the same transaction — so when `persist_refresh_token!` wrote `session_id`, there was no session yet to point at. The link was nil **by construction**, not by omission. Swapping the two statements is the fix; `refresh_token_session_id` now returns the session the same grant just materialised.
17
+
18
+ See rarebit-one/rarebit-ops#304.
19
+
20
+ ### Changed
21
+
22
+ - **BEHAVIOUR NARROWED — `validate_parent_session!` now refuses only on a REVOKED parent, no longer on an expired one.** This is a deliberate relaxation of what 0.35.0 shipped, and hosts should read it before upgrading.
23
+
24
+ `Session#active?` is `!revoked? && !expired?`, so the original `return if session.active?` also refused a refresh whose parent had merely aged out. That was harmless while nothing was linked — with no parent, neither branch could fire. The moment linkage above becomes real, an expiry branch silently becomes a **lifetime policy**, and a badly-scaled one: an app running an 86400s `browser_session_lifetime` against a 2592000s `refresh_token_lifetime` would see its session-backed clients drop from monthly to **daily** re-authorization, as a side effect of a security fix that was never about login frequency.
25
+
26
+ Revocation is the property the estate wants: an operator revokes a session and access ends. Lifetime remains `refresh_token_lifetime`'s job, which already exists and is already configured per host. Keeping them separate is what lets linkage ship without renegotiating anyone's login cadence.
27
+
28
+ - **`ServiceSession` is carved out of linkage on purpose.** Machine credentials keep their own lifecycle, matching the exclusion the `:account` revocation scope already makes, so a human's browser logout cannot kill a running CLI or MCP agent sharing the account.
29
+
30
+ ### Notes for hosts
31
+
32
+ **This release does not retroactively repair your app.** Linkage happens only where the host materialises a session at token-issue time via `config.session.session_type_resolver`. A host with no resolver configured keeps nil `session_id` and is unchanged in every respect. The gem now makes the property *achievable and automatic*; enabling it per app is separate work.
33
+
34
+ Existing rows cannot be linked retroactively — there is no record of which session issued them.
35
+
10
36
  ## [0.36.2] - 2026-08-04
11
37
 
12
38
  **The third release in one chain, and the last of it.** 0.36.0 made the account guards bite on live sessions; 0.36.1 repaired the 500 that exposed on `ApiEngine`; this repairs 0.36.1's own overreach onto the OAuth token endpoint. That reads like flailing, so state it plainly: 0.36.0 was a correct behaviour change, and each follow-up is a narrower blast radius of the same root cause — an exception pair that no controller in the ancestry had ever had to answer for, meeting two route families with two different wire contracts. 0.36.1 fixed the first family and, by rescuing on the shared parent, silently annexed the second. 0.36.2 draws the line between them. Nothing here reverts 0.36.0 or 0.36.1; resource-endpoint behaviour is byte-identical to 0.36.1.
@@ -84,9 +84,26 @@ module StandardId
84
84
  #
85
85
  # Checking the parent here makes the property true by construction: it
86
86
  # holds however the session was revoked — `revoke!`, a bare `update!`, a
87
- # bulk `update_all`, a DBA, a data fix — and it covers expiry as well.
87
+ # bulk `update_all`, a DBA, a data fix.
88
88
  # See rarebit-one/rarebit-ops#297.
89
89
  #
90
+ # REVOCATION, NOT EXPIRY — deliberately, and this is a narrowing of what
91
+ # 0.35.0 shipped. `Session#active?` is `!revoked? && !expired?`, so the
92
+ # original `return if session.active?` also refused a refresh whose parent
93
+ # had merely aged out. That was harmless while `session_id` was nil
94
+ # everywhere; the moment rarebit-one/rarebit-ops#304 makes linkage real it
95
+ # silently becomes a lifetime policy — and a badly-scaled one. jumpdrive
96
+ # runs an 86400s browser_session_lifetime against a 2592000s
97
+ # refresh_token_lifetime, so tying the two would cut its session-backed
98
+ # MCP clients from monthly to DAILY re-authorization, as a side effect of
99
+ # a security fix nobody asked to change login frequency.
100
+ #
101
+ # Revocation is the property the estate actually wants: an operator clicks
102
+ # "revoke this session" and access ends. Expiry is a lifetime decision that
103
+ # belongs to `refresh_token_lifetime`, which already exists and is already
104
+ # configured per app. Keeping them separate means linkage can ship without
105
+ # renegotiating anyone's login cadence.
106
+ #
90
107
  # A refresh token with no session (`session_id` nil) is unaffected: that
91
108
  # is the machine-to-machine shape (client_credentials and any other grant
92
109
  # the gem issues without persisting a session), where there is no parent
@@ -99,7 +116,7 @@ module StandardId
99
116
  def validate_parent_session!
100
117
  session = @current_refresh_token_record.session
101
118
  return if session.nil?
102
- return if session.active?
119
+ return unless session.revoked?
103
120
 
104
121
  raise StandardId::InvalidGrantError, "Refresh token is no longer valid"
105
122
  end
@@ -63,8 +63,15 @@ module StandardId
63
63
  # (see there for why) — that path is safe because the swallowed
64
64
  # exception fires before any DB work in this block.
65
65
  ActiveRecord::Base.transaction do
66
+ # Session FIRST, refresh token second. The order is load-bearing, not
67
+ # stylistic: `persist_refresh_token!` writes `session_id`, so the
68
+ # session row has to exist before the token row is built or the link
69
+ # is nil by construction. It was the other way round until
70
+ # rarebit-one/rarebit-ops#304, which is the whole reason
71
+ # `refresh_tokens.session_id` was nil in every app in the estate —
72
+ # there was no session to point at yet.
73
+ @oauth_session = maybe_persist_session_for_token!
66
74
  response[:refresh_token] = generate_refresh_token if supports_refresh_token?
67
- maybe_persist_session_for_token!
68
75
  end
69
76
 
70
77
  emit_token_issued(expires_in)
@@ -166,8 +173,33 @@ module StandardId
166
173
  )
167
174
  end
168
175
 
176
+ # The parent session for the refresh token issued by THIS grant, when the
177
+ # host materialised one via `config.session.session_type_resolver`.
178
+ #
179
+ # Returning a real id here is what makes "revoking a session ends that
180
+ # session's access" true by construction rather than an emergent property
181
+ # of every caller remembering to reach for the right method. With it nil —
182
+ # which it was everywhere before rarebit-one/rarebit-ops#304 — every
183
+ # session→refresh cascade in the gem and in the apps matched zero rows,
184
+ # and 0.35.0's parent-session check had no parent to consult.
185
+ #
186
+ # Still nil where there is genuinely no parent: no resolver configured
187
+ # (the default), or a grant the host does not materialise a session for.
188
+ # That keeps machine-to-machine grants (client_credentials) exactly as
189
+ # they were — there is no session to outlive.
190
+ #
191
+ # ServiceSessions are carved out on purpose. Machine credentials have
192
+ # their own lifecycle, and the gem's `:account` revocation scope already
193
+ # excludes them; linking one here would let a human's browser logout kill
194
+ # a running CLI or MCP agent sharing the account. `OauthSessionPersistence`
195
+ # refuses to build one at all, so this guard is belt-and-braces against a
196
+ # host that reaches around it — cheap, and the failure it prevents is
197
+ # silent.
169
198
  def refresh_token_session_id
170
- nil
199
+ return nil if @oauth_session.nil?
200
+ return nil if @oauth_session.is_a?(StandardId::ServiceSession)
201
+
202
+ @oauth_session.id
171
203
  end
172
204
 
173
205
  def previous_refresh_token_record
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.36.2"
2
+ VERSION = "0.37.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.36.2
4
+ version: 0.37.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim