basecradle 0.10.1 → 0.10.2

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: 0a805499915d7672ddf23664f7a5147b41915446a488a122c018cb46ac65f076
4
- data.tar.gz: 5739cfa3f912b9d9d29d03a9a083c58371175b0bd7dd75dbad1ee6209d426288
3
+ metadata.gz: 4e662740bb7ecbde3769c3430a269cbe32a0c507a4244626688af1ca13ce3ce4
4
+ data.tar.gz: 64820f7409159b2cfd1b063a53c96858302bfc939e4ff2df17b6ffcaf3b494e5
5
5
  SHA512:
6
- metadata.gz: 3836f2b3204ca798e3d2c4bd17dcc43f59faf0dc86c7097504f05e01519e5d91cc8c10f0889852899c64a816ef7c33da009cd3a2948f0f9cd8019ee279246870
7
- data.tar.gz: 44894880f4119664ffdceff8d9550b8d69e715073a55ad31287bfbd507ba8a7758bc92003de5fb0f1e8b17919ddf3f0968ff4c6344c988c53f91360aca5c685d
6
+ metadata.gz: 6c7a007a0c7eef4fe1bb8d4d19fc108c361fc5e5d4a618d5d61600a0cdd55fbb6b97ecc5ea5c07ed8ac6488840b7cc33133c59a6160feab2a04222da73683eeb
7
+ data.tar.gz: 7059369ae648787b45a97afe3b12ba721474629078031ac036461732cc7dfc9d13e764b916f48a7e72fa96b248bac4c53c1ee3ed686f2abc98d13ed1c54a6c28
data/CHANGELOG.md CHANGED
@@ -9,6 +9,40 @@ section**. The newest heading is always the version `lib/basecradle/version.rb`
9
9
  a release writes its entry and its version in the same PR — and `test/changelog_test.rb`
10
10
  fails CI if the two ever disagree.
11
11
 
