dregs 0.1.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 +7 -0
- data/CHANGELOG.md +27 -0
- data/LICENSE +21 -0
- data/README.md +247 -0
- data/SECURITY.md +36 -0
- data/lib/dregs/client.rb +402 -0
- data/lib/dregs/errors.rb +143 -0
- data/lib/dregs/identities.rb +91 -0
- data/lib/dregs/models.rb +520 -0
- data/lib/dregs/version.rb +6 -0
- data/lib/dregs/webhooks.rb +144 -0
- data/lib/dregs.rb +38 -0
- data/sig/dregs.rbs +286 -0
- metadata +64 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d4f2ed05bc7ad6b7f11275cd1433eb3289d2eb9e0591067bbf238bf65c614e1d
|
|
4
|
+
data.tar.gz: cdd0d2097662216dee63c3453ea925295f62710adbc546d3c3e8ee1e8ae96b9e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5f41a1a52cdc61c9cdd50f8b8fcba9ebd635070946eeaea1330da0494f167138bc9f0c4ebd6b766181f779bf328a5ad221779cc4d72731a32687e1a1efc05271
|
|
7
|
+
data.tar.gz: 86d8d64737dabd2c619b94a45795b774cf5035bfed7dc7494be30f1255ff0c41b2269967e2f52f9941bfdd408baeb33cac6dabccd6b83224ce8ffbb2f84f3b89
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-09-22
|
|
10
|
+
|
|
11
|
+
The first release. A server-side client for tracking events and reading scores.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `Dregs::Client`, authenticating with a credential's `sk_` secret key against a configurable base
|
|
16
|
+
URL, with no runtime dependencies beyond the standard library.
|
|
17
|
+
- `track` for recording backend events against an identity, always sending an idempotency `id` so a
|
|
18
|
+
retry cannot double-count.
|
|
19
|
+
- `identities.get`, `identities.scores`, `identities.analysis`, and `identities.analyze`.
|
|
20
|
+
- Typed errors for 400, 401, 402, 403, 404, 429, and 5xx, plus connection and timeout failures, all
|
|
21
|
+
deriving from `Dregs::Error`.
|
|
22
|
+
- Automatic retries with exponential backoff and jitter, honouring `Retry-After`.
|
|
23
|
+
- `Dregs::Webhooks.verify` for checking a webhook's signature and rejecting replays.
|
|
24
|
+
- Lenient models with a `raw` escape hatch, and RBS signatures in `sig/`.
|
|
25
|
+
|
|
26
|
+
[Unreleased]: https://github.com/dregs-sdk/dregs-sdk-ruby/compare/v0.1.0...HEAD
|
|
27
|
+
[0.1.0]: https://github.com/dregs-sdk/dregs-sdk-ruby/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dregs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Dregs Ruby SDK
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/dregs)
|
|
4
|
+
[](https://www.ruby-lang.org/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
The official Ruby client for [Dregs](https://dregs.com), which scores the users of your application
|
|
8
|
+
for fraud and abuse across four categories: humanity, authenticity, uniqueness, and behavior.
|
|
9
|
+
|
|
10
|
+
Send events from your backend, read back the scores and the observations behind them.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bundle add dregs
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The gem has no runtime dependencies: it uses `net/http`, `json`, and `openssl` from the standard
|
|
17
|
+
library, so adding it cannot conflict with the HTTP client your application already has.
|
|
18
|
+
|
|
19
|
+
## Getting started
|
|
20
|
+
|
|
21
|
+
You need the **secret key** from an API credential, which you will find under **Settings → Credentials**
|
|
22
|
+
in the Dregs dashboard. It starts with `sk_`. The `pk_` public key is for the browser tracker and cannot
|
|
23
|
+
read identities or scores.
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "dregs"
|
|
27
|
+
|
|
28
|
+
client = Dregs::Client.new(secret_key: ENV.fetch("DREGS_SECRET_KEY"))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The key is read from `DREGS_SECRET_KEY` when you do not pass one, so `Dregs::Client.new` on its own is
|
|
32
|
+
usually enough. Build one at startup and keep it; the client holds no per-request state, so it is safe
|
|
33
|
+
to share across threads.
|
|
34
|
+
|
|
35
|
+
## Tracking events
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
client.track(
|
|
39
|
+
"user.signup",
|
|
40
|
+
identity: "user_12345",
|
|
41
|
+
data: { plan: "pro", referrer: "partner-x" },
|
|
42
|
+
identity_data: { email: "ada@example.com", name: "Ada Lovelace" }
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`identity:` is your own id for the user — the same one you pass to `dregs.identify()` in the browser
|
|
47
|
+
tracker, and the one you look scores up by. It is required: a server-side event carries no device
|
|
48
|
+
signature, so the identity is the only thing tying the event to a user.
|
|
49
|
+
|
|
50
|
+
`identity_data:` carries attributes of the *user* rather than the event. The analyzers lean on these
|
|
51
|
+
heavily, so send them whenever you have them. Name the keys the way your application already does and
|
|
52
|
+
map them to Dregs's canonical fields under **Settings → Mappings**; the same goes for event names.
|
|
53
|
+
|
|
54
|
+
### Idempotency
|
|
55
|
+
|
|
56
|
+
Every event is sent with an `id`, which makes ingestion idempotent: reposting the same id returns the
|
|
57
|
+
original event instead of recording a second one. Pass the id your application already has, and a retry
|
|
58
|
+
after a timeout can never double-count.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
client.track("purchase", identity: "user_12345", event_id: "order-#{order.id}")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When you omit it the SDK generates one, which is what makes its own retries safe.
|
|
65
|
+
|
|
66
|
+
### What comes back
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
result = client.track("user.signup", identity: "user_12345")
|
|
70
|
+
|
|
71
|
+
result.accepted? # true when Dregs recorded the event
|
|
72
|
+
result.id # the event's id
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`accepted?` is `false` in the uncommon case where Dregs accepts the request without recording
|
|
76
|
+
an event. Failures that are yours to act on raise instead — see [Errors](#errors).
|
|
77
|
+
|
|
78
|
+
## Reading scores
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
scores = client.identities.scores("user_12345")
|
|
82
|
+
|
|
83
|
+
scores.humanity # 85
|
|
84
|
+
scores.authenticity # 72
|
|
85
|
+
scores.uniqueness # 91
|
|
86
|
+
scores.behavior # 68
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This is the cheap read and the one most integrations want. A category Dregs has not scored yet reads as
|
|
90
|
+
`nil`, and a brand-new identity comes back empty. `Scores` is `Enumerable`, so you can map and sort it.
|
|
91
|
+
|
|
92
|
+
Scoring is **asynchronous**. Scores appear moments after the events that move them, not in the same
|
|
93
|
+
breath, so read them at a decision point rather than immediately after a `track` call.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
hold_for_review("user_12345") if scores.authenticity && scores.authenticity < 40
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Seeing exactly why
|
|
100
|
+
|
|
101
|
+
The scores are the summary; the observations are the evidence. When you need to show or log *why* an
|
|
102
|
+
identity scored the way it did, ask for the analysis.
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
analysis = client.identities.analysis("user_12345")
|
|
106
|
+
|
|
107
|
+
analysis.observations.each do |observation|
|
|
108
|
+
puts "#{observation.label}: #{observation.explanation} (value #{observation.value})"
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each observation carries the analyzer that produced it, a `value` from 0.0 (suspicious) to 1.0
|
|
113
|
+
(legitimate), a `confidence`, a `weight`, and the counts behind the finding in `metadata`. `analysis`
|
|
114
|
+
raises `Dregs::NotFoundError` until the identity has been analyzed at least once.
|
|
115
|
+
|
|
116
|
+
### The whole identity
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
identity = client.identities.get("user_12345")
|
|
120
|
+
|
|
121
|
+
identity.display_email # "ada@example.com"
|
|
122
|
+
identity.humanity_score # 85
|
|
123
|
+
identity.badges # [#<Dregs::Badge name="Account Takeover Suspected" ...>]
|
|
124
|
+
identity.data # every attribute you have sent
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Forcing a rescore
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
client.identities.analyze("user_12345")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This queues the work and returns; it does not wait for the cycle to finish. Dregs rescores on its own
|
|
134
|
+
as events arrive, so you rarely need this outside of a support or backfill flow.
|
|
135
|
+
|
|
136
|
+
## Errors
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
begin
|
|
140
|
+
client.track("user.signup", identity: "user_12345")
|
|
141
|
+
rescue Dregs::QuotaExceededError
|
|
142
|
+
# over the monthly event limit; the event was not queued
|
|
143
|
+
rescue Dregs::RateLimitError => e
|
|
144
|
+
# ingesting too fast; e.retry_after when the server said how long
|
|
145
|
+
rescue Dregs::Error => e
|
|
146
|
+
# anything else this library raises
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
| Exception | When |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `Dregs::BadRequestError` | 400, the event was malformed |
|
|
153
|
+
| `Dregs::AuthenticationError` | 401, the secret key was not recognized |
|
|
154
|
+
| `Dregs::QuotaExceededError` | 402, the account is over its monthly event limit |
|
|
155
|
+
| `Dregs::PermissionDeniedError` | 403, the credential may not do this |
|
|
156
|
+
| `Dregs::NotFoundError` | 404, no such identity, or it has not been analyzed |
|
|
157
|
+
| `Dregs::RateLimitError` | 429, too many requests |
|
|
158
|
+
| `Dregs::ServerError` | 5xx |
|
|
159
|
+
| `Dregs::TimeoutError` | the request timed out |
|
|
160
|
+
| `Dregs::ConnectionError` | the request never reached Dregs |
|
|
161
|
+
|
|
162
|
+
All of them derive from `Dregs::Error`. Those that reached the API also derive from `Dregs::APIError`
|
|
163
|
+
and carry `status_code`, `body`, and `request_id`. A mistake in how you called the SDK — a missing
|
|
164
|
+
secret key, an event id the API would refuse — raises `ArgumentError` before anything leaves the
|
|
165
|
+
process.
|
|
166
|
+
|
|
167
|
+
### Retries
|
|
168
|
+
|
|
169
|
+
Connection failures, timeouts, 429s, and 5xx are retried automatically with exponential backoff and
|
|
170
|
+
jitter, honouring `Retry-After` when the server sends one. Two retries by default:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
client = Dregs::Client.new(max_retries: 5) # or 0 to handle it yourself
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Concurrency
|
|
177
|
+
|
|
178
|
+
The client is synchronous: every call blocks until Dregs answers or the retries run out. Ruby has no
|
|
179
|
+
idiomatic async twin to offer, so there is one client rather than two.
|
|
180
|
+
|
|
181
|
+
It holds no connection state between calls, which makes it safe to share across threads and to call
|
|
182
|
+
from a background job. If you do not want a signup to wait on an HTTP round trip, put the `track` call
|
|
183
|
+
in Sidekiq, Active Job, or a thread pool rather than reaching for a different client.
|
|
184
|
+
|
|
185
|
+
## Webhooks
|
|
186
|
+
|
|
187
|
+
Dregs signs every webhook with the channel's signing secret. Verify it against the **raw request body**
|
|
188
|
+
before acting on the payload — a re-serialized hash will not match, because key order and whitespace
|
|
189
|
+
change.
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
post "/webhooks/dregs" do
|
|
193
|
+
event = Dregs::Webhooks.verify(
|
|
194
|
+
payload: request.body.read,
|
|
195
|
+
signature: request.env["HTTP_X_DREGS_SIGNATURE"],
|
|
196
|
+
secret: ENV.fetch("DREGS_WEBHOOK_SECRET")
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
handle(event)
|
|
200
|
+
rescue Dregs::WebhookVerificationError
|
|
201
|
+
halt 400
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
In Rails the raw body is `request.raw_post`; in Rack it is `request.body.read`, and you may need to
|
|
206
|
+
`rewind` the body if something downstream reads it again. Do not use `params` — by the time Rails has
|
|
207
|
+
parsed and merged them, the bytes that were signed are gone.
|
|
208
|
+
|
|
209
|
+
`verify` also rejects payloads older than five minutes as replays; pass `tolerance: nil` to skip that if
|
|
210
|
+
you are deduplicating on the event id yourself. The signing secret is shown once, when you create the
|
|
211
|
+
webhook channel, and is not your API secret key.
|
|
212
|
+
|
|
213
|
+
## Configuration
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
client = Dregs::Client.new(
|
|
217
|
+
secret_key: nil, # defaults to $DREGS_SECRET_KEY
|
|
218
|
+
base_url: nil, # defaults to $DREGS_BASE_URL, then https://dregs.com/api
|
|
219
|
+
timeout: 10, # seconds, for connecting and for reading
|
|
220
|
+
max_retries: 2
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Type signatures
|
|
225
|
+
|
|
226
|
+
The gem ships RBS signatures in `sig/`, so Steep, RBS, and editors that read them see the full surface
|
|
227
|
+
without stubs. Responses are plain objects with readers; each one also keeps the body it was built from
|
|
228
|
+
in `raw`, so a field Dregs adds after this release is reachable without waiting for an SDK upgrade.
|
|
229
|
+
|
|
230
|
+
## Contributing
|
|
231
|
+
|
|
232
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The short version:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
bundle install
|
|
236
|
+
bundle exec rake # rubocop, rbs validate, and rspec
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Links
|
|
240
|
+
|
|
241
|
+
- [Dregs manual](https://dregs.com/manual/) and [REST API reference](https://dregs.com/manual/api/)
|
|
242
|
+
- [Dregs MCP server](https://github.com/dregs-sdk/dregs-mcp), for connecting AI agents to your data
|
|
243
|
+
- [Security policy](SECURITY.md)
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT. See [LICENSE](LICENSE).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Report security issues in this SDK, or anywhere else in the Dregs service, to
|
|
6
|
+
[security@dregs.com](mailto:security@dregs.com). Dregs's
|
|
7
|
+
[security page](https://dregs.com/legal/security/) describes how reports are handled. Please
|
|
8
|
+
don't open a public GitHub issue for security reports.
|
|
9
|
+
|
|
10
|
+
## Handling keys
|
|
11
|
+
|
|
12
|
+
- The **secret key** (`sk_…`) this SDK authenticates with acts as an administrator of the team
|
|
13
|
+
that owns it. Treat it like a password: keep it in your server's environment or secret manager,
|
|
14
|
+
never in source control, and never anywhere a browser can reach it.
|
|
15
|
+
- The **public key** (`pk_…`) is the one that belongs in a browser. It is a different credential
|
|
16
|
+
and cannot read identities or scores. This SDK refuses one with an explanation rather than
|
|
17
|
+
letting you discover the difference through confusing 401s.
|
|
18
|
+
- A credential's **webhook signing secret** is a third value again. It proves a payload came from
|
|
19
|
+
Dregs; it does not authenticate you to Dregs. Rotate it separately.
|
|
20
|
+
- Revoke a leaked credential under **Settings → Credentials** in the dashboard. Revocation takes
|
|
21
|
+
effect immediately.
|
|
22
|
+
|
|
23
|
+
## Verifying webhooks
|
|
24
|
+
|
|
25
|
+
Verify every webhook before acting on it, against the **raw request body**. `Dregs::Webhooks.verify`
|
|
26
|
+
does the HMAC comparison in constant time, with `OpenSSL.secure_compare`, and rejects stale payloads
|
|
27
|
+
as replays. Verifying a re-serialized hash instead of the received bytes will not match, and working
|
|
28
|
+
around that by skipping verification would leave the endpoint open to anyone who learns its URL. In
|
|
29
|
+
Rails, read `request.raw_post` rather than `params`.
|
|
30
|
+
|
|
31
|
+
## Treating scored data as untrusted
|
|
32
|
+
|
|
33
|
+
The identity attributes, display names, and event data this SDK returns were written by the users
|
|
34
|
+
being scored. Treat them as untrusted input: escape them before rendering, and do not feed them to
|
|
35
|
+
an LLM or an agent as instructions. This matters more than usual here, because the people writing
|
|
36
|
+
that data are, by construction, the ones trying to get something past you.
|