basecradle 0.10.0 → 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: 3acdf9b8dad4d29a40926030c480cc415b96ffff03c56a121838b4fdb42cd638
4
- data.tar.gz: 2b88488a27cae407aa366cce9899da840b7ea1e74db86d21c511f0f2b1ac47d7
3
+ metadata.gz: 4e662740bb7ecbde3769c3430a269cbe32a0c507a4244626688af1ca13ce3ce4
4
+ data.tar.gz: 64820f7409159b2cfd1b063a53c96858302bfc939e4ff2df17b6ffcaf3b494e5
5
5
  SHA512:
6
- metadata.gz: 7f753c98128c1935e0503dd1963917ac1918e171401b4a39f6c1b650cd716213f02c52b1dd538a54bb201de0ede89af19e7ab2ec9abfa79d2eba68d71faa5f57
7
- data.tar.gz: 31772a4161d2e3563a835ffbd2c9ae5a2d9e285a157ca2772be3213037297b168f8e89b67e8290a7897a77a009add74b9769bf5e623ac5256ec19f26eece04d9
6
+ metadata.gz: 6c7a007a0c7eef4fe1bb8d4d19fc108c361fc5e5d4a618d5d61600a0cdd55fbb6b97ecc5ea5c07ed8ac6488840b7cc33133c59a6160feab2a04222da73683eeb
7
+ data.tar.gz: 7059369ae648787b45a97afe3b12ba721474629078031ac036461732cc7dfc9d13e764b916f48a7e72fa96b248bac4c53c1ee3ed686f2abc98d13ed1c54a6c28
data/CHANGELOG.md CHANGED
@@ -9,6 +9,94 @@ 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
+
46
+ ## [0.10.1] - 2026-09-30
47
+
48
+ ### Security
49
+
50
+ - **A `Client` no longer serializes, and never emits its token**
51
+ ([#198](https://github.com/basecradle/basecradle-ruby/issues/198)). A client is a
52
+ connection, not a record, and it defined no `as_json`/`to_json` — so with ActiveSupport
53
+ loaded it fell through to `Object#as_json`, which is `instance_values`, walked
54
+ recursively. Its instance variables include the raw `bc_uat_` token, so
55
+ `render json: { conn: bc }` or `logger.info({ conn: bc }.to_json)` wrote the live
56
+ credential into a response body or a log line, and **a token in a log is a token to
57
+ rotate**. `bc.to_json` now raises `BaseCradle::NotSerializableError`, a
58
+ `BaseCradle::Error`, naming what to serialize instead. Long-standing and latent; no
59
+ token is known to have been emitted.
60
+ - **`Client#inspect` and `#to_s` redact the token** — the same exposure through a
61
+ different door. Ruby's default `inspect` dumps every instance variable, so the
62
+ credential printed into every exception message, REPL transcript and `p` call that
63
+ touched a client. Both now read
64
+ `#<BaseCradle::Client base_url="https://basecradle.com" token=[REDACTED]>`, with `to_s`
65
+ the same string because interpolation is the door people reach for without thinking.
66
+
67
+ ### Fixed
68
+
69
+ - **A collection resource no longer serializes either.** `bc.timelines`, `bc.messages`,
70
+ `timeline.tasks`, any `.filter(...)`, and the `Paginator` behind them are lazy queries,
71
+ not records. ActiveSupport's `Enumerable#as_json` calls `to_a`, so serializing one ran
72
+ a page-by-page GET loop over the whole resource from **inside a view render** and
73
+ emitted every record it fetched. It did *not* reach the token — `Enumerable#as_json`
74
+ shadows the `instance_values` walk above — but a client's instance variables are eight
75
+ collections, so serializing a client fired those loops on its way to the credential.
76
+ All of them now raise the same `NotSerializableError`, before any HTTP, pointing at
77
+ `.to_a` so how much you fetch is a visible act in your own code.
78
+ Without ActiveSupport these calls were useless rather than dangerous
79
+ (`"#<BaseCradle::MessagesResource:0x…>"`, the heap address 0.10.0 removed for models);
80
+ that is closed too.
81
+ - **The collection resources' `inspect` is unchanged** and still shows Ruby's default
82
+ ivar dump — which renders the client through the redacting `Client#inspect` above, so
83
+ no token reaches it. Pinned by test so it stays that way.
84
+
85
+ ### Known limitation
86
+
87
+ The refusals are on the SDK's own objects. An `Enumerator` built from a collection —
88
+ `bc.messages.each`, `bc.messages.lazy` — is a plain Ruby object this SDK does not own,
89
+ and ActiveSupport gives it an `as_json` that calls `to_a`, so
90
+ `render json: { recent: bc.messages.lazy }` still pages the whole resource. No token is
91
+ exposed; call `.first(n)` or `.to_a` before handing a query to a renderer.
92
+
93
+ ### Migrating
94
+
95
+ Nothing to change unless you were serializing a `Client` or a collection, which never
96
+ emitted anything useful and, for a client under ActiveSupport, emitted your token. If you
97
+ were: serialize the record you meant (`bc.me`, a timeline, a message), or call `.to_a` /
98
+ `.first(n)` on a collection first.
99
+
12
100
  ## [0.10.0] - 2026-09-30
13
101
 
14
102
  ### Fixed
@@ -396,6 +484,8 @@ the Python SDK's behavior in idiomatic Ruby. Zero runtime dependencies.
396
484
  - **Quality bars** — a README-as-tested-doc harness (every example runs against a mocked
397
485
  API) and a spec drift-guard (CI fails if the live API grows beyond the SDK).
398
486
 
487
+ [0.10.2]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.2
488
+ [0.10.1]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.1
399
489
  [0.10.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.0
400
490
  [0.9.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.9.0
401
491
  [0.8.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.8.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
@@ -359,6 +372,57 @@ something Rails renders comes out right too. Three caveats worth knowing:
359
372
  pretty-prints. What an unknown option does is your `json` version's business — 3.x
360
373
  raises, the 2.x older Rubies ship ignores it.
361
374
 
375
+ ## What does not serialize: clients and collections
376
+
377
+ Two things in this SDK are *not* records, and serializing either raises
378
+ `BaseCradle::NotSerializableError` rather than emitting something:
379
+
380
+ ```ruby
381
+ require "basecradle"
382
+
383
+ bc = BaseCradle::Client.new
384
+
385
+ begin
386
+ bc.to_json # and render json: { conn: bc }
387
+ rescue BaseCradle::NotSerializableError => e
388
+ puts e.message.include?("bc_uat_") # => true
389
+ end
390
+
391
+ begin
392
+ bc.messages.to_json # a lazy query, not a record
393
+ rescue BaseCradle::NotSerializableError => e
394
+ puts e.message.include?(".to_a") # => true
395
+ end
396
+
397
+ puts bc.messages.first(20).to_json # serialize the page you actually asked for
398
+ ```
399
+
400
+ A **client** holds your `bc_uat_` token, and under ActiveSupport `Object#as_json` is
401
+ `instance_values` — walked recursively. So `render json: { conn: bc }` or
402
+ `logger.info({ conn: bc }.to_json)` used to write the raw credential into a response body
403
+ or a log line, and a token in a log is a token to rotate.
404
+
405
+ A **collection** (`bc.timelines`, `bc.messages`, `timeline.tasks`, any `.filter(...)`) is
406
+ a lazy query. ActiveSupport's `Enumerable#as_json` calls `to_a`, so serializing one ran a
407
+ page-by-page GET loop over the whole resource from inside the render and emitted every
408
+ record it fetched. (It did *not* reach the token — `Enumerable#as_json` shadows the ivar
409
+ walk — but a client's instance variables are eight collections, so serializing a *client*
410
+ ran those loops too, on its way to the credential.) Call `.to_a` or `.first(n)` yourself
411
+ and serialize that, so how much you fetch is a visible act in your code.
412
+
413
+ Both refusals happen before any HTTP — but they are on the SDK's own objects. An
414
+ `Enumerator` you built from one is a plain Ruby object this SDK does not own, so
415
+ `render json: { recent: bc.messages.lazy }` and `bc.messages.each.to_json` still page the
416
+ whole resource. Call `.first(n)` or `.to_a` before handing a query to a renderer.
417
+
418
+ `bc.inspect` and `"#{bc}"` are redacted for the same reason — Ruby's default `inspect`
419
+ dumps every instance variable, which would print the token into every exception message
420
+ and REPL transcript that touched a client:
421
+
422
+ ```
423
+ #<BaseCradle::Client base_url="https://basecradle.com" token=[REDACTED]>
424
+ ```
425
+
362
426
  ## Development
363
427
 
364
428
  ```bash
@@ -375,6 +439,12 @@ CI additionally lints the workflow files with [actionlint](https://github.com/rh
375
439
  both and run `actionlint` from the repo root — CI pins actionlint by version and SHA256 in
376
440
  `.github/workflows/ci.yml`; match that version to see what CI sees.
377
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
+
378
448
  ## Contributing
379
449
 
380
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.
@@ -7,6 +7,7 @@ require "uri"
7
7
  require_relative "dashboard"
8
8
  require_relative "errors"
9
9
  require_relative "items"
10
+ require_relative "serialization"
10
11
  require_relative "sessions"
11
12
  require_relative "timelines"
12
13
  require_relative "user"
@@ -23,6 +24,10 @@ module BaseCradle
23
24
  # Every resource is built on +#request+, which is also the escape hatch for API
24
25
  # endpoints added before the SDK wraps them (the API is additive-only).
25
26
  class Client
27
+ # A client is a connection holding a live credential, never a record: serializing one
28
+ # raises rather than write the token into a response body or a log line.
29
+ include NotSerializable
30
+
26
31
  DEFAULT_BASE_URL = "https://basecradle.com"
27
32
  DEFAULT_TIMEOUT = 30
28
33
 
@@ -192,8 +197,20 @@ module BaseCradle
192
197
  handle(response)
193
198
  end
194
199
 
200
+ # Never the token. Ruby's default +inspect+ dumps every ivar, which would print the
201
+ # credential into every exception message, REPL transcript and +p+ call that touched
202
+ # a client — the same leak as serializing one, through a different door. The
203
+ # redaction marker is fixed text, so what it stands for cannot be guessed from its
204
+ # length. +to_s+ is the same string: string interpolation is the door people reach
205
+ # for without thinking about it.
195
206
  def inspect
196
- "#<#{self.class} base_url=#{@base_url.inspect}>"
207
+ "#<#{self.class} base_url=#{@base_url.inspect} token=[REDACTED]>"
208
+ end
209
+
210
+ # Late-bound rather than `alias to_s inspect`, so a subclass that redacts more in
211
+ # inspect is not bypassed by interpolation.
212
+ def to_s
213
+ inspect
197
214
  end
198
215
 
199
216
  # The shared low-level send: returns the Net::HTTPResponse or raises APIConnectionError.
@@ -226,6 +243,16 @@ module BaseCradle
226
243
 
227
244
  private
228
245
 
246
+ # Why +to_json+ / +as_json+ refuse (see BaseCradle::NotSerializable). A client is not
247
+ # a record, and the thing it would emit is a live credential.
248
+ def serialization_refusal
249
+ "#{self.class} is a connection holding your bc_uat_ token, not a record. " \
250
+ "Serializing it writes the raw credential into whatever you were rendering or " \
251
+ "logging, and a token in a log is a token to rotate. Serialize the record you " \
252
+ "meant instead (bc.me, a timeline, a message), or bc.base_url to name the " \
253
+ "connection itself."
254
+ end
255
+
229
256
  # Record what the mint response said about the credential just issued. Private: only
230
257
  # +.login+ calls it, on a client it has just built.
231
258
  def minted(body)
@@ -30,6 +30,12 @@ module BaseCradle
30
30
  # this response form). The SDK raises rather than return an ambiguous nil.
31
31
  class MissingFieldError < Error; end
32
32
 
33
+ # A Client or a collection resource was serialized (+to_json+ / +as_json+). Neither is
34
+ # a record: a Client holds your bearer token, and a collection is a lazy query whose
35
+ # serialization would page the whole API from inside your renderer. The SDK refuses
36
+ # loudly rather than emit a credential or a heap address — see BaseCradle::NotSerializable.
37
+ class NotSerializableError < Error; end
38
+
33
39
  # The request never got an API response (DNS failure, refused connection, timeout).
34
40
  # The underlying exception is preserved as +cause+.
35
41
  class APIConnectionError < Error; end
@@ -5,6 +5,7 @@ require "time"
5
5
 
6
6
  require_relative "api_object"
7
7
  require_relative "pagination"
8
+ require_relative "serialization"
8
9
  require_relative "user"
9
10
 
10
11
  module BaseCradle
@@ -93,6 +94,7 @@ module BaseCradle
93
94
  # PLURAL / SINGULAR / MODEL.
94
95
  class ItemsResource
95
96
  include Enumerable
97
+ include NotSerializableCollection
96
98
 
97
99
  def initialize(client, filters: {})
98
100
  @client = client
@@ -163,6 +165,7 @@ module BaseCradle
163
165
  # One timeline's messages: create here, or iterate (newest first).
164
166
  class TimelineMessages
165
167
  include Enumerable
168
+ include NotSerializableCollection
166
169
 
167
170
  def initialize(client, timeline_uuid)
168
171
  @client = client
@@ -190,6 +193,7 @@ module BaseCradle
190
193
  # One timeline's assets: upload here (multipart), or iterate (newest first).
191
194
  class TimelineAssets
192
195
  include Enumerable
196
+ include NotSerializableCollection
193
197
 
194
198
  def initialize(client, timeline_uuid)
195
199
  @client = client
@@ -234,6 +238,7 @@ module BaseCradle
234
238
  # One timeline's tasks: create here, or iterate (newest first).
235
239
  class TimelineTasks
236
240
  include Enumerable
241
+ include NotSerializableCollection
237
242
 
238
243
  def initialize(client, timeline_uuid)
239
244
  @client = client
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "serialization"
4
+
3
5
  module BaseCradle
4
6
  # The shared cursor-pagination engine. Every list endpoint paginates the same way:
5
7
  # newest first, up to 50 per page, +next_cursor+ in the response passed back as
@@ -10,6 +12,7 @@ module BaseCradle
10
12
  # and cursors never appear in calling code.
11
13
  class Paginator
12
14
  include Enumerable
15
+ include NotSerializableCollection
13
16
 
14
17
  def initialize(client, path, envelope_key:, model:, params: nil)
15
18
  @client = client
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module BaseCradle
6
+ # Refuse to serialize an object that is a connection or a live query rather than a
7
+ # record, and say what to serialize instead.
8
+ #
9
+ # With ActiveSupport loaded, +Object#as_json+ is +instance_values+ — every instance
10
+ # variable, walked recursively. A Client's ivars include its raw +bc_uat_+ token, so
11
+ # <tt>render json: { conn: bc }</tt> or <tt>logger.info({ conn: bc }.to_json)</tt> put
12
+ # the credential in a response body or a log line, and **a token in a log is a token to
13
+ # rotate**. That same walk reaches the client's eight collection ivars, and
14
+ # ActiveSupport's +Enumerable#as_json+ calls +to_a+ — so serializing a client also fired
15
+ # a live, unbounded, page-by-page GET loop before it got to the token, and serializing a
16
+ # collection on its own fired one from inside the render for no stated reason.
17
+ #
18
+ # Without ActiveSupport the same calls were merely useless: +"#<BaseCradle::Client:0x…>"+,
19
+ # a heap address that differs every run.
20
+ #
21
+ # Includers supply +serialization_refusal+; the default below is the honest fallback
22
+ # for one that does not, so the failure is still a rescuable BaseCradle::Error rather
23
+ # than a NoMethodError out of a renderer.
24
+ module NotSerializable
25
+ # ActiveSupport's serialization hook — what reaches this object when it is nested in
26
+ # something Rails renders (<tt>render json: { conn: bc }</tt>).
27
+ def as_json(*)
28
+ raise NotSerializableError, serialization_refusal
29
+ end
30
+
31
+ # +JSON.generate+, +Hash#to_json+, +Array#to_json+ and a bare
32
+ # <tt>render json: bc</tt> all arrive here.
33
+ def to_json(*)
34
+ raise NotSerializableError, serialization_refusal
35
+ end
36
+
37
+ private
38
+
39
+ def serialization_refusal
40
+ "#{self.class} is not a record and cannot be serialized."
41
+ end
42
+ end
43
+
44
+ # What every lazy collection resource mixes in — the auto-paginating resources
45
+ # (+bc.timelines+, +bc.messages+, +timeline.tasks+, any +.filter(...)+) and the
46
+ # Paginator behind them.
47
+ #
48
+ # It includes +Enumerable+ itself, and that is load-bearing rather than tidy:
49
+ # ActiveSupport defines +as_json+ and +to_json+ on +Enumerable+, so the refusal only
50
+ # takes effect while it sits *ahead* of Enumerable in the ancestor chain. Pulling
51
+ # Enumerable in here makes that true whichever order an including class writes its
52
+ # +include+ lines — a module already in the chain is not moved by a later include — so
53
+ # the guarantee is structural instead of a rule in a comment that a future resource can
54
+ # forget. The classes still write +include Enumerable+ explicitly, because being
55
+ # Enumerable is part of what they are.
56
+ module NotSerializableCollection
57
+ include Enumerable
58
+ include NotSerializable
59
+
60
+ private
61
+
62
+ # Deliberately does *not* claim a token leak. Measured on this tree: ActiveSupport's
63
+ # +Enumerable#as_json+ shadows +Object#as_json+, so a collection serialized as its
64
+ # fetched records, never as its ivars — the client it holds was never walked. The
65
+ # harm is the unbounded fetch and the records that came out with it.
66
+ def serialization_refusal
67
+ "#{self.class} is a lazy, auto-paginating query, not a record, so there is no one " \
68
+ "record to serialize. Serializing it runs a page-by-page GET loop over the whole " \
69
+ "resource from inside your renderer, and emits every record it fetched. Call " \
70
+ ".to_a (or .first(n), or .filter(...).to_a) and serialize that — then how much you " \
71
+ "fetch is a visible act in your own code."
72
+ end
73
+ end
74
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "api_object"
4
4
  require_relative "pagination"
5
+ require_relative "serialization"
5
6
 
6
7
  module BaseCradle
7
8
  # One credential you hold — a web sign-in or a bc_uat_ API token.
@@ -39,6 +40,7 @@ module BaseCradle
39
40
  # end
40
41
  class SessionsResource
41
42
  include Enumerable
43
+ include NotSerializableCollection
42
44
 
43
45
  def initialize(client)
44
46
  @client = client
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "pagination"
4
+ require_relative "serialization"
4
5
  require_relative "timeline"
5
6
 
6
7
  module BaseCradle
@@ -10,6 +11,7 @@ module BaseCradle
10
11
  # method (+map+, +find+, +first+ — which stop early without fetching every page).
11
12
  class TimelinesResource
12
13
  include Enumerable
14
+ include NotSerializableCollection
13
15
 
14
16
  def initialize(client)
15
17
  @client = client
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "api_object"
4
+ require_relative "serialization"
4
5
 
5
6
  module BaseCradle
6
7
  # The trust relationship between you and another user, from your point of view.
@@ -98,6 +99,7 @@ module BaseCradle
98
99
  # bc.users.each { |user| puts [user.handle, user.kind, user.trust.mutual].inspect }
99
100
  class UsersResource
100
101
  include Enumerable
102
+ include NotSerializableCollection
101
103
 
102
104
  def initialize(client)
103
105
  @client = client
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BaseCradle
4
- VERSION = "0.10.0"
4
+ VERSION = "0.10.2"
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "api_object"
4
4
  require_relative "items"
5
+ require_relative "serialization"
5
6
  require_relative "user"
6
7
 
7
8
  module BaseCradle
@@ -98,6 +99,14 @@ module BaseCradle
98
99
  # that already happened. Converting away from this type gives up the folding too
99
100
  # (+to_h+, <tt>{**headers}</tt>, and anything else that hands back a plain +Hash+);
100
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.
101
110
  class WebhookEventHeaders < Hash
102
111
  # Hash's own exact-match membership, kept under a private name before the case-folding
103
112
  # +key?+ below takes the name: a caller who wrote the wire's own spelling is answered
@@ -166,6 +175,43 @@ module BaseCradle
166
175
  ->(name) { dig(name) }
167
176
  end
168
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
+
169
215
  private
170
216
 
171
217
  # The wire's own spelling of +name+, or nil if no casing of it was delivered. Anything
@@ -260,6 +306,7 @@ module BaseCradle
260
306
  # One timeline's webhook endpoints: create here, or iterate (newest first).
261
307
  class TimelineWebhookEndpoints
262
308
  include Enumerable
309
+ include NotSerializableCollection
263
310
 
264
311
  def initialize(client, timeline_uuid)
265
312
  @client = client
@@ -287,6 +334,7 @@ module BaseCradle
287
334
  # One timeline's webhook events — read-only, so iterate is all there is.
288
335
  class TimelineWebhookEvents
289
336
  include Enumerable
337
+ include NotSerializableCollection
290
338
 
291
339
  def initialize(client, timeline_uuid)
292
340
  @client = client
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.0
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Drawk Kwast
@@ -26,6 +26,7 @@ files:
26
26
  - lib/basecradle/errors.rb
27
27
  - lib/basecradle/items.rb
28
28
  - lib/basecradle/pagination.rb
29
+ - lib/basecradle/serialization.rb
29
30
  - lib/basecradle/sessions.rb
30
31
  - lib/basecradle/timeline.rb
31
32
  - lib/basecradle/timelines.rb