12
+ ## [0.10.2] - 2026-09-30
13
+
14
+ ### Security
15
+
16
+ - **`WebhookEventHeaders` no longer prints header values when inspected**
17
+ ([#206](https://github.com/basecradle/basecradle-ruby/issues/206)). An inbound
18
+ delivery's headers are the *sender's*, stored verbatim by the platform, so one of them
19
+ may be the sender's own credential — a POST authenticated to an ingest URL carries its
20
+ `Authorization` or `X-Api-Key` right there. `WebhookEventHeaders` is a `Hash`
21
+ descendant and inherited `Hash`'s render, which prints every pair, so
22
+ `logger.debug(event.content.headers)` wrote **another party's secret** into your logs.
23
+ `inspect`, `to_s` and `pp` now render the header names alone, sorted, matching the rule
24
+ every other object in this SDK already followed (`ApiObject#inspect` prints field
25
+ names, never values):
26
+
27
+ ```
28
+ #<BaseCradle::WebhookEventHeaders Authorization, Content-Type, X-Api-Key>
29
+ ```
30
+
31
+ **Only the human-facing render changed.** Every read is untouched and still wire-exact:
32
+ the case-folded lookup, `fetch`, `each`, `keys`, `to_h`, `to_json`, `==`. If you were
33
+ relying on `inspect` to see values, use `to_h` — which is what it was always for. No
34
+ credential of yours was exposed; the exposure was of whoever sent you the webhook, so
35
+ if you have logged inbound deliveries at debug level, treat those senders' headers as
36
+ disclosed.
37
+ - **Two ways a header value still reaches a log, both by design.** `to_json`/`as_json`
38
+ emit the delivery verbatim — that is what a webhook record *is* — so
39
+ `render json: event` and `logger.info(event.to_json)` still write the sender's headers
40
+ in full. And converting away from the type gives up the redaction exactly as it gives up
41
+ the case-folding: `slice`, `except`, `select`, `transform_values` and `to_h` each hand
42
+ back a plain `Hash`, whose `inspect` prints pairs. Only `merge` and `dup` keep the type,
43
+ and so the redaction. Changing what a webhook record *serializes* as is a separate
44
+ decision and is not made here.
45
+
12
46
  ## [0.10.1] - 2026-09-30
13
47
 
14
48
  ### Security
@@ -450,6 +484,7 @@ the Python SDK's behavior in idiomatic Ruby. Zero runtime dependencies.
450
484
  - **Quality bars** — a README-as-tested-doc harness (every example runs against a mocked
451
485
  API) and a spec drift-guard (CI fails if the live API grows beyond the SDK).
452
486
 
487
+ [0.10.2]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.2
453
488
  [0.10.1]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.1
454
489
  [0.10.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.0
455
490
  [0.9.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.9.0
data/README.md CHANGED
@@ -220,6 +220,19 @@ fallback-less `fetch` raise `KeyError` naming the headers that did arrive, while
220
220
  and the methods that reshape it (`slice`, `except`, `select`) work on those stored names,
221
221
  case-sensitively.
222
222
 
223
+ What it does **not** show you is the values. `inspect`, `to_s` and `pp` print the header
224
+ names alone — `#<BaseCradle::WebhookEventHeaders Authorization, Content-Type>` — because
225
+ these headers are the **sender's**, and a sender that authenticates its POST to your
226
+ ingest URL carries its credential in one of them. So `logger.debug(event.content.headers)`
227
+ cannot write another party's secret into your logs. Every read is unaffected; `to_h` is
228
+ how you look at values.
229
+
230
+ Two things do still show them, deliberately. `to_json` emits the delivery verbatim — that
231
+ is what the record is — so `render json: event` and `logger.info(event.to_json)` write the
232
+ headers in full. And converting away from the type gives up the redaction exactly as it
233
+ gives up the case-folding: `slice`, `except`, `select`, `transform_values` and `to_h` hand
234
+ back a plain `Hash`, which prints pairs. `merge` and `dup` keep the type, and keep both.
235
+
223
236
  This holds however you reach the record. A timeline item's `content` is typed by the
224
237
  item's own `type`, so a `webhook_event` row of `timeline.items` is a `WebhookEventContent`
225
238
  just like one from `bc.webhook_events` — `item.content.headers["X-GitHub-Delivery"]` folds
@@ -426,6 +439,12 @@ CI additionally lints the workflow files with [actionlint](https://github.com/rh
426
439
  both and run `actionlint` from the repo root — CI pins actionlint by version and SHA256 in
427
440
  `.github/workflows/ci.yml`; match that version to see what CI sees.
428
441
 
442
+ Bare `actionlint` covers slightly more than CI blocks on. It lints everything under
443
+ `.github/workflows`, including `needs-human-alert.yml` and `dependabot-auto-merge.yml`,
444
+ which are shared verbatim from the BaseCradle core repo and are not this repo's to edit;
445
+ CI lints those two advisory-only, so a finding in them is reported upstream rather than
446
+ failing the build. A finding in any other workflow does fail it.
447
+
429
448
  ## Contributing
430
449
 
431
450
  Human and AI contributors work under identical rules here: branch → PR → green CI → merge. See [`CLAUDE.md`](CLAUDE.md) for the project conventions and the issues for the roadmap.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BaseCradle
4
- VERSION = "0.10.1"
4
+ VERSION = "0.10.2"
5
5
  end
@@ -99,6 +99,14 @@ module BaseCradle
99
99
  # that already happened. Converting away from this type gives up the folding too
100
100
  # (+to_h+, <tt>{**headers}</tt>, and anything else that hands back a plain +Hash+);
101
101
  # +merge+ and +dup+ keep it.
102
+ #
103
+ # What it does *not* print is the values: +inspect+, +to_s+ and +pp+ render the header
104
+ # names alone, because these are the sender's headers and one of them may be the
105
+ # sender's credential. Every read is unaffected — and so is +to_json+, which still
106
+ # emits the delivery verbatim. Converting away from this type gives the redaction up
107
+ # exactly as it gives up the folding, and for the same reason: +slice+, +except+,
108
+ # +select+, +transform_values+ and +to_h+ hand back a plain +Hash+, whose +inspect+
109
+ # prints pairs.
102
110
  class WebhookEventHeaders < Hash
103
111
  # Hash's own exact-match membership, kept under a private name before the case-folding
104
112
  # +key?+ below takes the name: a caller who wrote the wire's own spelling is answered
@@ -167,6 +175,43 @@ module BaseCradle
167
175
  ->(name) { dig(name) }
168
176
  end
169
177
 
178
+ # The header *names* this delivery carried, and none of their values.
179
+ #
180
+ # +Hash+'s own +inspect+ prints every pair, and these headers are the *sender's*,
181
+ # stored verbatim by the platform: a sender that authenticates its POST to an ingest
182
+ # URL puts its credential in one of them, so +logger.debug(event.content.headers)+
183
+ # would write another party's secret into our logs. Every other renderable object in
184
+ # this SDK is already safe by one rule — +ApiObject#inspect+ prints field names, never
185
+ # values — and this is the one class that sat outside it, because +Hash+ brought its
186
+ # own render.
187
+ #
188
+ # Only the human-facing render changes. Every *read* is untouched and still
189
+ # wire-exact: lookups, +fetch+, +each+, +keys+, +to_h+, +to_json+, +==+.
190
+ #
191
+ # Names are sorted, as +ApiObject#inspect+ sorts its fields and as the +KeyError+ a
192
+ # missing header raises already lists the ones that did arrive.
193
+ def inspect
194
+ return "#<#{self.class}>" if empty?
195
+
196
+ "#<#{self.class} #{keys.sort_by(&:to_s).join(', ')}>"
197
+ end
198
+
199
+ # +Hash+ aliases +to_s+ to its *own* +inspect+ — one shared method entry — so
200
+ # overriding +inspect+ alone would leave <tt>"#{headers}"</tt> and +puts headers+
201
+ # printing every value. Late-bound rather than +alias to_s inspect+, so a subclass
202
+ # that redacts more is not bypassed by interpolation (as +Client#to_s+ is).
203
+ def to_s
204
+ inspect
205
+ end
206
+
207
+ # +pp+ reaches for +pretty_print+, and +Hash+ brings one that prints the pairs — the
208
+ # same leak through the one door +inspect+ does not cover. Delegating puts +pp+ back
209
+ # on the SDK-wide rule: +pp+ uses an object's own +inspect+ when it defines one, which
210
+ # is why every +ApiObject+ is already safe here and only a +Hash+ descendant was not.
211
+ def pretty_print(printer)
212
+ printer.text(inspect)
213
+ end
214
+
170
215
  private
171
216
 
172
217
  # The wire's own spelling of +name+, or nil if no casing of it was delivered. Anything
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basecradle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Drawk Kwast