basecradle 0.9.0 → 0.10.1
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 +110 -0
- data/README.md +86 -0
- data/lib/basecradle/api_object.rb +42 -0
- data/lib/basecradle/client.rb +28 -1
- data/lib/basecradle/errors.rb +6 -0
- data/lib/basecradle/items.rb +5 -0
- data/lib/basecradle/pagination.rb +3 -0
- data/lib/basecradle/serialization.rb +74 -0
- data/lib/basecradle/sessions.rb +2 -0
- data/lib/basecradle/timelines.rb +2 -0
- data/lib/basecradle/user.rb +2 -0
- data/lib/basecradle/version.rb +1 -1
- data/lib/basecradle/webhooks.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a805499915d7672ddf23664f7a5147b41915446a488a122c018cb46ac65f076
|
|
4
|
+
data.tar.gz: 5739cfa3f912b9d9d29d03a9a083c58371175b0bd7dd75dbad1ee6209d426288
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3836f2b3204ca798e3d2c4bd17dcc43f59faf0dc86c7097504f05e01519e5d91cc8c10f0889852899c64a816ef7c33da009cd3a2948f0f9cd8019ee279246870
|
|
7
|
+
data.tar.gz: 44894880f4119664ffdceff8d9550b8d69e715073a55ad31287bfbd507ba8a7758bc92003de5fb0f1e8b17919ddf3f0968ff4c6344c988c53f91360aca5c685d
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,111 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
One deliberate departure from that format: this changelog keeps **no `Unreleased`
|
|
8
|
+
section**. The newest heading is always the version `lib/basecradle/version.rb` builds —
|
|
9
|
+
a release writes its entry and its version in the same PR — and `test/changelog_test.rb`
|
|
10
|
+
fails CI if the two ever disagree.
|
|
11
|
+
|
|
12
|
+
## [0.10.1] - 2026-09-30
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
|
|
16
|
+
- **A `Client` no longer serializes, and never emits its token**
|
|
17
|
+
([#198](https://github.com/basecradle/basecradle-ruby/issues/198)). A client is a
|
|
18
|
+
connection, not a record, and it defined no `as_json`/`to_json` — so with ActiveSupport
|
|
19
|
+
loaded it fell through to `Object#as_json`, which is `instance_values`, walked
|
|
20
|
+
recursively. Its instance variables include the raw `bc_uat_` token, so
|
|
21
|
+
`render json: { conn: bc }` or `logger.info({ conn: bc }.to_json)` wrote the live
|
|
22
|
+
credential into a response body or a log line, and **a token in a log is a token to
|
|
23
|
+
rotate**. `bc.to_json` now raises `BaseCradle::NotSerializableError`, a
|
|
24
|
+
`BaseCradle::Error`, naming what to serialize instead. Long-standing and latent; no
|
|
25
|
+
token is known to have been emitted.
|
|
26
|
+
- **`Client#inspect` and `#to_s` redact the token** — the same exposure through a
|
|
27
|
+
different door. Ruby's default `inspect` dumps every instance variable, so the
|
|
28
|
+
credential printed into every exception message, REPL transcript and `p` call that
|
|
29
|
+
touched a client. Both now read
|
|
30
|
+
`#<BaseCradle::Client base_url="https://basecradle.com" token=[REDACTED]>`, with `to_s`
|
|
31
|
+
the same string because interpolation is the door people reach for without thinking.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **A collection resource no longer serializes either.** `bc.timelines`, `bc.messages`,
|
|
36
|
+
`timeline.tasks`, any `.filter(...)`, and the `Paginator` behind them are lazy queries,
|
|
37
|
+
not records. ActiveSupport's `Enumerable#as_json` calls `to_a`, so serializing one ran
|
|
38
|
+
a page-by-page GET loop over the whole resource from **inside a view render** and
|
|
39
|
+
emitted every record it fetched. It did *not* reach the token — `Enumerable#as_json`
|
|
40
|
+
shadows the `instance_values` walk above — but a client's instance variables are eight
|
|
41
|
+
collections, so serializing a client fired those loops on its way to the credential.
|
|
42
|
+
All of them now raise the same `NotSerializableError`, before any HTTP, pointing at
|
|
43
|
+
`.to_a` so how much you fetch is a visible act in your own code.
|
|
44
|
+
Without ActiveSupport these calls were useless rather than dangerous
|
|
45
|
+
(`"#<BaseCradle::MessagesResource:0x…>"`, the heap address 0.10.0 removed for models);
|
|
46
|
+
that is closed too.
|
|
47
|
+
- **The collection resources' `inspect` is unchanged** and still shows Ruby's default
|
|
48
|
+
ivar dump — which renders the client through the redacting `Client#inspect` above, so
|
|
49
|
+
no token reaches it. Pinned by test so it stays that way.
|
|
50
|
+
|
|
51
|
+
### Known limitation
|
|
52
|
+
|
|
53
|
+
The refusals are on the SDK's own objects. An `Enumerator` built from a collection —
|
|
54
|
+
`bc.messages.each`, `bc.messages.lazy` — is a plain Ruby object this SDK does not own,
|
|
55
|
+
and ActiveSupport gives it an `as_json` that calls `to_a`, so
|
|
56
|
+
`render json: { recent: bc.messages.lazy }` still pages the whole resource. No token is
|
|
57
|
+
exposed; call `.first(n)` or `.to_a` before handing a query to a renderer.
|
|
58
|
+
|
|
59
|
+
### Migrating
|
|
60
|
+
|
|
61
|
+
Nothing to change unless you were serializing a `Client` or a collection, which never
|
|
62
|
+
emitted anything useful and, for a client under ActiveSupport, emitted your token. If you
|
|
63
|
+
were: serialize the record you meant (`bc.me`, a timeline, a message), or call `.to_a` /
|
|
64
|
+
`.first(n)` on a collection first.
|
|
65
|
+
|
|
66
|
+
## [0.10.0] - 2026-09-30
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- **A model serializes as the record it stands for**
|
|
71
|
+
([#191](https://github.com/basecradle/basecradle-ruby/issues/191)). `ApiObject` defined
|
|
72
|
+
no `to_json`, so serializing *any* model — `bc.me`, a `Timeline`, a `Message`, any
|
|
73
|
+
content object — fell through to Ruby's default and emitted a heap address:
|
|
74
|
+
`"#<BaseCradle::MessageContent:0x000072be94b09…>"`. Silently, with no field names and a
|
|
75
|
+
different value every run. `model.to_json` and `JSON.generate(model)` now emit the
|
|
76
|
+
record, and so does a model nested in an array or a hash. What `to_h` holds is exactly
|
|
77
|
+
what goes out: nothing renamed, nothing dropped, and a field newer than this SDK
|
|
78
|
+
release serialized like any other.
|
|
79
|
+
`WebhookEventHeaders` is unchanged — it subclasses `Hash`, so it already serialized as
|
|
80
|
+
the delivery's headers.
|
|
81
|
+
Long-standing; 0.9.0's typing of `timeline.items` content only widened which read path
|
|
82
|
+
reached it. **0.9.0's Migrating note on `to_json` is superseded by this release.**
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
|
|
86
|
+
- **`as_json`**, ActiveSupport's serialization hook, returning the same wire `Hash` as
|
|
87
|
+
`to_h`. It is what reaches a model nested inside a structure Rails renders
|
|
88
|
+
(`render json: { user: bc.me }`); a bare `render json: model` calls `to_json` directly
|
|
89
|
+
and never reaches it. Two things to know, neither of which a Rails habit expects:
|
|
90
|
+
- **It returns the same `Hash` as `to_h`, not a copy** — read-only by the same
|
|
91
|
+
convention, where every ActiveSupport `as_json` builds a fresh one. So
|
|
92
|
+
`model.as_json.merge!(extra)` rewrites the model's wire record, and for a wrapped
|
|
93
|
+
child its parent's too. Use `model.to_h.dup` (shallow) if you need to touch it.
|
|
94
|
+
- **Options are accepted and ignored, `only:` and `except:` included** — a model is a
|
|
95
|
+
read of one record, not a presenter. `as_json(except: ["ingest_url"])` returns the
|
|
96
|
+
whole record, silently, and it *disagrees* with `to_json`: with ActiveSupport loaded
|
|
97
|
+
`model.to_json(except: [...])` does redact, because the option reaches `Hash#to_json`.
|
|
98
|
+
Do not route redaction through either — build the subset with
|
|
99
|
+
`model.to_h.except("ingest_url")`.
|
|
100
|
+
|
|
101
|
+
`to_json` forwards its argument to `Hash#to_json` rather than swallowing it, so
|
|
102
|
+
`JSON.pretty_generate` pretty-prints; what an unknown option does is the host app's
|
|
103
|
+
`json` version's business (3.x raises, the 2.x older Rubies ship ignores it).
|
|
104
|
+
|
|
105
|
+
`to_s` is untouched, so interpolating a model (`"#{model}"`, `puts model`) still shows
|
|
106
|
+
the default object form. `inspect` names the model and its fields; `to_json` / `to_h`
|
|
107
|
+
give the record.
|
|
108
|
+
|
|
109
|
+
Minor rather than patch: the `to_json` output is a fix, but `as_json` is new public
|
|
110
|
+
surface and 0.9.0 documented the old behavior as expected — semver takes the higher.
|
|
111
|
+
|
|
7
112
|
## [0.9.0] - 2026-09-30
|
|
8
113
|
|
|
9
114
|
### Changed
|
|
@@ -46,6 +151,9 @@ enrichments come with them — but three things that worked on a `Hash` no longe
|
|
|
46
151
|
field names — because `ApiObject` defines no `to_json`. That is long-standing for
|
|
47
152
|
every other model in the SDK and now reaches this path too. Serialize
|
|
48
153
|
`item.content.to_h` instead.
|
|
154
|
+
> ⚠️ **Superseded in [0.10.0](#0100---2026-09-30)**, which gives `ApiObject` a
|
|
155
|
+
> `to_json`. `item.content.to_json` emits the record; the `to_h` workaround above is
|
|
156
|
+
> no longer needed. Left as written for the historical record.
|
|
49
157
|
- **`inspect` and `==` change, both for the better.** `inspect` names the model and its
|
|
50
158
|
wire fields (`#<BaseCradle::MessageContent body, uuid>`) rather than printing a `Hash`;
|
|
51
159
|
and equality now *holds* between an item's content and the same record fetched
|
|
@@ -342,6 +450,8 @@ the Python SDK's behavior in idiomatic Ruby. Zero runtime dependencies.
|
|
|
342
450
|
- **Quality bars** — a README-as-tested-doc harness (every example runs against a mocked
|
|
343
451
|
API) and a spec drift-guard (CI fails if the live API grows beyond the SDK).
|
|
344
452
|
|
|
453
|
+
[0.10.1]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.1
|
|
454
|
+
[0.10.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.0
|
|
345
455
|
[0.9.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.9.0
|
|
346
456
|
[0.8.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.8.0
|
|
347
457
|
[0.7.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.7.0
|
data/README.md
CHANGED
|
@@ -324,6 +324,92 @@ timeline = bc.timelines.create(name: "Incident response")
|
|
|
324
324
|
timeline.add_participant(nova)
|
|
325
325
|
```
|
|
326
326
|
|
|
327
|
+
## Reading models, and serializing them
|
|
328
|
+
|
|
329
|
+
Every model is a read-only, wire-exact view of one API JSON object. Field readers mirror
|
|
330
|
+
the wire names; `model[key]` is the raw escape hatch for a field newer than your SDK
|
|
331
|
+
release; `model.to_h` is the wire `Hash` itself.
|
|
332
|
+
|
|
333
|
+
Models serialize as the record they stand for — what `to_h` holds is what goes out, so
|
|
334
|
+
logging or re-emitting one gives you the wire record rather than an object address:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
require "basecradle"
|
|
338
|
+
require "json"
|
|
339
|
+
|
|
340
|
+
bc = BaseCradle::Client.new
|
|
341
|
+
message = bc.messages.first
|
|
342
|
+
|
|
343
|
+
puts message.content.to_json # {"uuid":"019e...","body":"Hello from a peer."}
|
|
344
|
+
puts JSON.generate([message.content]) # nested in an array
|
|
345
|
+
puts({ latest: message.content }.to_json) # nested in a hash
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`as_json` is ActiveSupport's hook and returns the same wire `Hash`, so a model nested in
|
|
349
|
+
something Rails renders comes out right too. Three caveats worth knowing:
|
|
350
|
+
|
|
351
|
+
- **`as_json` returns the same `Hash` as `to_h`, not a copy** — read-only by the same
|
|
352
|
+
convention. Mutating it rewrites the model's record. Use `model.to_h.dup` (shallow).
|
|
353
|
+
- **Options are accepted and ignored, `only:` and `except:` included.** A model is a read
|
|
354
|
+
of one record, not a presenter, so `as_json(except: ["ingest_url"])` returns the whole
|
|
355
|
+
record, silently — and it *disagrees* with `to_json`, which under ActiveSupport does
|
|
356
|
+
redact, because the option reaches `Hash#to_json`. Do not route redaction through
|
|
357
|
+
either: build the subset with `model.to_h.except("ingest_url")`.
|
|
358
|
+
- **`to_json` forwards its argument** to `Hash#to_json`, so `JSON.pretty_generate`
|
|
359
|
+
pretty-prints. What an unknown option does is your `json` version's business — 3.x
|
|
360
|
+
raises, the 2.x older Rubies ship ignores it.
|
|
361
|
+
|
|
362
|
+
## What does not serialize: clients and collections
|
|
363
|
+
|
|
364
|
+
Two things in this SDK are *not* records, and serializing either raises
|
|
365
|
+
`BaseCradle::NotSerializableError` rather than emitting something:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
require "basecradle"
|
|
369
|
+
|
|
370
|
+
bc = BaseCradle::Client.new
|
|
371
|
+
|
|
372
|
+
begin
|
|
373
|
+
bc.to_json # and render json: { conn: bc }
|
|
374
|
+
rescue BaseCradle::NotSerializableError => e
|
|
375
|
+
puts e.message.include?("bc_uat_") # => true
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
begin
|
|
379
|
+
bc.messages.to_json # a lazy query, not a record
|
|
380
|
+
rescue BaseCradle::NotSerializableError => e
|
|
381
|
+
puts e.message.include?(".to_a") # => true
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
puts bc.messages.first(20).to_json # serialize the page you actually asked for
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
A **client** holds your `bc_uat_` token, and under ActiveSupport `Object#as_json` is
|
|
388
|
+
`instance_values` — walked recursively. So `render json: { conn: bc }` or
|
|
389
|
+
`logger.info({ conn: bc }.to_json)` used to write the raw credential into a response body
|
|
390
|
+
or a log line, and a token in a log is a token to rotate.
|
|
391
|
+
|
|
392
|
+
A **collection** (`bc.timelines`, `bc.messages`, `timeline.tasks`, any `.filter(...)`) is
|
|
393
|
+
a lazy query. ActiveSupport's `Enumerable#as_json` calls `to_a`, so serializing one ran a
|
|
394
|
+
page-by-page GET loop over the whole resource from inside the render and emitted every
|
|
395
|
+
record it fetched. (It did *not* reach the token — `Enumerable#as_json` shadows the ivar
|
|
396
|
+
walk — but a client's instance variables are eight collections, so serializing a *client*
|
|
397
|
+
ran those loops too, on its way to the credential.) Call `.to_a` or `.first(n)` yourself
|
|
398
|
+
and serialize that, so how much you fetch is a visible act in your code.
|
|
399
|
+
|
|
400
|
+
Both refusals happen before any HTTP — but they are on the SDK's own objects. An
|
|
401
|
+
`Enumerator` you built from one is a plain Ruby object this SDK does not own, so
|
|
402
|
+
`render json: { recent: bc.messages.lazy }` and `bc.messages.each.to_json` still page the
|
|
403
|
+
whole resource. Call `.first(n)` or `.to_a` before handing a query to a renderer.
|
|
404
|
+
|
|
405
|
+
`bc.inspect` and `"#{bc}"` are redacted for the same reason — Ruby's default `inspect`
|
|
406
|
+
dumps every instance variable, which would print the token into every exception message
|
|
407
|
+
and REPL transcript that touched a client:
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
#<BaseCradle::Client base_url="https://basecradle.com" token=[REDACTED]>
|
|
411
|
+
```
|
|
412
|
+
|
|
327
413
|
## Development
|
|
328
414
|
|
|
329
415
|
```bash
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
require_relative "errors"
|
|
4
6
|
|
|
5
7
|
module BaseCradle
|
|
@@ -61,6 +63,46 @@ module BaseCradle
|
|
|
61
63
|
@data
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# The wire record, for a serializer — ActiveSupport's hook, and what reaches a model
|
|
67
|
+
# nested inside a structure Rails renders (<tt>render json: { user: bc.me }</tt>).
|
|
68
|
+
# A bare <tt>render json: model</tt> calls +to_json+ instead and never comes here.
|
|
69
|
+
#
|
|
70
|
+
# Returns +to_h+: the wire keys, nothing renamed, nothing dropped, and a field newer
|
|
71
|
+
# than this release carried like any other. (It is the record *as this object holds
|
|
72
|
+
# it* — for a Timeline fetched whole, that is the API's two-key envelope merged into
|
|
73
|
+
# one object, so it carries +items+ alongside the timeline's own fields.)
|
|
74
|
+
#
|
|
75
|
+
# Two traps, because neither is what a Rails habit expects:
|
|
76
|
+
#
|
|
77
|
+
# - **It is the same Hash +to_h+ returns, not a copy** — read-only by the same
|
|
78
|
+
# convention, where every ActiveSupport +as_json+ builds a fresh one. So
|
|
79
|
+
# <tt>model.as_json.merge!(extra)</tt> rewrites the model's wire record, and for a
|
|
80
|
+
# wrapped child its parent's too (a child wraps the parent's own nested Hash).
|
|
81
|
+
# +to_h.dup+ if you need to touch it; note that is shallow.
|
|
82
|
+
# - **Options are accepted and ignored, +only:+ and +except:+ included** — a model is
|
|
83
|
+
# a read of one record, not a presenter. +as_json(except: ["ingest_url"])+ returns
|
|
84
|
+
# the whole record, *silently*. Worse, it disagrees with +to_json+: with
|
|
85
|
+
# ActiveSupport loaded <tt>model.to_json(except: [...])</tt> *does* redact, because
|
|
86
|
+
# the option reaches +Hash#to_json+. Do not rely on either — build the subset
|
|
87
|
+
# yourself with <tt>model.to_h.except("ingest_url")</tt>.
|
|
88
|
+
def as_json(*)
|
|
89
|
+
to_h
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The wire record as JSON. Without this a model fell through to Ruby's default and
|
|
93
|
+
# emitted a heap address — silently, with no field names and a different value every
|
|
94
|
+
# run. +JSON.generate+ and +Array#to_json+ / +Hash#to_json+ call this, so a model
|
|
95
|
+
# nested in either serializes correctly too.
|
|
96
|
+
#
|
|
97
|
+
# The argument is forwarded to +Hash#to_json+ rather than swallowed, so
|
|
98
|
+
# +JSON.pretty_generate+ pretty-prints — and what an unknown option does is the host
|
|
99
|
+
# app's json version's business, not this SDK's: json 3.x raises on an unknown
|
|
100
|
+
# keyword, the json 2.x that older Rubies ship ignores it, and ActiveSupport acts on
|
|
101
|
+
# +only:+ / +except:+. See +as_json+ above: do not route redaction through either.
|
|
102
|
+
def to_json(*args)
|
|
103
|
+
to_h.to_json(*args)
|
|
104
|
+
end
|
|
105
|
+
|
|
64
106
|
def ==(other)
|
|
65
107
|
other.instance_of?(self.class) && other.to_h == @data
|
|
66
108
|
end
|
data/lib/basecradle/client.rb
CHANGED
|
@@ -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)
|
data/lib/basecradle/errors.rb
CHANGED
|
@@ -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
|
data/lib/basecradle/items.rb
CHANGED
|
@@ -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
|
data/lib/basecradle/sessions.rb
CHANGED
|
@@ -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
|
data/lib/basecradle/timelines.rb
CHANGED
|
@@ -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
|
data/lib/basecradle/user.rb
CHANGED
|
@@ -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
|
data/lib/basecradle/version.rb
CHANGED
data/lib/basecradle/webhooks.rb
CHANGED
|
@@ -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
|
|
@@ -260,6 +261,7 @@ module BaseCradle
|
|
|
260
261
|
# One timeline's webhook endpoints: create here, or iterate (newest first).
|
|
261
262
|
class TimelineWebhookEndpoints
|
|
262
263
|
include Enumerable
|
|
264
|
+
include NotSerializableCollection
|
|
263
265
|
|
|
264
266
|
def initialize(client, timeline_uuid)
|
|
265
267
|
@client = client
|
|
@@ -287,6 +289,7 @@ module BaseCradle
|
|
|
287
289
|
# One timeline's webhook events — read-only, so iterate is all there is.
|
|
288
290
|
class TimelineWebhookEvents
|
|
289
291
|
include Enumerable
|
|
292
|
+
include NotSerializableCollection
|
|
290
293
|
|
|
291
294
|
def initialize(client, timeline_uuid)
|
|
292
295
|
@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.
|
|
4
|
+
version: 0.10.1
|
|
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
|