standard_id 0.37.0 → 0.38.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: 26e62c374edcaa53ab63756178e869a2d0c54c7ab00d26c2da823b28867fbeb6
4
- data.tar.gz: 1a9a4e63f3e7f0e603c771c78f9fe503aad2768ea6302f675c360056f9d7c8d7
3
+ metadata.gz: ea8084bb866fc4da82d81ec18d2b24676e3209ac8d21975c20deb9aa334160fc
4
+ data.tar.gz: 1cc922486ca4204c8308ff489dc5f425424c06e758a1a4b77276b6670a637f10
5
5
  SHA512:
6
- metadata.gz: d29d16c5b0d02017f574df54ee5a14a2455e4440da51c72d6cb480e89c284de9167fbb5f0dbdf4bb8e659009c2020ec06d80f7e7b56c2e06323969b5356c9db2
7
- data.tar.gz: 510aaa0c2252ae3a1a1ab548a9f4272fdaa304cef732cb9b87995d0996734340f5673750abfe827af29b875dde7e0b08cf88b345da2e1543b9459879d4072168
6
+ metadata.gz: 3e030d75f3f08d7961ac1cab6ede04da0940ee5471191bfc5c06ef832469f6da2a9af44539fa04e889801defded08c8bdf138c7a8bd1661d8c0628c5b707d08f
7
+ data.tar.gz: b5301f0db610433bcb542699f95beddcd9e91e93dda098f940edafc9af5c70ab264ef6c70b427e84ac34a9edae861316cf3d608f4f5c2dad6996eb6b9780cbcf
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.38.0] - 2026-08-06
11
+
12
+ ### Added
13
+
14
+ - **Event payloads now carry record identifiers alongside the records — the emitter half of rarebit-one/rarebit-ops#296.** This gem publishes whole ActiveRecord objects on the notification bus (`account:`, `current_account:`, `session:`, `code_challenge:`), and a record serialises with **all** its attributes. That is how `account.password_digest`, `session.token_digest`, `session.lookup_hash` and the plaintext passwordless OTP in `code_challenge.code` reached append-only audit rows across the estate.
15
+
16
+ `standard_audit` 0.11.0 closed its own write path with `dereference_record_metadata`, but it could only ever protect `audit_logs`. Every host subscriber receives the same payload and may put the record into Sentry context, a structured log, or a table of its own — all outside that gem's reach. Curating at the emitter is the only fix that closes the class.
17
+
18
+ `config.events.publish_record_identifiers` (default **`true`**) adds `<key>_id` and `<key>_gid` beside every record-valued key and **removes nothing**.
19
+
20
+ - **`config.events.publish_records` (default `true`) is the opt-out** for hosts ready to stop receiving the records themselves.
21
+
22
+ ### Notes for hosts
23
+
24
+ - **This release is additive on purpose — no consumer changes behaviour today.** All five consumers configure an `actor_extractor` reading `payload[:actor] || payload[:current_account] || payload[:account]` and calling `to_global_id` on the result, so swapping records for identifiers in one step would break actor attribution in five apps simultaneously.
25
+
26
+ The migration path is per-host and provable: point your subscribers at the new `_gid` keys, confirm in staging that nothing depends on the records, then set `publish_records` to `false`. The intended end state is for that to default to `false` in a later major, and for the records to go away in the one after.
27
+
28
+ - **Both `_id` and `_gid` are published.** A GlobalID carries the class, and `account_id` alone cannot tell an `Account` from anything else sharing a primary key. The audit gems key on the GID.
29
+
30
+ - **`to_global_id` is rescued.** It raises on an unpersisted record, and publishing sits on the authentication path — an event must never fail because of metadata this method adds to it.
31
+
10
32
  ## [0.37.0] - 2026-08-05
11
33
 
12
34
  ### Fixed
@@ -108,6 +108,34 @@ StandardId::ConfigSchema.define do
108
108
 
109
109
  scope :events do
110
110
  field :enable_logging, type: :boolean, default: false
