@dregs/sdk 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
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, ported from the
12
+ reference [Python SDK](https://github.com/dregs-sdk/dregs-sdk-python).
13
+
14
+ ### Added
15
+
16
+ - A `Dregs` client, authenticating with a credential's `sk_` secret key against a configurable
17
+ base URL. Every method returns a promise; there is no separate async client.
18
+ - `track()` for recording backend events against an identity, always sending an idempotency `id`
19
+ so a retry cannot double-count.
20
+ - `identities.get()`, `identities.scores()`, `identities.analysis()`, and `identities.analyze()`.
21
+ - Typed errors for 400, 401, 402, 403, 404, 429, and 5xx, plus connection and timeout failures,
22
+ all deriving from `DregsError`.
23
+ - Automatic retries with exponential backoff and full jitter, honouring `Retry-After`.
24
+ - `verifyWebhook()` for checking a webhook's signature and rejecting replays, exported from the
25
+ package root and from the `@dregs/sdk/webhooks` subpath.
26
+ - ESM and CommonJS builds with generated declarations for both, and no runtime dependencies.
27
+
28
+ [Unreleased]: https://github.com/dregs-sdk/dregs-sdk-typescript/compare/v0.1.0...HEAD
29
+ [0.1.0]: https://github.com/dregs-sdk/dregs-sdk-typescript/releases/tag/v0.1.0
package/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.
package/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # Dregs TypeScript SDK
2
+
3
+ [![npm](https://img.shields.io/npm/v/@dregs/sdk.svg)](https://www.npmjs.com/package/@dregs/sdk)
4
+ [![Node](https://img.shields.io/node/v/@dregs/sdk.svg)](https://www.npmjs.com/package/@dregs/sdk)
5
+ [![License](https://img.shields.io/npm/l/@dregs/sdk.svg)](LICENSE)
6
+
7
+ The official TypeScript 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
+ npm install @dregs/sdk
14
+ ```
15
+
16
+ Node 20 or newer. No runtime dependencies: the SDK calls the runtime's own `fetch`.
17
+
18
+ > This is the **server-side** SDK, which authenticates with a secret key and can read scores.
19
+ > The browser tracking script is a separate package, [`dregs`](https://www.npmjs.com/package/dregs),
20
+ > and uses the public key. You will usually want both: the tracker in the browser, this on your
21
+ > backend.
22
+
23
+ ## Getting started
24
+
25
+ You need the **secret key** from an API credential, which you will find under **Settings → Credentials**
26
+ in the Dregs dashboard. It starts with `sk_`. The `pk_` public key is for the browser tracker and cannot
27
+ read identities or scores.
28
+
29
+ ```ts
30
+ import { Dregs } from '@dregs/sdk';
31
+
32
+ const client = new Dregs({ secretKey: process.env.DREGS_SECRET_KEY });
33
+ ```
34
+
35
+ The key is read from `DREGS_SECRET_KEY` when you do not pass one, so `new Dregs()` on its own is usually
36
+ enough. Build one at startup and keep it; there is nothing to close.
37
+
38
+ CommonJS works too:
39
+
40
+ ```js
41
+ const { Dregs } = require('@dregs/sdk');
42
+ ```
43
+
44
+ ## Tracking events
45
+
46
+ ```ts
47
+ await client.track('user.signup', {
48
+ identity: 'user_12345',
49
+ data: { plan: 'pro', referrer: 'partner-x' },
50
+ identityData: { email: 'ada@example.com', name: 'Ada Lovelace' },
51
+ });
52
+ ```
53
+
54
+ `identity` is your own id for the user — the same one you pass to `dregs.identify()` in the browser
55
+ tracker, and the one you look scores up by. It is required: a server-side event carries no device
56
+ signature, so the identity is the only thing tying the event to a user.
57
+
58
+ `identityData` carries attributes of the _user_ rather than the event. The analyzers lean on these
59
+ heavily, so send them whenever you have them. Name the keys the way your application already does and
60
+ map them to Dregs's canonical fields under **Settings → Mappings**; the same goes for event names.
61
+
62
+ ### Idempotency
63
+
64
+ Every event is sent with an `id`, which makes ingestion idempotent: reposting the same id returns the
65
+ original event instead of recording a second one. Pass the id your application already has, and a retry
66
+ after a timeout can never double-count.
67
+
68
+ ```ts
69
+ await client.track('purchase', { identity: 'user_12345', eventId: `order-${order.id}` });
70
+ ```
71
+
72
+ When you omit it the SDK generates one, which is what makes its own retries safe.
73
+
74
+ ### What comes back
75
+
76
+ ```ts
77
+ const result = await client.track('user.signup', { identity: 'user_12345' });
78
+
79
+ result.accepted; // true when Dregs recorded the event
80
+ result.id; // the event's id
81
+ ```
82
+
83
+ `accepted` is `false` in the uncommon case where Dregs accepts the request without recording
84
+ an event. Failures that are yours to act on throw instead — see [Errors](#errors).
85
+
86
+ ## Reading scores
87
+
88
+ ```ts
89
+ const scores = await client.identities.scores('user_12345');
90
+
91
+ scores.humanity; // 85
92
+ scores.authenticity; // 72
93
+ scores.uniqueness; // 91
94
+ scores.behavior; // 68
95
+ ```
96
+
97
+ This is the cheap read and the one most integrations want. A category Dregs has not scored yet reads as
98
+ `null`, and a brand-new identity comes back empty. `Scores` is an array, so you can iterate, map, and
99
+ destructure it as usual.
100
+
101
+ Scoring is **asynchronous**. Scores appear moments after the events that move them, not in the same
102
+ breath, so read them at a decision point rather than immediately after a `track()` call.
103
+
104
+ ```ts
105
+ if (scores.authenticity !== null && scores.authenticity < 40) {
106
+ await holdForReview('user_12345');
107
+ }
108
+ ```
109
+
110
+ ### Seeing exactly why
111
+
112
+ The scores are the summary; the observations are the evidence. When you need to show or log _why_ an
113
+ identity scored the way it did, ask for the analysis.
114
+
115
+ ```ts
116
+ const analysis = await client.identities.analysis('user_12345');
117
+
118
+ for (const observation of analysis.observations) {
119
+ console.log(`${observation.label}: ${observation.explanation} (value ${observation.value})`);
120
+ }
121
+ ```
122
+
123
+ Each observation carries the analyzer that produced it, a `value` from 0.0 (suspicious) to 1.0
124
+ (legitimate), a `confidence`, a `weight`, and the counts behind the finding in `metadata`. `analysis()`
125
+ throws `NotFoundError` until the identity has been analyzed at least once.
126
+
127
+ ### The whole identity
128
+
129
+ ```ts
130
+ const identity = await client.identities.get('user_12345');
131
+
132
+ identity.displayEmail; // "ada@example.com"
133
+ identity.humanityScore; // 85
134
+ identity.badges; // [{ name: "Account Takeover Suspected", ... }]
135
+ identity.data; // every attribute you have sent
136
+ ```
137
+
138
+ ### Forcing a rescore
139
+
140
+ ```ts
141
+ await client.identities.analyze('user_12345');
142
+ ```
143
+
144
+ This queues the work and resolves; it does not wait for the cycle to finish. Dregs rescores on its own
145
+ as events arrive, so you rarely need this outside of a support or backfill flow.
146
+
147
+ ## Errors
148
+
149
+ ```ts
150
+ import { DregsError, NotFoundError, QuotaExceededError, RateLimitError } from '@dregs/sdk';
151
+
152
+ try {
153
+ await client.track('user.signup', { identity: 'user_12345' });
154
+ } catch (error) {
155
+ if (error instanceof QuotaExceededError) {
156
+ // over the monthly event limit; the event was not queued
157
+ } else if (error instanceof RateLimitError) {
158
+ // ingesting too fast; error.retryAfter when the server said how long
159
+ } else if (error instanceof DregsError) {
160
+ // anything else this library throws
161
+ } else {
162
+ throw error;
163
+ }
164
+ }
165
+ ```
166
+
167
+ | Error | When |
168
+ | ----------------------- | -------------------------------------------------- |
169
+ | `BadRequestError` | 400, the event was malformed |
170
+ | `AuthenticationError` | 401, the secret key was not recognized |
171
+ | `QuotaExceededError` | 402, the account is over its monthly event limit |
172
+ | `PermissionDeniedError` | 403, the credential may not do this |
173
+ | `NotFoundError` | 404, no such identity, or it has not been analyzed |
174
+ | `RateLimitError` | 429, too many requests |
175
+ | `ServerError` | 5xx |
176
+ | `DregsTimeoutError` | the request timed out |
177
+ | `DregsConnectionError` | the request never reached Dregs |
178
+
179
+ All of them derive from `DregsError`. Those that reached the API also carry `statusCode`, `body`, and
180
+ `requestId`; `error.message` is the message the API sent, and `error.toString()` prefixes it with the
181
+ status and the request id, which is the form worth putting in a log line.
182
+
183
+ Arguments the SDK can reject without asking Dregs — a missing identity, an event id over 64 characters,
184
+ a `pk_` key — throw a plain `TypeError` before anything is sent.
185
+
186
+ ### Retries
187
+
188
+ Connection failures, timeouts, 408s, 429s, and 5xx are retried automatically with exponential backoff and
189
+ full jitter, honouring `Retry-After` when the server sends one. Two retries by default:
190
+
191
+ ```ts
192
+ const client = new Dregs({ maxRetries: 5 }); // or 0 to handle it yourself
193
+ ```
194
+
195
+ ## Promises, not an async twin
196
+
197
+ There is one `Dregs` class and every method returns a promise. JavaScript has no meaningful sync/async
198
+ split, so unlike the Python SDK there is no async client to choose between — `await` everything.
199
+
200
+ ## Webhooks
201
+
202
+ Dregs signs every webhook with the channel's signing secret. Verify it against the **raw request body**
203
+ before acting on the payload — a re-serialized object will not match, because key order and whitespace
204
+ change.
205
+
206
+ ```ts
207
+ import express from 'express';
208
+ import { verifyWebhook, WebhookVerificationError } from '@dregs/sdk/webhooks';
209
+
210
+ app.post('/webhooks/dregs', express.raw({ type: 'application/json' }), (req, res) => {
211
+ let event;
212
+
213
+ try {
214
+ event = verifyWebhook({
215
+ payload: req.body, // the Buffer, not req.body parsed as JSON
216
+ signature: req.header('X-Dregs-Signature') ?? '',
217
+ secret: process.env.DREGS_WEBHOOK_SECRET!,
218
+ });
219
+ } catch (error) {
220
+ if (error instanceof WebhookVerificationError) {
221
+ return res.sendStatus(400);
222
+ }
223
+
224
+ throw error;
225
+ }
226
+
227
+ handle(event);
228
+
229
+ res.sendStatus(204);
230
+ });
231
+ ```
232
+
233
+ `express.raw()` matters: the default `express.json()` hands you a parsed object and the original bytes are
234
+ gone. The helpers are exported from the package root as well, so `import { verifyWebhook } from '@dregs/sdk'`
235
+ works if you would rather not reach for the subpath.
236
+
237
+ `verifyWebhook` also rejects payloads older than five minutes as replays; pass `tolerance: null` to skip
238
+ that if you are deduplicating on the event id yourself. The signing secret is shown once, when you create
239
+ the webhook channel, and is not your API secret key.
240
+
241
+ ## Configuration
242
+
243
+ ```ts
244
+ const client = new Dregs({
245
+ secretKey: undefined, // defaults to $DREGS_SECRET_KEY
246
+ baseUrl: undefined, // defaults to $DREGS_BASE_URL, then https://dregs.com/api
247
+ timeout: 10_000, // milliseconds
248
+ maxRetries: 2,
249
+ fetch: undefined, // bring your own fetch for a proxy agent, custom TLS, or instrumentation
250
+ });
251
+ ```
252
+
253
+ ## Type checking
254
+
255
+ The package ships generated declarations for both the ESM and CommonJS entry points, so there is no
256
+ `@types/dregs` to install and every public type is exported. Responses are plain readonly objects; each
257
+ one also keeps the body it was built from in `raw`, so a field Dregs adds after this release is reachable
258
+ without waiting for an SDK upgrade.
259
+
260
+ ```ts
261
+ import type { Analysis, Category, Identity, Observation, Score, TrackResult } from '@dregs/sdk';
262
+ ```
263
+
264
+ ## Contributing
265
+
266
+ See [CONTRIBUTING.md](CONTRIBUTING.md). The short version:
267
+
268
+ ```bash
269
+ npm ci
270
+ npm test
271
+ npm run lint
272
+ npm run typecheck
273
+ npm run build
274
+ ```
275
+
276
+ `npm ci` installs exactly what `package-lock.json` pins and fails if the lock is out of step, so the same
277
+ commands produce the same environment locally and in CI.
278
+
279
+ ## Links
280
+
281
+ - [Dregs manual](https://dregs.com/manual/) and [REST API reference](https://dregs.com/manual/api/)
282
+ - [Dregs MCP server](https://github.com/dregs-sdk/dregs-mcp), for connecting AI agents to your data
283
+ - [Security policy](SECURITY.md)
284
+
285
+ ## License
286
+
287
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,149 @@
1
+ import { createHmac, timingSafeEqual } from 'crypto';
2
+
3
+ // src/webhooks.ts
4
+
5
+ // src/errors.ts
6
+ var DregsError = class extends Error {
7
+ constructor(message, options) {
8
+ super(message, options);
9
+ Object.setPrototypeOf(this, new.target.prototype);
10
+ this.name = new.target.name;
11
+ }
12
+ };
13
+ var DregsConnectionError = class extends DregsError {
14
+ };
15
+ var DregsTimeoutError = class extends DregsConnectionError {
16
+ };
17
+ var WebhookVerificationError = class extends DregsError {
18
+ };
19
+ var DregsAPIError = class extends DregsError {
20
+ /** The HTTP status code. */
21
+ statusCode;
22
+ /** The parsed JSON body, or `null` when the response was not JSON. */
23
+ body;
24
+ /**
25
+ * Value of the `X-Request-Id` response header, when present.
26
+ *
27
+ * Quote it when you report a problem: it is what lets Dregs find your exact request.
28
+ */
29
+ requestId;
30
+ constructor(message, options) {
31
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
32
+ this.statusCode = options.statusCode;
33
+ this.body = options.body ?? null;
34
+ this.requestId = options.requestId ?? null;
35
+ }
36
+ toString() {
37
+ const suffix = this.requestId ? ` (request ${this.requestId})` : "";
38
+ return `${this.name}: HTTP ${this.statusCode}: ${this.message}${suffix}`;
39
+ }
40
+ };
41
+ var BadRequestError = class extends DregsAPIError {
42
+ };
43
+ var AuthenticationError = class extends DregsAPIError {
44
+ };
45
+ var QuotaExceededError = class extends DregsAPIError {
46
+ };
47
+ var PermissionDeniedError = class extends DregsAPIError {
48
+ };
49
+ var NotFoundError = class extends DregsAPIError {
50
+ };
51
+ var RateLimitError = class extends DregsAPIError {
52
+ /**
53
+ * Seconds to wait before retrying, from the `Retry-After` header when the response carried a
54
+ * numeric one, and `null` otherwise.
55
+ */
56
+ retryAfter;
57
+ constructor(message, options = {}) {
58
+ super(message, { ...options, statusCode: options.statusCode ?? 429 });
59
+ this.retryAfter = options.retryAfter ?? null;
60
+ }
61
+ };
62
+ var ServerError = class extends DregsAPIError {
63
+ };
64
+ var STATUS_ERRORS = {
65
+ 400: BadRequestError,
66
+ 401: AuthenticationError,
67
+ 402: QuotaExceededError,
68
+ 403: PermissionDeniedError,
69
+ 404: NotFoundError
70
+ };
71
+ function errorForStatus(statusCode) {
72
+ const mapped = STATUS_ERRORS[statusCode];
73
+ if (mapped) {
74
+ return mapped;
75
+ }
76
+ return statusCode >= 500 ? ServerError : DregsAPIError;
77
+ }
78
+
79
+ // src/webhooks.ts
80
+ var SIGNATURE_HEADER = "X-Dregs-Signature";
81
+ var TIMESTAMP_HEADER = "X-Dregs-Timestamp";
82
+ var EVENT_HEADER = "X-Dregs-Event";
83
+ var DEFAULT_TOLERANCE_SECONDS = 300;
84
+ function computeWebhookSignature(payload, secret) {
85
+ return createHmac("sha256", secret).update(toBytes(payload)).digest("hex");
86
+ }
87
+ function verifyWebhookSignature(payload, signature, secret) {
88
+ if (!signature || !secret) {
89
+ return false;
90
+ }
91
+ const expected = Buffer.from(computeWebhookSignature(payload, secret), "utf8");
92
+ const received = Buffer.from(signature.trim(), "utf8");
93
+ return expected.length === received.length && timingSafeEqual(expected, received);
94
+ }
95
+ function verifyWebhook(options) {
96
+ const { payload, signature, secret, tolerance = DEFAULT_TOLERANCE_SECONDS, now } = options;
97
+ if (!verifyWebhookSignature(payload, signature, secret)) {
98
+ throw new WebhookVerificationError(
99
+ "The webhook signature did not match. Check that you are verifying the raw request body rather than a re-serialized copy, and that the signing secret belongs to the channel that sent this delivery."
100
+ );
101
+ }
102
+ let event;
103
+ try {
104
+ event = JSON.parse(toText(payload));
105
+ } catch (cause) {
106
+ throw new WebhookVerificationError(
107
+ `The webhook body was not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`,
108
+ { cause }
109
+ );
110
+ }
111
+ if (typeof event !== "object" || event === null || Array.isArray(event)) {
112
+ throw new WebhookVerificationError("The webhook body was not a JSON object.");
113
+ }
114
+ const body = event;
115
+ if (tolerance !== null && tolerance !== void 0) {
116
+ checkFreshness(body, tolerance, now);
117
+ }
118
+ return body;
119
+ }
120
+ function checkFreshness(event, tolerance, now) {
121
+ const raw = event.timestamp;
122
+ if (typeof raw !== "string" || !raw) {
123
+ throw new WebhookVerificationError(
124
+ "The webhook carried no timestamp, so it cannot be checked for replay. Pass tolerance: null if you are deduplicating deliveries some other way."
125
+ );
126
+ }
127
+ const sent = new Date(raw);
128
+ if (Number.isNaN(sent.getTime())) {
129
+ throw new WebhookVerificationError(
130
+ `The webhook timestamp was unreadable: ${JSON.stringify(raw)}`
131
+ );
132
+ }
133
+ const age = Math.abs(((now ?? /* @__PURE__ */ new Date()).getTime() - sent.getTime()) / 1e3);
134
+ if (age > tolerance) {
135
+ throw new WebhookVerificationError(
136
+ `The webhook timestamp is ${age.toFixed(0)}s away from now, beyond the ${tolerance}s tolerance. Treating it as a replay.`
137
+ );
138
+ }
139
+ }
140
+ function toBytes(payload) {
141
+ return typeof payload === "string" ? Buffer.from(payload, "utf8") : Buffer.from(payload);
142
+ }
143
+ function toText(payload) {
144
+ return typeof payload === "string" ? payload : Buffer.from(payload).toString("utf8");
145
+ }
146
+
147
+ export { AuthenticationError, BadRequestError, DEFAULT_TOLERANCE_SECONDS, DregsAPIError, DregsConnectionError, DregsError, DregsTimeoutError, EVENT_HEADER, NotFoundError, PermissionDeniedError, QuotaExceededError, RateLimitError, SIGNATURE_HEADER, ServerError, TIMESTAMP_HEADER, WebhookVerificationError, computeWebhookSignature, errorForStatus, verifyWebhook, verifyWebhookSignature };
148
+ //# sourceMappingURL=chunk-AIUWT3Z7.js.map
149
+ //# sourceMappingURL=chunk-AIUWT3Z7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/webhooks.ts"],"names":[],"mappings":"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","file":"chunk-AIUWT3Z7.js","sourcesContent":["/**\n * Errors thrown by the Dregs SDK.\n *\n * Everything this library throws derives from {@link DregsError}, so a caller that only wants a\n * coarse \"the Dregs call failed\" branch can catch that one class. Errors that came back from the\n * API carry the HTTP status and the parsed body; errors that never reached the API (DNS failure,\n * connection refused, timeout) derive from {@link DregsConnectionError} instead.\n *\n * @module\n */\n\n/**\n * Base class for everything this library throws.\n *\n * `instanceof DregsError` is the one check that catches every failure mode, including webhook\n * verification and transport failures that never produced an HTTP status.\n */\nexport class DregsError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n\n // Subclassing a built-in loses the prototype link under a downlevelled target, so pin it\n // back on. Without this, `err instanceof RateLimitError` can quietly answer false.\n Object.setPrototypeOf(this, new.target.prototype);\n\n this.name = new.target.name;\n }\n}\n\n/** The request never reached Dregs: DNS, TCP, TLS, or a dropped connection. */\nexport class DregsConnectionError extends DregsError {}\n\n/** The request was still outstanding when the configured timeout elapsed. */\nexport class DregsTimeoutError extends DregsConnectionError {}\n\n/** An incoming webhook did not verify against the channel's signing secret. */\nexport class WebhookVerificationError extends DregsError {}\n\n/** The fields carried by every error that reached the API and came back an error. */\nexport interface DregsAPIErrorOptions {\n /** The HTTP status code. */\n statusCode: number;\n /** The parsed JSON body, or `null` when the response was not JSON. */\n body?: unknown;\n /** Value of the `X-Request-Id` response header, when the response carried one. */\n requestId?: string | null;\n /** The underlying cause, when there is one worth keeping. */\n cause?: unknown;\n}\n\n/**\n * Dregs answered, and the answer was an error.\n *\n * `message` is the human-readable message the API sent, so `err.message` reads the way a JS\n * caller expects. `toString()` prefixes it with the status and the request id, which is the form\n * worth putting in a log line when you open a support ticket.\n */\nexport class DregsAPIError extends DregsError {\n /** The HTTP status code. */\n readonly statusCode: number;\n\n /** The parsed JSON body, or `null` when the response was not JSON. */\n readonly body: unknown;\n\n /**\n * Value of the `X-Request-Id` response header, when present.\n *\n * Quote it when you report a problem: it is what lets Dregs find your exact request.\n */\n readonly requestId: string | null;\n\n constructor(message: string, options: DregsAPIErrorOptions) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n\n this.statusCode = options.statusCode;\n this.body = options.body ?? null;\n this.requestId = options.requestId ?? null;\n }\n\n override toString(): string {\n const suffix = this.requestId ? ` (request ${this.requestId})` : '';\n\n return `${this.name}: HTTP ${this.statusCode}: ${this.message}${suffix}`;\n }\n}\n\n/**\n * 400. The request was malformed or missing something Dregs requires.\n *\n * For event ingestion this most often means the event carried neither an identity nor a device,\n * or the body failed validation.\n */\nexport class BadRequestError extends DregsAPIError {}\n\n/** 401. The secret key was missing, unrecognized, revoked, or expired. */\nexport class AuthenticationError extends DregsAPIError {}\n\n/**\n * 402. The account is over its monthly event limit and ingestion is refused.\n *\n * Events are not queued while an account is over its limit, so the caller decides whether to drop\n * the event or hold it. The limit resets with the billing period; upgrading the plan clears it\n * immediately.\n */\nexport class QuotaExceededError extends DregsAPIError {}\n\n/** 403. The credential authenticated but is not allowed to do this. */\nexport class PermissionDeniedError extends DregsAPIError {}\n\n/** 404. No such identity, or no analysis has been run for it yet. */\nexport class NotFoundError extends DregsAPIError {}\n\n/** The fields carried by a 429, on top of the usual API error fields. */\nexport interface RateLimitErrorOptions extends Omit<DregsAPIErrorOptions, 'statusCode'> {\n statusCode?: number;\n /** Seconds to wait before retrying, from the `Retry-After` header. */\n retryAfter?: number | null;\n}\n\n/**\n * 429. The credential exceeded its request rate limit.\n *\n * The client retries these on its own; you only see one when the retries were exhausted or turned\n * off. Wait {@link RateLimitError.retryAfter} seconds before trying again when it is set.\n */\nexport class RateLimitError extends DregsAPIError {\n /**\n * Seconds to wait before retrying, from the `Retry-After` header when the response carried a\n * numeric one, and `null` otherwise.\n */\n readonly retryAfter: number | null;\n\n constructor(message: string, options: RateLimitErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 429 });\n\n this.retryAfter = options.retryAfter ?? null;\n }\n}\n\n/** 5xx. Something went wrong inside Dregs. These are retried automatically. */\nexport class ServerError extends DregsAPIError {}\n\nconst STATUS_ERRORS: Readonly<\n Record<number, new (message: string, options: DregsAPIErrorOptions) => DregsAPIError>\n> = {\n 400: BadRequestError,\n 401: AuthenticationError,\n 402: QuotaExceededError,\n 403: PermissionDeniedError,\n 404: NotFoundError,\n};\n\n/**\n * Returns the error class that represents `statusCode`.\n *\n * 429 is deliberately absent from the table: it needs the `Retry-After` header, so the client\n * builds a {@link RateLimitError} directly rather than going through here.\n */\nexport function errorForStatus(\n statusCode: number,\n): new (message: string, options: DregsAPIErrorOptions) => DregsAPIError {\n const mapped = STATUS_ERRORS[statusCode];\n\n if (mapped) {\n return mapped;\n }\n\n return statusCode >= 500 ? ServerError : DregsAPIError;\n}\n","/**\n * Verifying webhooks Dregs sends you.\n *\n * Dregs signs every webhook with the channel's signing secret: `X-Dregs-Signature` is the\n * hex-encoded HMAC-SHA256 of the raw request body. Verify it before you act on the payload, and\n * verify it against the bytes you received rather than a re-serialized object, because\n * re-serializing changes key order and whitespace and will not match.\n *\n * ```ts\n * import { verifyWebhook } from '@dregs/sdk/webhooks';\n *\n * app.post('/webhooks/dregs', express.raw({ type: 'application/json' }), (req, res) => {\n * const event = verifyWebhook({\n * payload: req.body, // the Buffer, not req.body parsed as JSON\n * signature: req.header('X-Dregs-Signature') ?? '',\n * secret: process.env.DREGS_WEBHOOK_SECRET!,\n * });\n *\n * handle(event);\n * });\n * ```\n *\n * The signing secret is shown once, when you create the webhook channel. It is not your API\n * secret key: one authenticates you to Dregs, the other proves a payload came from Dregs.\n *\n * @module\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport { WebhookVerificationError } from './errors.js';\n\n/** The header carrying the hex-encoded HMAC-SHA256 of the raw body. */\nexport const SIGNATURE_HEADER = 'X-Dregs-Signature';\n\n/** The header carrying the delivery's timestamp. */\nexport const TIMESTAMP_HEADER = 'X-Dregs-Timestamp';\n\n/** The header naming the event type. */\nexport const EVENT_HEADER = 'X-Dregs-Event';\n\n/** How far out of date a webhook's timestamp may be before {@link verifyWebhook} rejects it. */\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\n/**\n * A raw webhook body.\n *\n * A `Buffer` or `Uint8Array` is what you want: the bytes exactly as received. A string is\n * accepted for frameworks that hand you the raw text, and is hashed as UTF-8.\n */\nexport type WebhookPayload = string | Uint8Array;\n\n/** A verified webhook body: `event`, `timestamp`, and the payload for that event. */\nexport type WebhookEvent = Record<string, unknown>;\n\n/** The arguments to {@link verifyWebhook}. */\nexport interface VerifyWebhookOptions {\n /** The raw request body, exactly as received. Not a parsed object. */\n payload: WebhookPayload;\n\n /** The `X-Dregs-Signature` header. */\n signature: string;\n\n /** The channel's signing secret. */\n secret: string;\n\n /**\n * How many seconds out of date the payload's own `timestamp` may be before it is treated as a\n * replay. Defaults to 300. Pass `null` to skip the check, which you should only do if you are\n * deduplicating on the event id yourself.\n *\n * The timestamp is inside the signed body, so an attacker cannot alter it without breaking\n * the signature.\n */\n tolerance?: number | null;\n\n /** The current time. For tests. */\n now?: Date;\n}\n\n/** Returns the hex-encoded HMAC-SHA256 of `payload` under `secret`. */\nexport function computeWebhookSignature(payload: WebhookPayload, secret: string): string {\n return createHmac('sha256', secret).update(toBytes(payload)).digest('hex');\n}\n\n/**\n * Returns whether `signature` matches `payload`.\n *\n * The comparison is constant-time. Prefer {@link verifyWebhook}, which also rejects replays and\n * hands back the parsed event; reach for this one only when you need the boolean.\n */\nexport function verifyWebhookSignature(\n payload: WebhookPayload,\n signature: string,\n secret: string,\n): boolean {\n if (!signature || !secret) {\n return false;\n }\n\n const expected = Buffer.from(computeWebhookSignature(payload, secret), 'utf8');\n const received = Buffer.from(signature.trim(), 'utf8');\n\n // timingSafeEqual throws on a length mismatch rather than returning false, and a wrong-length\n // signature is wrong regardless; the length is not a secret.\n return expected.length === received.length && timingSafeEqual(expected, received);\n}\n\n/**\n * Verifies a webhook and returns its parsed body.\n *\n * @returns The parsed webhook body: `event`, `timestamp`, and the payload for that event.\n * @throws {WebhookVerificationError} The signature did not match, the body was not a JSON\n * object, or the payload is older than the tolerance. Answer 400 and do not act on it.\n */\nexport function verifyWebhook(options: VerifyWebhookOptions): WebhookEvent {\n const { payload, signature, secret, tolerance = DEFAULT_TOLERANCE_SECONDS, now } = options;\n\n if (!verifyWebhookSignature(payload, signature, secret)) {\n throw new WebhookVerificationError(\n 'The webhook signature did not match. Check that you are verifying the raw request body ' +\n 'rather than a re-serialized copy, and that the signing secret belongs to the channel ' +\n 'that sent this delivery.',\n );\n }\n\n let event: unknown;\n\n try {\n event = JSON.parse(toText(payload));\n } catch (cause) {\n throw new WebhookVerificationError(\n `The webhook body was not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`,\n { cause },\n );\n }\n\n if (typeof event !== 'object' || event === null || Array.isArray(event)) {\n throw new WebhookVerificationError('The webhook body was not a JSON object.');\n }\n\n const body = event as WebhookEvent;\n\n if (tolerance !== null && tolerance !== undefined) {\n checkFreshness(body, tolerance, now);\n }\n\n return body;\n}\n\nfunction checkFreshness(event: WebhookEvent, tolerance: number, now: Date | undefined): void {\n const raw = event.timestamp;\n\n if (typeof raw !== 'string' || !raw) {\n throw new WebhookVerificationError(\n 'The webhook carried no timestamp, so it cannot be checked for replay. Pass ' +\n 'tolerance: null if you are deduplicating deliveries some other way.',\n );\n }\n\n const sent = new Date(raw);\n\n if (Number.isNaN(sent.getTime())) {\n throw new WebhookVerificationError(\n `The webhook timestamp was unreadable: ${JSON.stringify(raw)}`,\n );\n }\n\n const age = Math.abs(((now ?? new Date()).getTime() - sent.getTime()) / 1000);\n\n if (age > tolerance) {\n throw new WebhookVerificationError(\n `The webhook timestamp is ${age.toFixed(0)}s away from now, beyond the ${tolerance}s ` +\n 'tolerance. Treating it as a replay.',\n );\n }\n}\n\nfunction toBytes(payload: WebhookPayload): Buffer {\n return typeof payload === 'string' ? Buffer.from(payload, 'utf8') : Buffer.from(payload);\n}\n\nfunction toText(payload: WebhookPayload): string {\n return typeof payload === 'string' ? payload : Buffer.from(payload).toString('utf8');\n}\n"]}