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 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
+ [![Gem](https://img.shields.io/gem/v/dregs.svg)](https://rubygems.org/gems/dregs)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-CC342D.svg)](https://www.ruby-lang.org/)
5
+ [![License](https://img.shields.io/github/license/dregs-sdk/dregs-sdk-ruby.svg)](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.