111
+
112
+ # Publish `<key>_id` / `<key>_gid` alongside every record-valued payload key
113
+ # (rarebit-one/rarebit-ops#296).
114
+ #
115
+ # StandardId publishes whole ActiveRecord objects on the notification bus —
116
+ # `account:`, `current_account:`, `session:`, `code_challenge:` — and a
117
+ # record serialises with ALL its attributes. `standard_audit` 0.11.0 stopped
118
+ # that reaching `audit_logs`, but the bus is not the audit gem's to police:
119
+ # a host's own subscribers still receive the full record and may put it in
120
+ # Sentry context, a structured log, or a table of their own.
121
+ #
122
+ # On by default because it only ADDS keys. Nothing that reads `account:`
123
+ # today changes behaviour — including every consumer's `actor_extractor`,
124
+ # which reads the record to call `to_global_id` on it.
125
+ field :publish_record_identifiers, type: :boolean, default: true
126
+
127
+ # The other half of the migration, and deliberately OFF by default.
128
+ #
129
+ # Turning it off stops the records themselves being published, leaving only
130
+ # the identifiers. That IS a breaking payload change — it is the whole point
131
+ # — so it is a host's opt-in, taken once its subscribers read the `_id`/`_gid`
132
+ # keys instead. A host can flip it in staging to prove nothing depends on
133
+ # the records before committing.
134
+ #
135
+ # The intended end state is this defaulting to false in a later major, and
136
+ # the records going away in the one after. Until a host opts in, behaviour
137
+ # is exactly as it was.
138
+ field :publish_records, type: :boolean, default: true
111
139
  end
112
140
 
113
141
  scope :passwordless do
@@ -132,7 +132,65 @@ module StandardId
132
132
  enriched[:scope] ||= ::Current.scope if ::Current.respond_to?(:scope) && ::Current.scope.present?
133
133
  end
134
134
 
135
- enriched.merge(payload)
135
+ with_record_identifiers(enriched.merge(payload))
136
+ end
137
+
138
+ # Adds `<key>_id` and `<key>_gid` next to every record-valued key, and —
139
+ # only when a host opts in — removes the records themselves.
140
+ #
141
+ # WHY (rarebit-one/rarebit-ops#296)
142
+ # ---------------------------------
143
+ # This gem publishes whole ActiveRecord objects: `account:`,
144
+ # `current_account:`, `session:`, `code_challenge:`. A record serialises
145
+ # with ALL its attributes, which is how `account.password_digest`,
146
+ # `session.token_digest`, `session.lookup_hash` and the plaintext
147
+ # passwordless OTP in `code_challenge.code` reached append-only audit rows
148
+ # — 3,007 of them on one app, unrepairable because the table refuses
149
+ # UPDATE by design.
150
+ #
151
+ # `standard_audit` 0.11.0 fixed its own write path. It cannot fix the bus:
152
+ # every host subscriber still receives the full record and may put it
153
+ # anywhere. Publishing the identifier alongside is what lets a subscriber
154
+ # stop needing the record at all.
155
+ #
156
+ # WHY ADDITIVE FIRST
157
+ # ------------------
158
+ # Swapping records for identifiers in one step breaks every consumer's
159
+ # `actor_extractor` simultaneously — all five apps configure one, and they
160
+ # read `payload[:actor] || payload[:current_account] || payload[:account]`
161
+ # expecting something they can call `to_global_id` on. So the default adds
162
+ # keys and removes nothing; `config.events.publish_records = false` is the
163
+ # opt-out a host takes once its subscribers read the identifiers.
164
+ #
165
+ # `_gid` as well as `_id` because a GlobalID carries the class, and the
166
+ # audit gems key on it. `to_global_id` is rescued: a new or unpersisted
167
+ # record raises, and an event must never fail because of the metadata this
168
+ # method adds to it.
169
+ def with_record_identifiers(payload)
170
+ return payload unless defined?(::ActiveRecord::Base)
171
+
172
+ config = StandardId.config.events
173
+ return payload unless config.publish_record_identifiers || !config.publish_records
174
+
175
+ payload.each_with_object({}) do |(key, value), out|
176
+ unless value.is_a?(::ActiveRecord::Base)
177
+ out[key] = value
178
+ next
179
+ end
180
+
181
+ out[key] = value if config.publish_records
182
+
183
+ next unless config.publish_record_identifiers
184
+
185
+ out[:"#{key}_id"] ||= value.id
186
+ out[:"#{key}_gid"] ||= safe_global_id(value)
187
+ end.compact
188
+ end
189
+
190
+ def safe_global_id(record)
191
+ record.to_global_id.to_s
192
+ rescue StandardError
193
+ nil
136
194
  end
137
195
  end
138
196
  end
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.37.0"
2
+ VERSION = "0.38.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.37.0
4
+ version: 0.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim