@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 +29 -0
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/dist/chunk-AIUWT3Z7.js +149 -0
- package/dist/chunk-AIUWT3Z7.js.map +1 -0
- package/dist/chunk-K63AUEQR.cjs +170 -0
- package/dist/chunk-K63AUEQR.cjs.map +1 -0
- package/dist/index.cjs +645 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +576 -0
- package/dist/index.d.ts +576 -0
- package/dist/index.js +554 -0
- package/dist/index.js.map +1 -0
- package/dist/webhooks.cjs +36 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +83 -0
- package/dist/webhooks.d.ts +83 -0
- package/dist/webhooks.js +3 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { DregsTimeoutError, DregsConnectionError, DregsAPIError, RateLimitError, QuotaExceededError, errorForStatus } from './chunk-AIUWT3Z7.js';
|
|
2
|
+
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 } from './chunk-AIUWT3Z7.js';
|
|
3
|
+
|
|
4
|
+
// src/version.ts
|
|
5
|
+
var VERSION = "0.1.0";
|
|
6
|
+
|
|
7
|
+
// src/models.ts
|
|
8
|
+
var CATEGORIES = [
|
|
9
|
+
"HUMANITY",
|
|
10
|
+
"AUTHENTICITY",
|
|
11
|
+
"UNIQUENESS",
|
|
12
|
+
"BEHAVIOR"
|
|
13
|
+
];
|
|
14
|
+
var Scores = class extends Array {
|
|
15
|
+
/**
|
|
16
|
+
* `map`, `filter`, and `slice` return plain arrays rather than trying to rebuild a `Scores`
|
|
17
|
+
* through a constructor whose shape they know nothing about.
|
|
18
|
+
*/
|
|
19
|
+
static get [Symbol.species]() {
|
|
20
|
+
return Array;
|
|
21
|
+
}
|
|
22
|
+
constructor(items = []) {
|
|
23
|
+
super();
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
this.push(item);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Returns the score for `category`, or `null` when it has not been scored. */
|
|
29
|
+
get(category) {
|
|
30
|
+
return this.find((score) => score.category === category) ?? null;
|
|
31
|
+
}
|
|
32
|
+
value(category) {
|
|
33
|
+
return this.get(category)?.value ?? null;
|
|
34
|
+
}
|
|
35
|
+
/** How likely it is that a person, rather than a script, is behind the account. */
|
|
36
|
+
get humanity() {
|
|
37
|
+
return this.value("HUMANITY");
|
|
38
|
+
}
|
|
39
|
+
/** How genuine the details on the account look. */
|
|
40
|
+
get authenticity() {
|
|
41
|
+
return this.value("AUTHENTICITY");
|
|
42
|
+
}
|
|
43
|
+
/** How distinct the account is from others in the same tenant. */
|
|
44
|
+
get uniqueness() {
|
|
45
|
+
return this.value("UNIQUENESS");
|
|
46
|
+
}
|
|
47
|
+
/** How ordinary the account's activity looks. */
|
|
48
|
+
get behavior() {
|
|
49
|
+
return this.value("BEHAVIOR");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function asPayload(value) {
|
|
53
|
+
return isPayload(value) ? value : {};
|
|
54
|
+
}
|
|
55
|
+
function isPayload(value) {
|
|
56
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
function asString(value) {
|
|
59
|
+
return typeof value === "string" ? value : null;
|
|
60
|
+
}
|
|
61
|
+
function asNumber(value) {
|
|
62
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
63
|
+
}
|
|
64
|
+
function asDate(value) {
|
|
65
|
+
if (typeof value !== "string" || !value) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const parsed = new Date(value);
|
|
69
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
70
|
+
}
|
|
71
|
+
function asCategory(value) {
|
|
72
|
+
return typeof value === "string" && CATEGORIES.includes(value) ? value : null;
|
|
73
|
+
}
|
|
74
|
+
function asArray(value) {
|
|
75
|
+
return Array.isArray(value) ? value.filter(isPayload) : [];
|
|
76
|
+
}
|
|
77
|
+
function parseTrackResult(payload) {
|
|
78
|
+
const body = asPayload(payload);
|
|
79
|
+
const id = asString(body.id);
|
|
80
|
+
return {
|
|
81
|
+
status: asString(body.status),
|
|
82
|
+
id,
|
|
83
|
+
fingerprint: asString(body.fingerprint),
|
|
84
|
+
accepted: id !== null,
|
|
85
|
+
raw: body
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseBadge(payload) {
|
|
89
|
+
const body = asPayload(payload);
|
|
90
|
+
return {
|
|
91
|
+
slug: asString(body.slug),
|
|
92
|
+
name: asString(body.name),
|
|
93
|
+
type: asString(body.type),
|
|
94
|
+
explanation: asString(body.explanation),
|
|
95
|
+
metadata: asPayload(body.metadata),
|
|
96
|
+
raw: body
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseObservation(payload) {
|
|
100
|
+
const body = asPayload(payload);
|
|
101
|
+
return {
|
|
102
|
+
category: asCategory(body.category),
|
|
103
|
+
id: asString(body.id),
|
|
104
|
+
label: asString(body.label),
|
|
105
|
+
explanation: asString(body.explanation),
|
|
106
|
+
value: asNumber(body.value),
|
|
107
|
+
confidence: asNumber(body.confidence),
|
|
108
|
+
weight: asNumber(body.weight),
|
|
109
|
+
metadata: asPayload(body.metadata),
|
|
110
|
+
raw: body
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function parseScore(payload) {
|
|
114
|
+
const body = asPayload(payload);
|
|
115
|
+
return {
|
|
116
|
+
category: asCategory(body.category),
|
|
117
|
+
value: asNumber(body.value),
|
|
118
|
+
observations: asArray(body.observations).map(parseObservation),
|
|
119
|
+
raw: body
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function parseScores(payload) {
|
|
123
|
+
return new Scores(asArray(payload).map(parseScore));
|
|
124
|
+
}
|
|
125
|
+
function parseIdentity(payload) {
|
|
126
|
+
const body = asPayload(payload);
|
|
127
|
+
const humanityScore = asNumber(body.humanityScore);
|
|
128
|
+
const authenticityScore = asNumber(body.authenticityScore);
|
|
129
|
+
const uniquenessScore = asNumber(body.uniquenessScore);
|
|
130
|
+
const behaviorScore = asNumber(body.behaviorScore);
|
|
131
|
+
const pairs = [
|
|
132
|
+
["HUMANITY", humanityScore],
|
|
133
|
+
["AUTHENTICITY", authenticityScore],
|
|
134
|
+
["UNIQUENESS", uniquenessScore],
|
|
135
|
+
["BEHAVIOR", behaviorScore]
|
|
136
|
+
];
|
|
137
|
+
const scores = new Scores(
|
|
138
|
+
pairs.filter(([, value]) => value !== null).map(([category, value]) => ({ category, value, observations: [], raw: {} }))
|
|
139
|
+
);
|
|
140
|
+
return {
|
|
141
|
+
id: asString(body.id),
|
|
142
|
+
displayName: asString(body.displayName),
|
|
143
|
+
displayEmail: asString(body.displayEmail),
|
|
144
|
+
displayUsername: asString(body.displayUsername),
|
|
145
|
+
humanityScore,
|
|
146
|
+
authenticityScore,
|
|
147
|
+
uniquenessScore,
|
|
148
|
+
behaviorScore,
|
|
149
|
+
createdAt: asDate(body.createdAt),
|
|
150
|
+
updatedAt: asDate(body.updatedAt),
|
|
151
|
+
lastTrackedAt: asDate(body.lastTrackedAt),
|
|
152
|
+
lastScoredAt: asDate(body.lastScoredAt),
|
|
153
|
+
disregarded: body.disregarded === true,
|
|
154
|
+
badges: asArray(body.badges).map(parseBadge),
|
|
155
|
+
data: asPayload(body.data),
|
|
156
|
+
scores,
|
|
157
|
+
raw: body
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function parseAnalysis(payload) {
|
|
161
|
+
const body = asPayload(payload);
|
|
162
|
+
const scores = parseScores(body.scores);
|
|
163
|
+
return {
|
|
164
|
+
id: asNumber(body.id),
|
|
165
|
+
identityId: asString(body.identityId),
|
|
166
|
+
scores,
|
|
167
|
+
observations: scores.flatMap((score) => [...score.observations]),
|
|
168
|
+
eventCount: asNumber(body.eventCount),
|
|
169
|
+
deviceCount: asNumber(body.deviceCount),
|
|
170
|
+
durationMillis: asNumber(body.durationMillis),
|
|
171
|
+
startedAt: asDate(body.startedAt),
|
|
172
|
+
finishedAt: asDate(body.finishedAt),
|
|
173
|
+
raw: body
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/resources.ts
|
|
178
|
+
function path(identityId, suffix = "") {
|
|
179
|
+
if (!identityId) {
|
|
180
|
+
throw new TypeError("An identity id is required.");
|
|
181
|
+
}
|
|
182
|
+
return `/identities/${encodeURIComponent(identityId)}${suffix}`;
|
|
183
|
+
}
|
|
184
|
+
var Identities = class {
|
|
185
|
+
#request;
|
|
186
|
+
/** @internal */
|
|
187
|
+
constructor(request) {
|
|
188
|
+
this.#request = request;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Returns the identity, with its current scores, badges, and attributes.
|
|
192
|
+
*
|
|
193
|
+
* @param identityId Your own id for the user, the one you pass to `track()`.
|
|
194
|
+
* @throws {NotFoundError} Dregs has never seen this identity.
|
|
195
|
+
*/
|
|
196
|
+
async get(identityId) {
|
|
197
|
+
return parseIdentity(await this.#request("GET", path(identityId)));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns the current category scores.
|
|
201
|
+
*
|
|
202
|
+
* This is the cheap read and the one most integrations want. It reports the scores Dregs has
|
|
203
|
+
* already computed without triggering any work. For the observations behind them, use
|
|
204
|
+
* {@link Identities.analysis}.
|
|
205
|
+
*
|
|
206
|
+
* A category that has not been scored yet is absent, so a brand-new identity comes back empty.
|
|
207
|
+
*
|
|
208
|
+
* @throws {NotFoundError} Dregs has never seen this identity.
|
|
209
|
+
*/
|
|
210
|
+
async scores(identityId) {
|
|
211
|
+
return parseScores(await this.#request("GET", path(identityId, "/scores")));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Returns the most recent analysis cycle, with the observations behind each score.
|
|
215
|
+
*
|
|
216
|
+
* Use this when you need to show or log *why* an identity scored the way it did.
|
|
217
|
+
*
|
|
218
|
+
* @throws {NotFoundError} The identity is unknown, or it has not been analyzed yet.
|
|
219
|
+
*/
|
|
220
|
+
async analysis(identityId) {
|
|
221
|
+
return parseAnalysis(await this.#request("GET", path(identityId, "/analysis")));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Queues a re-analysis of the identity.
|
|
225
|
+
*
|
|
226
|
+
* Scoring is asynchronous: this resolves as soon as the job is queued, not when it has run.
|
|
227
|
+
* Poll {@link Identities.scores} or watch for a webhook rather than expecting fresh scores on
|
|
228
|
+
* the next line.
|
|
229
|
+
*
|
|
230
|
+
* @throws {NotFoundError} Dregs has never seen this identity.
|
|
231
|
+
*/
|
|
232
|
+
async analyze(identityId) {
|
|
233
|
+
await this.#request("POST", path(identityId, "/actions/analyze"));
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/client.ts
|
|
238
|
+
var DEFAULT_BASE_URL = "https://dregs.com/api";
|
|
239
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
240
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
241
|
+
var SECRET_KEY_ENV = "DREGS_SECRET_KEY";
|
|
242
|
+
var BASE_URL_ENV = "DREGS_BASE_URL";
|
|
243
|
+
var DEFAULT_SOURCE = "node-sdk";
|
|
244
|
+
var RETRY_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
245
|
+
var STATUS_RATE_LIMITED = "rate_limited";
|
|
246
|
+
var STATUS_QUOTA_EXCEEDED = "quota_exceeded";
|
|
247
|
+
var MAX_BACKOFF_MS = 8e3;
|
|
248
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
249
|
+
var Dregs = class {
|
|
250
|
+
/** Read identities, their scores, and their analysis. */
|
|
251
|
+
identities;
|
|
252
|
+
#secretKey;
|
|
253
|
+
#baseUrl;
|
|
254
|
+
#timeout;
|
|
255
|
+
#maxRetries;
|
|
256
|
+
#fetch;
|
|
257
|
+
/**
|
|
258
|
+
* @param options Configuration. Every field has a default, so `new Dregs()` reads the
|
|
259
|
+
* environment and is usually enough.
|
|
260
|
+
* @throws {TypeError} No secret key was found, the key given is a `pk_` public key, or
|
|
261
|
+
* `maxRetries` is negative.
|
|
262
|
+
*/
|
|
263
|
+
constructor(options = {}) {
|
|
264
|
+
const secretKey = options.secretKey ?? readEnv(SECRET_KEY_ENV);
|
|
265
|
+
if (!secretKey) {
|
|
266
|
+
throw new TypeError(
|
|
267
|
+
`No Dregs secret key. Pass { secretKey: '...' } or set the ${SECRET_KEY_ENV} environment variable. You will find your credential's secret key under Settings -> Credentials in the Dregs dashboard.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (secretKey.startsWith("pk_")) {
|
|
271
|
+
throw new TypeError(
|
|
272
|
+
"That is a public key. The public key is for the browser tracker and cannot read identities or scores; this SDK needs the secret key from the same credential, which starts with 'sk_'."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
276
|
+
if (maxRetries < 0 || !Number.isInteger(maxRetries)) {
|
|
277
|
+
throw new TypeError("maxRetries must be a non-negative integer.");
|
|
278
|
+
}
|
|
279
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
280
|
+
if (timeout <= 0) {
|
|
281
|
+
throw new TypeError("timeout must be greater than zero.");
|
|
282
|
+
}
|
|
283
|
+
const resolvedFetch = options.fetch ?? globalThis.fetch;
|
|
284
|
+
if (!resolvedFetch) {
|
|
285
|
+
throw new TypeError(
|
|
286
|
+
"No global fetch. This SDK needs Node 20 or newer, or a fetch implementation passed as the fetch option."
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
this.#secretKey = secretKey;
|
|
290
|
+
this.#baseUrl = (options.baseUrl ?? readEnv(BASE_URL_ENV) ?? DEFAULT_BASE_URL).replace(
|
|
291
|
+
/\/+$/,
|
|
292
|
+
""
|
|
293
|
+
);
|
|
294
|
+
this.#timeout = timeout;
|
|
295
|
+
this.#maxRetries = maxRetries;
|
|
296
|
+
this.#fetch = resolvedFetch;
|
|
297
|
+
const request = (method, path2, body) => this.request(method, path2, body);
|
|
298
|
+
this.identities = new Identities(request);
|
|
299
|
+
}
|
|
300
|
+
/** The API root every request is built against, with any trailing slash removed. */
|
|
301
|
+
get baseUrl() {
|
|
302
|
+
return this.#baseUrl;
|
|
303
|
+
}
|
|
304
|
+
/** How many times a failed request is retried before the error is thrown. */
|
|
305
|
+
get maxRetries() {
|
|
306
|
+
return this.#maxRetries;
|
|
307
|
+
}
|
|
308
|
+
/** Milliseconds before a request is abandoned. */
|
|
309
|
+
get timeout() {
|
|
310
|
+
return this.#timeout;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Records a backend event against an identity.
|
|
314
|
+
*
|
|
315
|
+
* ```ts
|
|
316
|
+
* await client.track('user.signup', {
|
|
317
|
+
* identity: 'user_12345',
|
|
318
|
+
* data: { plan: 'pro', referrer: 'partner-x' },
|
|
319
|
+
* identityData: { email: 'ada@example.com', name: 'Ada Lovelace' },
|
|
320
|
+
* });
|
|
321
|
+
* ```
|
|
322
|
+
*
|
|
323
|
+
* @param type Your name for the event, such as `"user.signup"`. Map it to one of Dregs's
|
|
324
|
+
* canonical types under **Settings → Mappings** so the analyzers know what it means.
|
|
325
|
+
* @param options The identity the event belongs to, and anything else worth sending.
|
|
326
|
+
* @returns The outcome. Check `.accepted` to confirm Dregs recorded the event.
|
|
327
|
+
* @throws {QuotaExceededError} The account is over its monthly event limit.
|
|
328
|
+
* @throws {RateLimitError} The credential is ingesting too fast.
|
|
329
|
+
* @throws {AuthenticationError} The secret key was not recognized.
|
|
330
|
+
* @throws {BadRequestError} The event was malformed.
|
|
331
|
+
* @throws {TypeError} The event type, identity, or event id was unusable. These are thrown
|
|
332
|
+
* before anything is sent.
|
|
333
|
+
*/
|
|
334
|
+
async track(type, options) {
|
|
335
|
+
return parseTrackResult(await this.request("POST", "/events", this.trackBody(type, options)));
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Builds the `POST /api/events` body.
|
|
339
|
+
*
|
|
340
|
+
* An event id is always sent. When the caller has an id of their own it is used verbatim, so
|
|
341
|
+
* reposting the same event is a no-op on the Dregs side; otherwise one is generated, which is
|
|
342
|
+
* what makes this client's own retries safe to perform.
|
|
343
|
+
*/
|
|
344
|
+
trackBody(type, options) {
|
|
345
|
+
if (!type) {
|
|
346
|
+
throw new TypeError("An event type is required.");
|
|
347
|
+
}
|
|
348
|
+
if (!options?.identity) {
|
|
349
|
+
throw new TypeError(
|
|
350
|
+
"An identity is required. A server-side event has no device signature, so the identity is the only thing tying the event to a user."
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const eventId = options.eventId ?? randomEventId();
|
|
354
|
+
if (eventId.startsWith("dregs-")) {
|
|
355
|
+
throw new TypeError("Event ids starting with 'dregs-' are reserved for Dregs itself.");
|
|
356
|
+
}
|
|
357
|
+
if (eventId.length > 64) {
|
|
358
|
+
throw new TypeError("Event ids cannot be longer than 64 characters.");
|
|
359
|
+
}
|
|
360
|
+
const body = {
|
|
361
|
+
id: eventId,
|
|
362
|
+
type,
|
|
363
|
+
data: { ...options.data ?? {} },
|
|
364
|
+
identity: { id: options.identity, data: { ...options.identityData ?? {} } },
|
|
365
|
+
source: options.source ?? DEFAULT_SOURCE
|
|
366
|
+
};
|
|
367
|
+
if (options.timestamp !== void 0) {
|
|
368
|
+
body.timestamp = toIsoUtc(options.timestamp);
|
|
369
|
+
}
|
|
370
|
+
return body;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Sends one request, retrying what is worth retrying, and returns the parsed JSON body.
|
|
374
|
+
*
|
|
375
|
+
* The retry loop reuses the request body verbatim, which is what keeps a retried event
|
|
376
|
+
* idempotent: the generated id is built once, before the first attempt.
|
|
377
|
+
*/
|
|
378
|
+
async request(method, path2, body) {
|
|
379
|
+
const url = `${this.#baseUrl}/${path2.replace(/^\/+/, "")}`;
|
|
380
|
+
const init = this.requestInit(method, body);
|
|
381
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
382
|
+
let response;
|
|
383
|
+
let text;
|
|
384
|
+
try {
|
|
385
|
+
response = await this.#fetch(url, { ...init, signal: AbortSignal.timeout(this.#timeout) });
|
|
386
|
+
text = await response.text();
|
|
387
|
+
} catch (cause) {
|
|
388
|
+
if (!this.shouldRetry(attempt, null)) {
|
|
389
|
+
throw isTimeout(cause) ? new DregsTimeoutError(
|
|
390
|
+
`The request to ${url} did not answer within ${this.#timeout}ms.`,
|
|
391
|
+
{ cause }
|
|
392
|
+
) : new DregsConnectionError(`Could not reach Dregs at ${url}: ${describe(cause)}`, {
|
|
393
|
+
cause
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
await sleep(this.backoff(attempt, null));
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
return processResponse(response, text);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof DregsAPIError && this.shouldRetry(attempt, error.statusCode)) {
|
|
403
|
+
await sleep(
|
|
404
|
+
this.backoff(attempt, error instanceof RateLimitError ? error.retryAfter : null)
|
|
405
|
+
);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
requestInit(method, body) {
|
|
413
|
+
const headers = {
|
|
414
|
+
Authorization: `Bearer ${this.#secretKey}`,
|
|
415
|
+
Accept: "application/json",
|
|
416
|
+
"User-Agent": userAgent()
|
|
417
|
+
};
|
|
418
|
+
if (body === void 0) {
|
|
419
|
+
return { method, headers };
|
|
420
|
+
}
|
|
421
|
+
headers["Content-Type"] = "application/json";
|
|
422
|
+
return { method, headers, body: JSON.stringify(body) };
|
|
423
|
+
}
|
|
424
|
+
shouldRetry(attempt, statusCode) {
|
|
425
|
+
if (attempt >= this.#maxRetries) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
return statusCode === null || RETRY_STATUSES.has(statusCode);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Milliseconds to wait before attempt `attempt + 1`.
|
|
432
|
+
*
|
|
433
|
+
* `Retry-After` wins when the server sent one. Otherwise this is exponential with full jitter,
|
|
434
|
+
* which keeps a fleet of workers that all hit the limit at once from retrying in lockstep.
|
|
435
|
+
*
|
|
436
|
+
* Protected rather than private so a test can stub the waiting out.
|
|
437
|
+
*/
|
|
438
|
+
backoff(attempt, retryAfterSeconds) {
|
|
439
|
+
if (retryAfterSeconds !== null && retryAfterSeconds >= 0) {
|
|
440
|
+
return Math.min(retryAfterSeconds * 1e3, MAX_RETRY_AFTER_MS);
|
|
441
|
+
}
|
|
442
|
+
return Math.random() * Math.min(500 * 2 ** attempt, MAX_BACKOFF_MS);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
function processResponse(response, text) {
|
|
446
|
+
const payload = parseJson(text);
|
|
447
|
+
if (response.status >= 400) {
|
|
448
|
+
throw apiError(response, payload);
|
|
449
|
+
}
|
|
450
|
+
if (isRecord(payload)) {
|
|
451
|
+
if (payload.status === STATUS_RATE_LIMITED) {
|
|
452
|
+
throw new RateLimitError("Ingestion rate limit exceeded for this credential.", {
|
|
453
|
+
body: payload,
|
|
454
|
+
requestId: requestId(response),
|
|
455
|
+
retryAfter: retryAfter(response)
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (payload.status === STATUS_QUOTA_EXCEEDED) {
|
|
459
|
+
throw new QuotaExceededError("The account is over its monthly event limit.", {
|
|
460
|
+
statusCode: 402,
|
|
461
|
+
body: payload,
|
|
462
|
+
requestId: requestId(response)
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return payload;
|
|
467
|
+
}
|
|
468
|
+
function apiError(response, payload) {
|
|
469
|
+
const message = messageFrom(payload) ?? response.statusText ?? "Request failed";
|
|
470
|
+
const id = requestId(response);
|
|
471
|
+
if (response.status === 429) {
|
|
472
|
+
return new RateLimitError(message, {
|
|
473
|
+
body: payload,
|
|
474
|
+
requestId: id,
|
|
475
|
+
retryAfter: retryAfter(response)
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
const ErrorClass = errorForStatus(response.status);
|
|
479
|
+
return new ErrorClass(message, { statusCode: response.status, body: payload, requestId: id });
|
|
480
|
+
}
|
|
481
|
+
function messageFrom(payload) {
|
|
482
|
+
if (!isRecord(payload)) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
for (const key of ["message", "error", "status"]) {
|
|
486
|
+
const value = payload[key];
|
|
487
|
+
if (typeof value === "string" && value) {
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
function requestId(response) {
|
|
494
|
+
return response.headers.get("X-Request-Id");
|
|
495
|
+
}
|
|
496
|
+
function retryAfter(response) {
|
|
497
|
+
const raw = response.headers.get("Retry-After");
|
|
498
|
+
if (raw === null) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
const seconds = Number(raw);
|
|
502
|
+
return Number.isFinite(seconds) ? seconds : null;
|
|
503
|
+
}
|
|
504
|
+
function parseJson(text) {
|
|
505
|
+
if (!text) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
return JSON.parse(text);
|
|
510
|
+
} catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function isRecord(value) {
|
|
515
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
516
|
+
}
|
|
517
|
+
function isTimeout(cause) {
|
|
518
|
+
const names = [nameOf(cause), nameOf(cause?.cause)];
|
|
519
|
+
return names.includes("TimeoutError") || names.includes("AbortError");
|
|
520
|
+
}
|
|
521
|
+
function nameOf(value) {
|
|
522
|
+
if (typeof value !== "object" || value === null || !("name" in value)) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const { name } = value;
|
|
526
|
+
return typeof name === "string" ? name : null;
|
|
527
|
+
}
|
|
528
|
+
function describe(cause) {
|
|
529
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
530
|
+
}
|
|
531
|
+
function sleep(ms) {
|
|
532
|
+
return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
533
|
+
}
|
|
534
|
+
function readEnv(name) {
|
|
535
|
+
return typeof process === "undefined" ? void 0 : process.env[name];
|
|
536
|
+
}
|
|
537
|
+
function randomEventId() {
|
|
538
|
+
return globalThis.crypto.randomUUID().replace(/-/g, "");
|
|
539
|
+
}
|
|
540
|
+
function toIsoUtc(value) {
|
|
541
|
+
const moment = value instanceof Date ? value : new Date(value);
|
|
542
|
+
if (Number.isNaN(moment.getTime())) {
|
|
543
|
+
throw new TypeError(`The timestamp ${JSON.stringify(value)} is not a valid date.`);
|
|
544
|
+
}
|
|
545
|
+
return moment.toISOString().replace(/\.000Z$/, "Z");
|
|
546
|
+
}
|
|
547
|
+
function userAgent() {
|
|
548
|
+
const runtime = typeof process !== "undefined" && process.versions?.node ? `node ${process.versions.node}` : "unknown";
|
|
549
|
+
return `dregs-node/${VERSION} (${runtime})`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export { BASE_URL_ENV, CATEGORIES, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_SOURCE, DEFAULT_TIMEOUT_MS, Dregs, Identities, SECRET_KEY_ENV, Scores, VERSION };
|
|
553
|
+
//# sourceMappingURL=index.js.map
|
|
554
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/version.ts","../src/models.ts","../src/resources.ts","../src/client.ts"],"names":["path"],"mappings":";;;;AAOO,IAAM,OAAA,GAAU;;;ACchB,IAAM,UAAA,GAAkC;AAAA,EAC7C,UAAA;AAAA,EACA,cAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF;AA0HO,IAAM,MAAA,GAAN,cAAqB,KAAA,CAAa;AAAA;AAAA;AAAA;AAAA;AAAA,EAKvC,YAAqB,MAAA,CAAO,OAAO,CAAA,GAAsB;AACvD,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAEA,WAAA,CAAY,KAAA,GAA0B,EAAC,EAAG;AACxC,IAAA,KAAA,EAAM;AAEN,IAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,MAAA,IAAA,CAAK,KAAK,IAAI,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,QAAA,EAAkC;AACpC,IAAA,OAAO,KAAK,IAAA,CAAK,CAAC,UAAU,KAAA,CAAM,QAAA,KAAa,QAAQ,CAAA,IAAK,IAAA;AAAA,EAC9D;AAAA,EAEQ,MAAM,QAAA,EAAmC;AAC/C,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,QAAQ,CAAA,EAAG,KAAA,IAAS,IAAA;AAAA,EACtC;AAAA;AAAA,EAGA,IAAI,QAAA,GAA0B;AAC5B,IAAA,OAAO,IAAA,CAAK,MAAM,UAAU,CAAA;AAAA,EAC9B;AAAA;AAAA,EAGA,IAAI,YAAA,GAA8B;AAChC,IAAA,OAAO,IAAA,CAAK,MAAM,cAAc,CAAA;AAAA,EAClC;AAAA;AAAA,EAGA,IAAI,UAAA,GAA4B;AAC9B,IAAA,OAAO,IAAA,CAAK,MAAM,YAAY,CAAA;AAAA,EAChC;AAAA;AAAA,EAGA,IAAI,QAAA,GAA0B;AAC5B,IAAA,OAAO,IAAA,CAAK,MAAM,UAAU,CAAA;AAAA,EAC9B;AACF;AAqGA,SAAS,UAAU,KAAA,EAA4B;AAC7C,EAAA,OAAO,SAAA,CAAU,KAAK,CAAA,GAAI,KAAA,GAAQ,EAAC;AACrC;AAEA,SAAS,UAAU,KAAA,EAAqC;AACtD,EAAA,OAAO,OAAO,UAAU,QAAA,IAAY,KAAA,KAAU,QAAQ,CAAC,KAAA,CAAM,QAAQ,KAAK,CAAA;AAC5E;AAEA,SAAS,SAAS,KAAA,EAA+B;AAC/C,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,GAAQ,IAAA;AAC7C;AAEA,SAAS,SAAS,KAAA,EAA+B;AAC/C,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,QAAA,CAAS,KAAK,IAAI,KAAA,GAAQ,IAAA;AACvE;AAEA,SAAS,OAAO,KAAA,EAA6B;AAC3C,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,KAAA,EAAO;AACvC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,KAAK,CAAA;AAE7B,EAAA,OAAO,OAAO,KAAA,CAAM,MAAA,CAAO,OAAA,EAAS,IAAI,IAAA,GAAO,MAAA;AACjD;AAEA,SAAS,WAAW,KAAA,EAAiC;AACnD,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAa,WAAiC,QAAA,CAAS,KAAK,IAC/E,KAAA,GACD,IAAA;AACN;AAEA,SAAS,QAAQ,KAAA,EAA8B;AAC7C,EAAA,OAAO,KAAA,CAAM,QAAQ,KAAK,CAAA,GAAI,MAAM,MAAA,CAAO,SAAS,IAAI,EAAC;AAC3D;AAGO,SAAS,iBAAiB,OAAA,EAA+B;AAC9D,EAAA,MAAM,IAAA,GAAO,UAAU,OAAO,CAAA;AAC9B,EAAA,MAAM,EAAA,GAAK,QAAA,CAAS,IAAA,CAAK,EAAE,CAAA;AAE3B,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA;AAAA,IAC5B,EAAA;AAAA,IACA,WAAA,EAAa,QAAA,CAAS,IAAA,CAAK,WAAW,CAAA;AAAA,IACtC,UAAU,EAAA,KAAO,IAAA;AAAA,IACjB,GAAA,EAAK;AAAA,GACP;AACF;AAGO,SAAS,WAAW,OAAA,EAAyB;AAClD,EAAA,MAAM,IAAA,GAAO,UAAU,OAAO,CAAA;AAE9B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA;AAAA,IACxB,IAAA,EAAM,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA;AAAA,IACxB,IAAA,EAAM,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA;AAAA,IACxB,WAAA,EAAa,QAAA,CAAS,IAAA,CAAK,WAAW,CAAA;AAAA,IACtC,QAAA,EAAU,SAAA,CAAU,IAAA,CAAK,QAAQ,CAAA;AAAA,IACjC,GAAA,EAAK;AAAA,GACP;AACF;AAGO,SAAS,iBAAiB,OAAA,EAA+B;AAC9D,EAAA,MAAM,IAAA,GAAO,UAAU,OAAO,CAAA;AAE9B,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,UAAA,CAAW,IAAA,CAAK,QAAQ,CAAA;AAAA,IAClC,EAAA,EAAI,QAAA,CAAS,IAAA,CAAK,EAAE,CAAA;AAAA,IACpB,KAAA,EAAO,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA;AAAA,IAC1B,WAAA,EAAa,QAAA,CAAS,IAAA,CAAK,WAAW,CAAA;AAAA,IACtC,KAAA,EAAO,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA;AAAA,IAC1B,UAAA,EAAY,QAAA,CAAS,IAAA,CAAK,UAAU,CAAA;AAAA,IACpC,MAAA,EAAQ,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA;AAAA,IAC5B,QAAA,EAAU,SAAA,CAAU,IAAA,CAAK,QAAQ,CAAA;AAAA,IACjC,GAAA,EAAK;AAAA,GACP;AACF;AAGO,SAAS,WAAW,OAAA,EAAyB;AAClD,EAAA,MAAM,IAAA,GAAO,UAAU,OAAO,CAAA;AAE9B,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,UAAA,CAAW,IAAA,CAAK,QAAQ,CAAA;AAAA,IAClC,KAAA,EAAO,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA;AAAA,IAC1B,cAAc,OAAA,CAAQ,IAAA,CAAK,YAAY,CAAA,CAAE,IAAI,gBAAgB,CAAA;AAAA,IAC7D,GAAA,EAAK;AAAA,GACP;AACF;AAGO,SAAS,YAAY,OAAA,EAA0B;AACpD,EAAA,OAAO,IAAI,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,CAAE,GAAA,CAAI,UAAU,CAAC,CAAA;AACpD;AAGO,SAAS,cAAc,OAAA,EAA4B;AACxD,EAAA,MAAM,IAAA,GAAO,UAAU,OAAO,CAAA;AAE9B,EAAA,MAAM,aAAA,GAAgB,QAAA,CAAS,IAAA,CAAK,aAAa,CAAA;AACjD,EAAA,MAAM,iBAAA,GAAoB,QAAA,CAAS,IAAA,CAAK,iBAAiB,CAAA;AACzD,EAAA,MAAM,eAAA,GAAkB,QAAA,CAAS,IAAA,CAAK,eAAe,CAAA;AACrD,EAAA,MAAM,aAAA,GAAgB,QAAA,CAAS,IAAA,CAAK,aAAa,CAAA;AAEjD,EAAA,MAAM,KAAA,GAAyD;AAAA,IAC7D,CAAC,YAAY,aAAa,CAAA;AAAA,IAC1B,CAAC,gBAAgB,iBAAiB,CAAA;AAAA,IAClC,CAAC,cAAc,eAAe,CAAA;AAAA,IAC9B,CAAC,YAAY,aAAa;AAAA,GAC5B;AAEA,EAAA,MAAM,SAAS,IAAI,MAAA;AAAA,IACjB,KAAA,CACG,MAAA,CAAO,CAAC,GAAG,KAAK,CAAA,KAAM,KAAA,KAAU,IAAI,CAAA,CACpC,GAAA,CAAI,CAAC,CAAC,QAAA,EAAU,KAAK,CAAA,MAAO,EAAE,QAAA,EAAU,KAAA,EAAO,YAAA,EAAc,EAAC,EAAG,GAAA,EAAK,EAAC,EAAE,CAAE;AAAA,GAChF;AAEA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,QAAA,CAAS,IAAA,CAAK,EAAE,CAAA;AAAA,IACpB,WAAA,EAAa,QAAA,CAAS,IAAA,CAAK,WAAW,CAAA;AAAA,IACtC,YAAA,EAAc,QAAA,CAAS,IAAA,CAAK,YAAY,CAAA;AAAA,IACxC,eAAA,EAAiB,QAAA,CAAS,IAAA,CAAK,eAAe,CAAA;AAAA,IAC9C,aAAA;AAAA,IACA,iBAAA;AAAA,IACA,eAAA;AAAA,IACA,aAAA;AAAA,IACA,SAAA,EAAW,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA;AAAA,IAChC,SAAA,EAAW,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA;AAAA,IAChC,aAAA,EAAe,MAAA,CAAO,IAAA,CAAK,aAAa,CAAA;AAAA,IACxC,YAAA,EAAc,MAAA,CAAO,IAAA,CAAK,YAAY,CAAA;AAAA,IACtC,WAAA,EAAa,KAAK,WAAA,KAAgB,IAAA;AAAA,IAClC,QAAQ,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAA,CAAE,IAAI,UAAU,CAAA;AAAA,IAC3C,IAAA,EAAM,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA;AAAA,IACzB,MAAA;AAAA,IACA,GAAA,EAAK;AAAA,GACP;AACF;AAGO,SAAS,cAAc,OAAA,EAA4B;AACxD,EAAA,MAAM,IAAA,GAAO,UAAU,OAAO,CAAA;AAC9B,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,IAAA,CAAK,MAAM,CAAA;AAEtC,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,QAAA,CAAS,IAAA,CAAK,EAAE,CAAA;AAAA,IACpB,UAAA,EAAY,QAAA,CAAS,IAAA,CAAK,UAAU,CAAA;AAAA,IACpC,MAAA;AAAA,IACA,YAAA,EAAc,OAAO,OAAA,CAAQ,CAAC,UAAU,CAAC,GAAG,KAAA,CAAM,YAAY,CAAC,CAAA;AAAA,IAC/D,UAAA,EAAY,QAAA,CAAS,IAAA,CAAK,UAAU,CAAA;AAAA,IACpC,WAAA,EAAa,QAAA,CAAS,IAAA,CAAK,WAAW,CAAA;AAAA,IACtC,cAAA,EAAgB,QAAA,CAAS,IAAA,CAAK,cAAc,CAAA;AAAA,IAC5C,SAAA,EAAW,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA;AAAA,IAChC,UAAA,EAAY,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA;AAAA,IAClC,GAAA,EAAK;AAAA,GACP;AACF;;;ACrbA,SAAS,IAAA,CAAK,UAAA,EAAoB,MAAA,GAAS,EAAA,EAAY;AACrD,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,MAAM,IAAI,UAAU,6BAA6B,CAAA;AAAA,EACnD;AAIA,EAAA,OAAO,CAAA,YAAA,EAAe,kBAAA,CAAmB,UAAU,CAAC,GAAG,MAAM,CAAA,CAAA;AAC/D;AAOO,IAAM,aAAN,MAAiB;AAAA,EACb,QAAA;AAAA;AAAA,EAGT,YAAY,OAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,IAAI,UAAA,EAAuC;AAC/C,IAAA,OAAO,aAAA,CAAc,MAAM,IAAA,CAAK,QAAA,CAAS,OAAO,IAAA,CAAK,UAAU,CAAC,CAAC,CAAA;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,OAAO,UAAA,EAAqC;AAChD,IAAA,OAAO,WAAA,CAAY,MAAM,IAAA,CAAK,QAAA,CAAS,OAAO,IAAA,CAAK,UAAA,EAAY,SAAS,CAAC,CAAC,CAAA;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,SAAS,UAAA,EAAuC;AACpD,IAAA,OAAO,aAAA,CAAc,MAAM,IAAA,CAAK,QAAA,CAAS,OAAO,IAAA,CAAK,UAAA,EAAY,WAAW,CAAC,CAAC,CAAA;AAAA,EAChF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,QAAQ,UAAA,EAAmC;AAC/C,IAAA,MAAM,KAAK,QAAA,CAAS,MAAA,EAAQ,IAAA,CAAK,UAAA,EAAY,kBAAkB,CAAC,CAAA;AAAA,EAClE;AACF;;;AC9DO,IAAM,gBAAA,GAAmB;AAGzB,IAAM,kBAAA,GAAqB;AAG3B,IAAM,mBAAA,GAAsB;AAG5B,IAAM,cAAA,GAAiB;AAGvB,IAAM,YAAA,GAAe;AAGrB,IAAM,cAAA,GAAiB;AAM9B,IAAM,cAAA,mBAAsC,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC,CAAA;AAGlF,IAAM,mBAAA,GAAsB,cAAA;AAC5B,IAAM,qBAAA,GAAwB,gBAAA;AAG9B,IAAM,cAAA,GAAiB,GAAA;AAGvB,IAAM,kBAAA,GAAqB,GAAA;AA0GpB,IAAM,QAAN,MAAY;AAAA;AAAA,EAER,UAAA;AAAA,EAEA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA,WAAA;AAAA,EACA,MAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQT,WAAA,CAAY,OAAA,GAAwB,EAAC,EAAG;AACtC,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAA,IAAa,OAAA,CAAQ,cAAc,CAAA;AAE7D,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,6DAA6D,cAAc,CAAA,uHAAA;AAAA,OAG7E;AAAA,IACF;AAEA,IAAA,IAAI,SAAA,CAAU,UAAA,CAAW,KAAK,CAAA,EAAG;AAC/B,MAAA,MAAM,IAAI,SAAA;AAAA,QACR;AAAA,OAGF;AAAA,IACF;AAEA,IAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,mBAAA;AAEzC,IAAA,IAAI,aAAa,CAAA,IAAK,CAAC,MAAA,CAAO,SAAA,CAAU,UAAU,CAAA,EAAG;AACnD,MAAA,MAAM,IAAI,UAAU,4CAA4C,CAAA;AAAA,IAClE;AAEA,IAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,IAAW,kBAAA;AAEnC,IAAA,IAAI,WAAW,CAAA,EAAG;AAChB,MAAA,MAAM,IAAI,UAAU,oCAAoC,CAAA;AAAA,IAC1D;AAEA,IAAA,MAAM,aAAA,GAAuC,OAAA,CAAQ,KAAA,IAAS,UAAA,CAAW,KAAA;AAEzE,IAAA,IAAI,CAAC,aAAA,EAAe;AAClB,MAAA,MAAM,IAAI,SAAA;AAAA,QACR;AAAA,OAEF;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAClB,IAAA,IAAA,CAAK,YAAY,OAAA,CAAQ,OAAA,IAAW,OAAA,CAAQ,YAAY,KAAK,gBAAA,EAAkB,OAAA;AAAA,MAC7E,MAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAChB,IAAA,IAAA,CAAK,WAAA,GAAc,UAAA;AACnB,IAAA,IAAA,CAAK,MAAA,GAAS,aAAA;AAEd,IAAA,MAAM,OAAA,GAAqB,CAAC,MAAA,EAAQA,KAAAA,EAAM,SAAS,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQA,KAAAA,EAAM,IAAI,CAAA;AAElF,IAAA,IAAA,CAAK,UAAA,GAAa,IAAI,UAAA,CAAW,OAAO,CAAA;AAAA,EAC1C;AAAA;AAAA,EAGA,IAAI,OAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,UAAA,GAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,WAAA;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,OAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,MAAM,KAAA,CAAM,IAAA,EAAc,OAAA,EAA6C;AACrE,IAAA,OAAO,gBAAA,CAAiB,MAAM,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,SAAA,EAAW,IAAA,CAAK,SAAA,CAAU,IAAA,EAAM,OAAO,CAAC,CAAC,CAAA;AAAA,EAC9F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,SAAA,CAAU,MAAc,OAAA,EAAmC;AACjE,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,IAAI,UAAU,4BAA4B,CAAA;AAAA,IAClD;AAEA,IAAA,IAAI,CAAC,SAAS,QAAA,EAAU;AACtB,MAAA,MAAM,IAAI,SAAA;AAAA,QACR;AAAA,OAEF;AAAA,IACF;AAEA,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,aAAA,EAAc;AAEjD,IAAA,IAAI,OAAA,CAAQ,UAAA,CAAW,QAAQ,CAAA,EAAG;AAChC,MAAA,MAAM,IAAI,UAAU,iEAAiE,CAAA;AAAA,IACvF;AAEA,IAAA,IAAI,OAAA,CAAQ,SAAS,EAAA,EAAI;AACvB,MAAA,MAAM,IAAI,UAAU,gDAAgD,CAAA;AAAA,IACtE;AAEA,IAAA,MAAM,IAAA,GAAgC;AAAA,MACpC,EAAA,EAAI,OAAA;AAAA,MACJ,IAAA;AAAA,MACA,MAAM,EAAE,GAAI,OAAA,CAAQ,IAAA,IAAQ,EAAC,EAAG;AAAA,MAChC,QAAA,EAAU,EAAE,EAAA,EAAI,OAAA,CAAQ,QAAA,EAAU,IAAA,EAAM,EAAE,GAAI,OAAA,CAAQ,YAAA,IAAgB,EAAC,EAAG,EAAE;AAAA,MAC5E,MAAA,EAAQ,QAAQ,MAAA,IAAU;AAAA,KAC5B;AAEA,IAAA,IAAI,OAAA,CAAQ,cAAc,MAAA,EAAW;AACnC,MAAA,IAAA,CAAK,SAAA,GAAY,QAAA,CAAS,OAAA,CAAQ,SAAS,CAAA;AAAA,IAC7C;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,OAAA,CAAQ,MAAA,EAAgBA,KAAAA,EAAc,IAAA,EAAkC;AACpF,IAAA,MAAM,GAAA,GAAM,GAAG,IAAA,CAAK,QAAQ,IAAIA,KAAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,CAAA;AACxD,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,WAAA,CAAY,MAAA,EAAQ,IAAI,CAAA;AAE1C,IAAA,KAAA,IAAS,OAAA,GAAU,CAAA,IAAK,OAAA,IAAW,CAAA,EAAG;AACpC,MAAA,IAAI,QAAA;AACJ,MAAA,IAAI,IAAA;AAEJ,MAAA,IAAI;AACF,QAAA,QAAA,GAAW,MAAM,IAAA,CAAK,MAAA,CAAO,GAAA,EAAK,EAAE,GAAG,IAAA,EAAM,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,IAAA,CAAK,QAAQ,GAAG,CAAA;AACzF,QAAA,IAAA,GAAO,MAAM,SAAS,IAAA,EAAK;AAAA,MAC7B,SAAS,KAAA,EAAO;AACd,QAAA,IAAI,CAAC,IAAA,CAAK,WAAA,CAAY,OAAA,EAAS,IAAI,CAAA,EAAG;AACpC,UAAA,MAAM,SAAA,CAAU,KAAK,CAAA,GACjB,IAAI,iBAAA;AAAA,YACF,CAAA,eAAA,EAAkB,GAAG,CAAA,uBAAA,EAA0B,IAAA,CAAK,QAAQ,CAAA,GAAA,CAAA;AAAA,YAC5D,EAAE,KAAA;AAAM,WACV,GACA,IAAI,oBAAA,CAAqB,CAAA,yBAAA,EAA4B,GAAG,CAAA,EAAA,EAAK,QAAA,CAAS,KAAK,CAAC,CAAA,CAAA,EAAI;AAAA,YAC9E;AAAA,WACD,CAAA;AAAA,QACP;AAEA,QAAA,MAAM,KAAA,CAAM,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,IAAI,CAAC,CAAA;AAEvC,QAAA;AAAA,MACF;AAEA,MAAA,IAAI;AACF,QAAA,OAAO,eAAA,CAAgB,UAAU,IAAI,CAAA;AAAA,MACvC,SAAS,KAAA,EAAO;AACd,QAAA,IAAI,iBAAiB,aAAA,IAAiB,IAAA,CAAK,YAAY,OAAA,EAAS,KAAA,CAAM,UAAU,CAAA,EAAG;AACjF,UAAA,MAAM,KAAA;AAAA,YACJ,KAAK,OAAA,CAAQ,OAAA,EAAS,iBAAiB,cAAA,GAAiB,KAAA,CAAM,aAAa,IAAI;AAAA,WACjF;AAEA,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,WAAA,CAAY,QAAgB,IAAA,EAA4B;AAC9D,IAAA,MAAM,OAAA,GAAkC;AAAA,MACtC,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,UAAU,CAAA,CAAA;AAAA,MACxC,MAAA,EAAQ,kBAAA;AAAA,MACR,cAAc,SAAA;AAAU,KAC1B;AAEA,IAAA,IAAI,SAAS,MAAA,EAAW;AACtB,MAAA,OAAO,EAAE,QAAQ,OAAA,EAAQ;AAAA,IAC3B;AAEA,IAAA,OAAA,CAAQ,cAAc,CAAA,GAAI,kBAAA;AAE1B,IAAA,OAAO,EAAE,MAAA,EAAQ,OAAA,EAAS,MAAM,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,EAAE;AAAA,EACvD;AAAA,EAEQ,WAAA,CAAY,SAAiB,UAAA,EAAoC;AACvE,IAAA,IAAI,OAAA,IAAW,KAAK,WAAA,EAAa;AAC/B,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,OAAO,UAAA,KAAe,IAAA,IAAQ,cAAA,CAAe,GAAA,CAAI,UAAU,CAAA;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUU,OAAA,CAAQ,SAAiB,iBAAA,EAA0C;AAC3E,IAAA,IAAI,iBAAA,KAAsB,IAAA,IAAQ,iBAAA,IAAqB,CAAA,EAAG;AACxD,MAAA,OAAO,IAAA,CAAK,GAAA,CAAI,iBAAA,GAAoB,GAAA,EAAM,kBAAkB,CAAA;AAAA,IAC9D;AAEA,IAAA,OAAO,IAAA,CAAK,QAAO,GAAI,IAAA,CAAK,IAAI,GAAA,GAAM,CAAA,IAAK,SAAS,cAAc,CAAA;AAAA,EACpE;AACF;AAGA,SAAS,eAAA,CAAgB,UAAoB,IAAA,EAAuB;AAClE,EAAA,MAAM,OAAA,GAAU,UAAU,IAAI,CAAA;AAE9B,EAAA,IAAI,QAAA,CAAS,UAAU,GAAA,EAAK;AAC1B,IAAA,MAAM,QAAA,CAAS,UAAU,OAAO,CAAA;AAAA,EAClC;AAIA,EAAA,IAAI,QAAA,CAAS,OAAO,CAAA,EAAG;AACrB,IAAA,IAAI,OAAA,CAAQ,WAAW,mBAAA,EAAqB;AAC1C,MAAA,MAAM,IAAI,eAAe,oDAAA,EAAsD;AAAA,QAC7E,IAAA,EAAM,OAAA;AAAA,QACN,SAAA,EAAW,UAAU,QAAQ,CAAA;AAAA,QAC7B,UAAA,EAAY,WAAW,QAAQ;AAAA,OAChC,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,OAAA,CAAQ,WAAW,qBAAA,EAAuB;AAC5C,MAAA,MAAM,IAAI,mBAAmB,8CAAA,EAAgD;AAAA,QAC3E,UAAA,EAAY,GAAA;AAAA,QACZ,IAAA,EAAM,OAAA;AAAA,QACN,SAAA,EAAW,UAAU,QAAQ;AAAA,OAC9B,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,OAAO,OAAA;AACT;AAEA,SAAS,QAAA,CAAS,UAAoB,OAAA,EAAiC;AACrE,EAAA,MAAM,OAAA,GAAU,WAAA,CAAY,OAAO,CAAA,IAAK,SAAS,UAAA,IAAc,gBAAA;AAC/D,EAAA,MAAM,EAAA,GAAK,UAAU,QAAQ,CAAA;AAE7B,EAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,IAAA,OAAO,IAAI,eAAe,OAAA,EAAS;AAAA,MACjC,IAAA,EAAM,OAAA;AAAA,MACN,SAAA,EAAW,EAAA;AAAA,MACX,UAAA,EAAY,WAAW,QAAQ;AAAA,KAChC,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,UAAA,GAAa,cAAA,CAAe,QAAA,CAAS,MAAM,CAAA;AAEjD,EAAA,OAAO,IAAI,UAAA,CAAW,OAAA,EAAS,EAAE,UAAA,EAAY,QAAA,CAAS,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,SAAA,EAAW,EAAA,EAAI,CAAA;AAC9F;AAEA,SAAS,YAAY,OAAA,EAAiC;AACpD,EAAA,IAAI,CAAC,QAAA,CAAS,OAAO,CAAA,EAAG;AACtB,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,KAAA,MAAW,GAAA,IAAO,CAAC,SAAA,EAAW,OAAA,EAAS,QAAQ,CAAA,EAAY;AACzD,IAAA,MAAM,KAAA,GAAQ,QAAQ,GAAG,CAAA;AAEzB,IAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,EAAO;AACtC,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,UAAU,QAAA,EAAmC;AACpD,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,cAAc,CAAA;AAC5C;AAEA,SAAS,WAAW,QAAA,EAAmC;AACrD,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAA;AAE9C,EAAA,IAAI,QAAQ,IAAA,EAAM;AAChB,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAA,GAAU,OAAO,GAAG,CAAA;AAI1B,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,GAAI,OAAA,GAAU,IAAA;AAC9C;AAEA,SAAS,UAAU,IAAA,EAAuB;AACxC,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EACxB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEA,SAAS,SAAS,KAAA,EAAkD;AAClE,EAAA,OAAO,OAAO,UAAU,QAAA,IAAY,KAAA,KAAU,QAAQ,CAAC,KAAA,CAAM,QAAQ,KAAK,CAAA;AAC5E;AAQA,SAAS,UAAU,KAAA,EAAyB;AAC1C,EAAA,MAAM,KAAA,GAAQ,CAAC,MAAA,CAAO,KAAK,GAAG,MAAA,CAAQ,KAAA,EAAsC,KAAK,CAAC,CAAA;AAElF,EAAA,OAAO,MAAM,QAAA,CAAS,cAAc,CAAA,IAAK,KAAA,CAAM,SAAS,YAAY,CAAA;AACtE;AAEA,SAAS,OAAO,KAAA,EAA+B;AAC7C,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,UAAU,IAAA,IAAQ,EAAE,UAAU,KAAA,CAAA,EAAQ;AACrE,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,EAAE,MAAK,GAAI,KAAA;AAEjB,EAAA,OAAO,OAAO,IAAA,KAAS,QAAA,GAAW,IAAA,GAAO,IAAA;AAC3C;AAEA,SAAS,SAAS,KAAA,EAAwB;AACxC,EAAA,OAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK,CAAA;AAC9D;AAEA,SAAS,MAAM,EAAA,EAA2B;AACxC,EAAA,OAAO,EAAA,GAAK,CAAA,GAAI,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA,GAAI,OAAA,CAAQ,OAAA,EAAQ;AACtF;AAEA,SAAS,QAAQ,IAAA,EAAkC;AACjD,EAAA,OAAO,OAAO,OAAA,KAAY,WAAA,GAAc,MAAA,GAAY,OAAA,CAAQ,IAAI,IAAI,CAAA;AACtE;AAGA,SAAS,aAAA,GAAwB;AAC/B,EAAA,OAAO,WAAW,MAAA,CAAO,UAAA,EAAW,CAAE,OAAA,CAAQ,MAAM,EAAE,CAAA;AACxD;AAQA,SAAS,SAAS,KAAA,EAA8B;AAC9C,EAAA,MAAM,SAAS,KAAA,YAAiB,IAAA,GAAO,KAAA,GAAQ,IAAI,KAAK,KAAK,CAAA;AAE7D,EAAA,IAAI,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,OAAA,EAAS,CAAA,EAAG;AAClC,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,cAAA,EAAiB,KAAK,SAAA,CAAU,KAAK,CAAC,CAAA,qBAAA,CAAuB,CAAA;AAAA,EACnF;AAIA,EAAA,OAAO,MAAA,CAAO,WAAA,EAAY,CAAE,OAAA,CAAQ,WAAW,GAAG,CAAA;AACpD;AAEA,SAAS,SAAA,GAAoB;AAC3B,EAAA,MAAM,OAAA,GACJ,OAAO,OAAA,KAAY,WAAA,IAAe,OAAA,CAAQ,QAAA,EAAU,IAAA,GAChD,CAAA,KAAA,EAAQ,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA,CAAA,GAC7B,SAAA;AAEN,EAAA,OAAO,CAAA,WAAA,EAAc,OAAO,CAAA,EAAA,EAAK,OAAO,CAAA,CAAA,CAAA;AAC1C","file":"index.js","sourcesContent":["/**\n * The package version, used in the `User-Agent` header.\n *\n * Kept here rather than read from `package.json` at runtime, because the published package is\n * both ESM and CommonJS and neither can reach its own manifest portably. A test asserts this\n * stays in step with `package.json`, so the two cannot drift.\n */\nexport const VERSION = '0.1.0';\n","/**\n * Typed views over the Dregs API's responses.\n *\n * Every model keeps the response it was built from in `raw`, so a field Dregs adds after this\n * release is still reachable without waiting for an SDK upgrade. Parsing is deliberately lenient:\n * a missing field becomes `null` rather than an error, because an SDK that refuses to parse a\n * response it half-understands is worse than one that hands back what it got.\n *\n * @module\n */\n\n/**\n * The four categories Dregs scores an identity in.\n *\n * A plain string union rather than an enum, so `scores.get('HUMANITY')` type-checks and a\n * category from a future Dregs release still survives parsing (as a `null` category on a\n * {@link Score} whose `raw` still names it).\n */\nexport type Category = 'HUMANITY' | 'AUTHENTICITY' | 'UNIQUENESS' | 'BEHAVIOR';\n\n/** The four categories, in the order the dashboard shows them. */\nexport const CATEGORIES: readonly Category[] = [\n 'HUMANITY',\n 'AUTHENTICITY',\n 'UNIQUENESS',\n 'BEHAVIOR',\n];\n\n/** An unparsed JSON object, as received. */\nexport type RawPayload = Readonly<Record<string, unknown>>;\n\n/** The outcome of a {@link Dregs.track} call. */\nexport interface TrackResult {\n /** The status Dregs reported, normally `\"success\"`. */\n readonly status: string | null;\n\n /**\n * The event's identifier, either the one you supplied or one the SDK generated. It is `null`\n * when the event was not recorded.\n */\n readonly id: string | null;\n\n /**\n * The device fingerprint Dregs resolved, for events that carried a device signature.\n * Server-side events do not, so this is normally `null`.\n */\n readonly fingerprint: string | null;\n\n /**\n * Whether Dregs recorded the event.\n *\n * This is `false` in the uncommon case where Dregs accepts the request without recording an\n * event. A server-side integration holding a valid secret key should not normally see it, so it\n * is worth a log line if you do. Ingestion failures that are yours to act on (a bad request, an\n * unknown key, an exhausted quota, a rate limit) throw instead of landing here.\n */\n readonly accepted: boolean;\n\n /** The response body as received. */\n readonly raw: RawPayload;\n}\n\n/** A label Dregs applied to an identity, from an analyzer or a badge rule. */\nexport interface Badge {\n /** The badge's slug, such as `\"behavior.account-takeover-signal\"`. */\n readonly slug: string | null;\n\n /** The human-readable name, such as `\"Account Takeover Suspected\"`. */\n readonly name: string | null;\n\n /** Where the badge came from: an analyzer observation or a badge rule. */\n readonly type: string | null;\n\n /** A sentence describing why the badge was applied. */\n readonly explanation: string | null;\n\n /** The counts and details behind the badge. */\n readonly metadata: RawPayload;\n\n /** The response fragment this badge was built from. */\n readonly raw: RawPayload;\n}\n\n/** One analyzer's finding, and the reasoning behind a slice of a score. */\nexport interface Observation {\n /** The category the observation contributes to, or `null` for one this release predates. */\n readonly category: Category | null;\n\n /** The analyzer's identifier, such as `\"humanity.user-agent\"`. */\n readonly id: string | null;\n\n /** A human-readable name for the analyzer. */\n readonly label: string | null;\n\n /** A sentence describing what the analyzer found. This is the text to show or log. */\n readonly explanation: string | null;\n\n /** 0.0 for entirely suspicious, 1.0 for entirely legitimate. */\n readonly value: number | null;\n\n /** How sure the analyzer is, from 0.0 to 1.0. */\n readonly confidence: number | null;\n\n /** How heavily this observation counts toward the category score. */\n readonly weight: number | null;\n\n /** The counts and details behind the finding. */\n readonly metadata: RawPayload;\n\n /** The response fragment this observation was built from. */\n readonly raw: RawPayload;\n}\n\n/** One category's score. */\nexport interface Score {\n /** The category scored, or `null` for one this release predates. */\n readonly category: Category | null;\n\n /** An integer from 0 (worst) to 100 (best). */\n readonly value: number | null;\n\n /**\n * The observations behind the score. Empty on the result of `identities.scores()`, which\n * reports the scores alone; the observations come from `identities.analysis()`.\n */\n readonly observations: readonly Observation[];\n\n /** The response fragment this score was built from. */\n readonly raw: RawPayload;\n}\n\n/**\n * An identity's category scores.\n *\n * An array of {@link Score}, so it indexes, iterates, spreads, and maps like any other, and it\n * also offers the four categories by name:\n *\n * ```ts\n * const scores = await client.identities.scores('user_12345');\n *\n * if (scores.authenticity !== null && scores.authenticity < 40) {\n * await holdForReview('user_12345');\n * }\n * ```\n *\n * A category Dregs has not scored yet is absent from the array, and its named accessor reads\n * `null`. A brand-new identity comes back empty.\n */\nexport class Scores extends Array<Score> {\n /**\n * `map`, `filter`, and `slice` return plain arrays rather than trying to rebuild a `Scores`\n * through a constructor whose shape they know nothing about.\n */\n static override get [Symbol.species](): ArrayConstructor {\n return Array;\n }\n\n constructor(items: readonly Score[] = []) {\n super();\n\n for (const item of items) {\n this.push(item);\n }\n }\n\n /** Returns the score for `category`, or `null` when it has not been scored. */\n get(category: Category): Score | null {\n return this.find((score) => score.category === category) ?? null;\n }\n\n private value(category: Category): number | null {\n return this.get(category)?.value ?? null;\n }\n\n /** How likely it is that a person, rather than a script, is behind the account. */\n get humanity(): number | null {\n return this.value('HUMANITY');\n }\n\n /** How genuine the details on the account look. */\n get authenticity(): number | null {\n return this.value('AUTHENTICITY');\n }\n\n /** How distinct the account is from others in the same tenant. */\n get uniqueness(): number | null {\n return this.value('UNIQUENESS');\n }\n\n /** How ordinary the account's activity looks. */\n get behavior(): number | null {\n return this.value('BEHAVIOR');\n }\n}\n\n/**\n * A user Dregs is tracking, and their current scores.\n *\n * `id` is your own identifier for the user, the one you pass to {@link Dregs.track} and to\n * `dregs.identify()` in the browser tracker, not an internal Dregs id.\n */\nexport interface Identity {\n /** Your own id for the user. */\n readonly id: string | null;\n\n /** The name Dregs resolved from the identity attributes you have sent. */\n readonly displayName: string | null;\n\n /** The email address Dregs resolved from the identity attributes you have sent. */\n readonly displayEmail: string | null;\n\n /** The username Dregs resolved from the identity attributes you have sent. */\n readonly displayUsername: string | null;\n\n /** The current humanity score, 0 to 100, or `null` when the category is unscored. */\n readonly humanityScore: number | null;\n\n /** The current authenticity score, 0 to 100, or `null` when the category is unscored. */\n readonly authenticityScore: number | null;\n\n /** The current uniqueness score, 0 to 100, or `null` when the category is unscored. */\n readonly uniquenessScore: number | null;\n\n /** The current behavior score, 0 to 100, or `null` when the category is unscored. */\n readonly behaviorScore: number | null;\n\n /** When Dregs first saw this identity. */\n readonly createdAt: Date | null;\n\n /** When the identity record last changed. */\n readonly updatedAt: Date | null;\n\n /** When the most recent event for this identity arrived. */\n readonly lastTrackedAt: Date | null;\n\n /** When the most recent analysis cycle finished. */\n readonly lastScoredAt: Date | null;\n\n /**\n * Whether the identity is excluded from fraud analysis, which is how an operator marks their\n * own admin or load-test accounts.\n */\n readonly disregarded: boolean;\n\n /** The badges currently applied to the identity. */\n readonly badges: readonly Badge[];\n\n /** Every identity attribute you have sent, merged. */\n readonly data: RawPayload;\n\n /**\n * The four category scores, as a {@link Scores} for parity with `identities.scores()`.\n *\n * Built from the score fields on this response, so it costs no extra request.\n */\n readonly scores: Scores;\n\n /** The response body this identity was built from. */\n readonly raw: RawPayload;\n}\n\n/** One analysis cycle: the scores an identity was given, and why. */\nexport interface Analysis {\n /** The cycle's identifier. */\n readonly id: number | null;\n\n /** The identity that was analyzed. */\n readonly identityId: string | null;\n\n /** The category scores, each carrying its observations. */\n readonly scores: Scores;\n\n /** Every observation from the cycle, flattened across all categories. */\n readonly observations: readonly Observation[];\n\n /** How many events the cycle considered. */\n readonly eventCount: number | null;\n\n /** How many devices the cycle considered. */\n readonly deviceCount: number | null;\n\n /** How long the cycle took, in milliseconds. */\n readonly durationMillis: number | null;\n\n /** When the cycle started. */\n readonly startedAt: Date | null;\n\n /** When the cycle finished. */\n readonly finishedAt: Date | null;\n\n /** The response body this analysis was built from. */\n readonly raw: RawPayload;\n}\n\nfunction asPayload(value: unknown): RawPayload {\n return isPayload(value) ? value : {};\n}\n\nfunction isPayload(value: unknown): value is RawPayload {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === 'string' ? value : null;\n}\n\nfunction asNumber(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\nfunction asDate(value: unknown): Date | null {\n if (typeof value !== 'string' || !value) {\n return null;\n }\n\n const parsed = new Date(value);\n\n return Number.isNaN(parsed.getTime()) ? null : parsed;\n}\n\nfunction asCategory(value: unknown): Category | null {\n return typeof value === 'string' && (CATEGORIES as readonly string[]).includes(value)\n ? (value as Category)\n : null;\n}\n\nfunction asArray(value: unknown): RawPayload[] {\n return Array.isArray(value) ? value.filter(isPayload) : [];\n}\n\n/** Builds a {@link TrackResult} from a `POST /api/events` body. @internal */\nexport function parseTrackResult(payload: unknown): TrackResult {\n const body = asPayload(payload);\n const id = asString(body.id);\n\n return {\n status: asString(body.status),\n id,\n fingerprint: asString(body.fingerprint),\n accepted: id !== null,\n raw: body,\n };\n}\n\n/** Builds a {@link Badge}. @internal */\nexport function parseBadge(payload: unknown): Badge {\n const body = asPayload(payload);\n\n return {\n slug: asString(body.slug),\n name: asString(body.name),\n type: asString(body.type),\n explanation: asString(body.explanation),\n metadata: asPayload(body.metadata),\n raw: body,\n };\n}\n\n/** Builds an {@link Observation}. @internal */\nexport function parseObservation(payload: unknown): Observation {\n const body = asPayload(payload);\n\n return {\n category: asCategory(body.category),\n id: asString(body.id),\n label: asString(body.label),\n explanation: asString(body.explanation),\n value: asNumber(body.value),\n confidence: asNumber(body.confidence),\n weight: asNumber(body.weight),\n metadata: asPayload(body.metadata),\n raw: body,\n };\n}\n\n/** Builds a {@link Score}, with any observations it carries. @internal */\nexport function parseScore(payload: unknown): Score {\n const body = asPayload(payload);\n\n return {\n category: asCategory(body.category),\n value: asNumber(body.value),\n observations: asArray(body.observations).map(parseObservation),\n raw: body,\n };\n}\n\n/** Builds a {@link Scores} from the `GET /scores` array. @internal */\nexport function parseScores(payload: unknown): Scores {\n return new Scores(asArray(payload).map(parseScore));\n}\n\n/** Builds an {@link Identity}. @internal */\nexport function parseIdentity(payload: unknown): Identity {\n const body = asPayload(payload);\n\n const humanityScore = asNumber(body.humanityScore);\n const authenticityScore = asNumber(body.authenticityScore);\n const uniquenessScore = asNumber(body.uniquenessScore);\n const behaviorScore = asNumber(body.behaviorScore);\n\n const pairs: readonly (readonly [Category, number | null])[] = [\n ['HUMANITY', humanityScore],\n ['AUTHENTICITY', authenticityScore],\n ['UNIQUENESS', uniquenessScore],\n ['BEHAVIOR', behaviorScore],\n ];\n\n const scores = new Scores(\n pairs\n .filter(([, value]) => value !== null)\n .map(([category, value]) => ({ category, value, observations: [], raw: {} })),\n );\n\n return {\n id: asString(body.id),\n displayName: asString(body.displayName),\n displayEmail: asString(body.displayEmail),\n displayUsername: asString(body.displayUsername),\n humanityScore,\n authenticityScore,\n uniquenessScore,\n behaviorScore,\n createdAt: asDate(body.createdAt),\n updatedAt: asDate(body.updatedAt),\n lastTrackedAt: asDate(body.lastTrackedAt),\n lastScoredAt: asDate(body.lastScoredAt),\n disregarded: body.disregarded === true,\n badges: asArray(body.badges).map(parseBadge),\n data: asPayload(body.data),\n scores,\n raw: body,\n };\n}\n\n/** Builds an {@link Analysis}. @internal */\nexport function parseAnalysis(payload: unknown): Analysis {\n const body = asPayload(payload);\n const scores = parseScores(body.scores);\n\n return {\n id: asNumber(body.id),\n identityId: asString(body.identityId),\n scores,\n observations: scores.flatMap((score) => [...score.observations]),\n eventCount: asNumber(body.eventCount),\n deviceCount: asNumber(body.deviceCount),\n durationMillis: asNumber(body.durationMillis),\n startedAt: asDate(body.startedAt),\n finishedAt: asDate(body.finishedAt),\n raw: body,\n };\n}\n","/**\n * The `client.identities` namespace.\n *\n * These are thin: they name the endpoint, then hand the response to a parser. The transport,\n * retries, and error mapping all live on the client.\n *\n * @module\n */\n\nimport type { Analysis, Identity, Scores } from './models.js';\nimport { parseAnalysis, parseIdentity, parseScores } from './models.js';\n\n/** How a resource reaches the client's transport. @internal */\nexport type RequestFn = (method: string, path: string, body?: unknown) => Promise<unknown>;\n\nfunction path(identityId: string, suffix = ''): string {\n if (!identityId) {\n throw new TypeError('An identity id is required.');\n }\n\n // Identity ids are the caller's own user ids and routinely contain characters that need\n // escaping, an email address being the common one.\n return `/identities/${encodeURIComponent(identityId)}${suffix}`;\n}\n\n/**\n * Read identities and their scores.\n *\n * Reached as `client.identities`; there is no reason to construct one yourself.\n */\nexport class Identities {\n readonly #request: RequestFn;\n\n /** @internal */\n constructor(request: RequestFn) {\n this.#request = request;\n }\n\n /**\n * Returns the identity, with its current scores, badges, and attributes.\n *\n * @param identityId Your own id for the user, the one you pass to `track()`.\n * @throws {NotFoundError} Dregs has never seen this identity.\n */\n async get(identityId: string): Promise<Identity> {\n return parseIdentity(await this.#request('GET', path(identityId)));\n }\n\n /**\n * Returns the current category scores.\n *\n * This is the cheap read and the one most integrations want. It reports the scores Dregs has\n * already computed without triggering any work. For the observations behind them, use\n * {@link Identities.analysis}.\n *\n * A category that has not been scored yet is absent, so a brand-new identity comes back empty.\n *\n * @throws {NotFoundError} Dregs has never seen this identity.\n */\n async scores(identityId: string): Promise<Scores> {\n return parseScores(await this.#request('GET', path(identityId, '/scores')));\n }\n\n /**\n * Returns the most recent analysis cycle, with the observations behind each score.\n *\n * Use this when you need to show or log *why* an identity scored the way it did.\n *\n * @throws {NotFoundError} The identity is unknown, or it has not been analyzed yet.\n */\n async analysis(identityId: string): Promise<Analysis> {\n return parseAnalysis(await this.#request('GET', path(identityId, '/analysis')));\n }\n\n /**\n * Queues a re-analysis of the identity.\n *\n * Scoring is asynchronous: this resolves as soon as the job is queued, not when it has run.\n * Poll {@link Identities.scores} or watch for a webhook rather than expecting fresh scores on\n * the next line.\n *\n * @throws {NotFoundError} Dregs has never seen this identity.\n */\n async analyze(identityId: string): Promise<void> {\n await this.#request('POST', path(identityId, '/actions/analyze'));\n }\n}\n","/**\n * The Dregs client.\n *\n * There is one client and every method returns a promise; JavaScript has no meaningful\n * sync/async split, so there is no async twin to pick between.\n *\n * @module\n */\n\nimport {\n DregsAPIError,\n DregsConnectionError,\n DregsTimeoutError,\n QuotaExceededError,\n RateLimitError,\n errorForStatus,\n} from './errors.js';\nimport type { RawPayload, TrackResult } from './models.js';\nimport { parseTrackResult } from './models.js';\nimport type { RequestFn } from './resources.js';\nimport { Identities } from './resources.js';\nimport { VERSION } from './version.js';\n\n/** The API root used when neither an option nor `DREGS_BASE_URL` names one. */\nexport const DEFAULT_BASE_URL = 'https://dregs.com/api';\n\n/** How long a request may take before it is abandoned, in milliseconds. */\nexport const DEFAULT_TIMEOUT_MS = 10_000;\n\n/** How many times a failed request is retried before the error is thrown. */\nexport const DEFAULT_MAX_RETRIES = 2;\n\n/** The environment variable the secret key is read from. */\nexport const SECRET_KEY_ENV = 'DREGS_SECRET_KEY';\n\n/** The environment variable the base URL is read from. */\nexport const BASE_URL_ENV = 'DREGS_BASE_URL';\n\n/** The `source` sent on events when the caller does not name one. */\nexport const DEFAULT_SOURCE = 'node-sdk';\n\n/**\n * Statuses worth another attempt. 429 and 5xx are transient by definition; 408 shows up in front\n * of some proxies.\n */\nconst RETRY_STATUSES: ReadonlySet<number> = new Set([408, 429, 500, 502, 503, 504]);\n\n/** Body-level statuses older API builds used on `POST /api/events`. */\nconst STATUS_RATE_LIMITED = 'rate_limited';\nconst STATUS_QUOTA_EXCEEDED = 'quota_exceeded';\n\n/** The longest a single backoff will ever be, in milliseconds. */\nconst MAX_BACKOFF_MS = 8_000;\n\n/** The longest a server-sent `Retry-After` will be honoured for, in milliseconds. */\nconst MAX_RETRY_AFTER_MS = 60_000;\n\n/** The `fetch` this client calls. Any implementation with the standard signature will do. */\nexport type FetchLike = (input: string, init: RequestInit) => Promise<Response>;\n\n/** Everything you can configure on a {@link Dregs} client. */\nexport interface DregsOptions {\n /**\n * Your credential's secret key, the one starting `sk_`. Found under **Settings → Credentials**\n * in the dashboard. Defaults to `$DREGS_SECRET_KEY`.\n */\n secretKey?: string;\n\n /**\n * The API root every request is built against. Defaults to `$DREGS_BASE_URL`, then\n * `https://dregs.com/api`. A trailing slash is harmless.\n */\n baseUrl?: string;\n\n /**\n * Milliseconds before a request is abandoned and retried. Defaults to 10 000.\n *\n * This covers the whole request, not just the connect, so raise it if you are behind a slow\n * egress proxy rather than lowering `maxRetries` to compensate.\n */\n timeout?: number;\n\n /**\n * How many times to retry a failed request. Defaults to 2; pass 0 to handle it yourself.\n *\n * Retries cover connection failures, timeouts, 408, 429, and 5xx, with exponential backoff and\n * full jitter. `Retry-After` wins when the server sends one. A retried event keeps its id, so\n * a retry can never double-count.\n */\n maxRetries?: number;\n\n /**\n * A `fetch` to call instead of the runtime's own, for callers who need a proxy agent, custom\n * TLS, or their own instrumentation. Anything with the standard signature works, including a\n * stub in a test.\n */\n fetch?: FetchLike;\n}\n\n/** The arguments to {@link Dregs.track} beyond the event type. */\nexport interface TrackOptions {\n /**\n * Your own id for the user. This is the same id you pass to `dregs.identify()` in the browser\n * tracker, and the one you look scores up by.\n *\n * It is required: a server-side event carries no device signature, so the identity is the only\n * thing tying the event to a user.\n */\n identity: string;\n\n /** Attributes of the event itself, such as the plan bought or the referrer that sent them. */\n data?: Readonly<Record<string, unknown>>;\n\n /**\n * Attributes of the *user*, such as email, name, or username. Dregs merges these into the\n * identity, and the analyzers lean on them heavily, so send them whenever you have them.\n *\n * Flat keys work best. Name them as your application already does and map them to Dregs's\n * canonical fields under **Settings → Mappings**.\n */\n identityData?: Readonly<Record<string, unknown>>;\n\n /**\n * Your own id for the event, which makes ingestion idempotent: reposting the same id returns\n * the original event instead of recording a second one.\n *\n * Pass the id your application already has — the row id of the record that triggered the\n * event, say. When you omit it the SDK generates one, which is what makes its own retries\n * safe. At most 64 characters, and it cannot start with `dregs-`.\n */\n eventId?: string;\n\n /**\n * When the event happened, if not now. A `Date`, or an ISO-8601 string. Sent as UTC.\n */\n timestamp?: Date | string;\n\n /** A label for where the event came from. Defaults to `\"node-sdk\"`. */\n source?: string;\n}\n\n/**\n * A Dregs client.\n *\n * The secret key comes from the `DREGS_SECRET_KEY` environment variable unless you pass one.\n * Find it under **Settings → Credentials** in the dashboard; it is the key starting `sk_`, not\n * the `pk_` public key the browser tracker uses.\n *\n * ```ts\n * import { Dregs } from '@dregs/sdk';\n *\n * const client = new Dregs();\n *\n * await client.track('user.signup', { identity: 'user_12345', data: { plan: 'pro' } });\n *\n * const scores = await client.identities.scores('user_12345');\n * ```\n *\n * Build one at startup and keep it. There is nothing to close: the client holds no state beyond\n * its configuration, and connection pooling belongs to the runtime's `fetch`.\n */\nexport class Dregs {\n /** Read identities, their scores, and their analysis. */\n readonly identities: Identities;\n\n readonly #secretKey: string;\n readonly #baseUrl: string;\n readonly #timeout: number;\n readonly #maxRetries: number;\n readonly #fetch: FetchLike;\n\n /**\n * @param options Configuration. Every field has a default, so `new Dregs()` reads the\n * environment and is usually enough.\n * @throws {TypeError} No secret key was found, the key given is a `pk_` public key, or\n * `maxRetries` is negative.\n */\n constructor(options: DregsOptions = {}) {\n const secretKey = options.secretKey ?? readEnv(SECRET_KEY_ENV);\n\n if (!secretKey) {\n throw new TypeError(\n `No Dregs secret key. Pass { secretKey: '...' } or set the ${SECRET_KEY_ENV} ` +\n \"environment variable. You will find your credential's secret key under \" +\n 'Settings -> Credentials in the Dregs dashboard.',\n );\n }\n\n if (secretKey.startsWith('pk_')) {\n throw new TypeError(\n 'That is a public key. The public key is for the browser tracker and cannot read ' +\n 'identities or scores; this SDK needs the secret key from the same credential, ' +\n \"which starts with 'sk_'.\",\n );\n }\n\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n\n if (maxRetries < 0 || !Number.isInteger(maxRetries)) {\n throw new TypeError('maxRetries must be a non-negative integer.');\n }\n\n const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n\n if (timeout <= 0) {\n throw new TypeError('timeout must be greater than zero.');\n }\n\n const resolvedFetch: FetchLike | undefined = options.fetch ?? globalThis.fetch;\n\n if (!resolvedFetch) {\n throw new TypeError(\n 'No global fetch. This SDK needs Node 20 or newer, or a fetch implementation passed ' +\n 'as the fetch option.',\n );\n }\n\n this.#secretKey = secretKey;\n this.#baseUrl = (options.baseUrl ?? readEnv(BASE_URL_ENV) ?? DEFAULT_BASE_URL).replace(\n /\\/+$/,\n '',\n );\n this.#timeout = timeout;\n this.#maxRetries = maxRetries;\n this.#fetch = resolvedFetch;\n\n const request: RequestFn = (method, path, body) => this.request(method, path, body);\n\n this.identities = new Identities(request);\n }\n\n /** The API root every request is built against, with any trailing slash removed. */\n get baseUrl(): string {\n return this.#baseUrl;\n }\n\n /** How many times a failed request is retried before the error is thrown. */\n get maxRetries(): number {\n return this.#maxRetries;\n }\n\n /** Milliseconds before a request is abandoned. */\n get timeout(): number {\n return this.#timeout;\n }\n\n /**\n * Records a backend event against an identity.\n *\n * ```ts\n * await client.track('user.signup', {\n * identity: 'user_12345',\n * data: { plan: 'pro', referrer: 'partner-x' },\n * identityData: { email: 'ada@example.com', name: 'Ada Lovelace' },\n * });\n * ```\n *\n * @param type Your name for the event, such as `\"user.signup\"`. Map it to one of Dregs's\n * canonical types under **Settings → Mappings** so the analyzers know what it means.\n * @param options The identity the event belongs to, and anything else worth sending.\n * @returns The outcome. Check `.accepted` to confirm Dregs recorded the event.\n * @throws {QuotaExceededError} The account is over its monthly event limit.\n * @throws {RateLimitError} The credential is ingesting too fast.\n * @throws {AuthenticationError} The secret key was not recognized.\n * @throws {BadRequestError} The event was malformed.\n * @throws {TypeError} The event type, identity, or event id was unusable. These are thrown\n * before anything is sent.\n */\n async track(type: string, options: TrackOptions): Promise<TrackResult> {\n return parseTrackResult(await this.request('POST', '/events', this.trackBody(type, options)));\n }\n\n /**\n * Builds the `POST /api/events` body.\n *\n * An event id is always sent. When the caller has an id of their own it is used verbatim, so\n * reposting the same event is a no-op on the Dregs side; otherwise one is generated, which is\n * what makes this client's own retries safe to perform.\n */\n private trackBody(type: string, options: TrackOptions): RawPayload {\n if (!type) {\n throw new TypeError('An event type is required.');\n }\n\n if (!options?.identity) {\n throw new TypeError(\n 'An identity is required. A server-side event has no device signature, so the ' +\n 'identity is the only thing tying the event to a user.',\n );\n }\n\n const eventId = options.eventId ?? randomEventId();\n\n if (eventId.startsWith('dregs-')) {\n throw new TypeError(\"Event ids starting with 'dregs-' are reserved for Dregs itself.\");\n }\n\n if (eventId.length > 64) {\n throw new TypeError('Event ids cannot be longer than 64 characters.');\n }\n\n const body: Record<string, unknown> = {\n id: eventId,\n type,\n data: { ...(options.data ?? {}) },\n identity: { id: options.identity, data: { ...(options.identityData ?? {}) } },\n source: options.source ?? DEFAULT_SOURCE,\n };\n\n if (options.timestamp !== undefined) {\n body.timestamp = toIsoUtc(options.timestamp);\n }\n\n return body;\n }\n\n /**\n * Sends one request, retrying what is worth retrying, and returns the parsed JSON body.\n *\n * The retry loop reuses the request body verbatim, which is what keeps a retried event\n * idempotent: the generated id is built once, before the first attempt.\n */\n private async request(method: string, path: string, body?: unknown): Promise<unknown> {\n const url = `${this.#baseUrl}/${path.replace(/^\\/+/, '')}`;\n const init = this.requestInit(method, body);\n\n for (let attempt = 0; ; attempt += 1) {\n let response: Response;\n let text: string;\n\n try {\n response = await this.#fetch(url, { ...init, signal: AbortSignal.timeout(this.#timeout) });\n text = await response.text();\n } catch (cause) {\n if (!this.shouldRetry(attempt, null)) {\n throw isTimeout(cause)\n ? new DregsTimeoutError(\n `The request to ${url} did not answer within ${this.#timeout}ms.`,\n { cause },\n )\n : new DregsConnectionError(`Could not reach Dregs at ${url}: ${describe(cause)}`, {\n cause,\n });\n }\n\n await sleep(this.backoff(attempt, null));\n\n continue;\n }\n\n try {\n return processResponse(response, text);\n } catch (error) {\n if (error instanceof DregsAPIError && this.shouldRetry(attempt, error.statusCode)) {\n await sleep(\n this.backoff(attempt, error instanceof RateLimitError ? error.retryAfter : null),\n );\n\n continue;\n }\n\n throw error;\n }\n }\n }\n\n private requestInit(method: string, body: unknown): RequestInit {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.#secretKey}`,\n Accept: 'application/json',\n 'User-Agent': userAgent(),\n };\n\n if (body === undefined) {\n return { method, headers };\n }\n\n headers['Content-Type'] = 'application/json';\n\n return { method, headers, body: JSON.stringify(body) };\n }\n\n private shouldRetry(attempt: number, statusCode: number | null): boolean {\n if (attempt >= this.#maxRetries) {\n return false;\n }\n\n return statusCode === null || RETRY_STATUSES.has(statusCode);\n }\n\n /**\n * Milliseconds to wait before attempt `attempt + 1`.\n *\n * `Retry-After` wins when the server sent one. Otherwise this is exponential with full jitter,\n * which keeps a fleet of workers that all hit the limit at once from retrying in lockstep.\n *\n * Protected rather than private so a test can stub the waiting out.\n */\n protected backoff(attempt: number, retryAfterSeconds: number | null): number {\n if (retryAfterSeconds !== null && retryAfterSeconds >= 0) {\n return Math.min(retryAfterSeconds * 1000, MAX_RETRY_AFTER_MS);\n }\n\n return Math.random() * Math.min(500 * 2 ** attempt, MAX_BACKOFF_MS);\n }\n}\n\n/** Turns a response into parsed JSON, or throws the matching error. */\nfunction processResponse(response: Response, text: string): unknown {\n const payload = parseJson(text);\n\n if (response.status >= 400) {\n throw apiError(response, payload);\n }\n\n // An older API build reported both of these as HTTP 200 with the outcome in the body. Reading\n // the body as well as the status keeps this SDK correct against either.\n if (isRecord(payload)) {\n if (payload.status === STATUS_RATE_LIMITED) {\n throw new RateLimitError('Ingestion rate limit exceeded for this credential.', {\n body: payload,\n requestId: requestId(response),\n retryAfter: retryAfter(response),\n });\n }\n\n if (payload.status === STATUS_QUOTA_EXCEEDED) {\n throw new QuotaExceededError('The account is over its monthly event limit.', {\n statusCode: 402,\n body: payload,\n requestId: requestId(response),\n });\n }\n }\n\n return payload;\n}\n\nfunction apiError(response: Response, payload: unknown): DregsAPIError {\n const message = messageFrom(payload) ?? response.statusText ?? 'Request failed';\n const id = requestId(response);\n\n if (response.status === 429) {\n return new RateLimitError(message, {\n body: payload,\n requestId: id,\n retryAfter: retryAfter(response),\n });\n }\n\n const ErrorClass = errorForStatus(response.status);\n\n return new ErrorClass(message, { statusCode: response.status, body: payload, requestId: id });\n}\n\nfunction messageFrom(payload: unknown): string | null {\n if (!isRecord(payload)) {\n return null;\n }\n\n for (const key of ['message', 'error', 'status'] as const) {\n const value = payload[key];\n\n if (typeof value === 'string' && value) {\n return value;\n }\n }\n\n return null;\n}\n\nfunction requestId(response: Response): string | null {\n return response.headers.get('X-Request-Id');\n}\n\nfunction retryAfter(response: Response): number | null {\n const raw = response.headers.get('Retry-After');\n\n if (raw === null) {\n return null;\n }\n\n const seconds = Number(raw);\n\n // The header also allows an HTTP date, which is rare enough here that falling back to the\n // client's own backoff beats parsing one.\n return Number.isFinite(seconds) ? seconds : null;\n}\n\nfunction parseJson(text: string): unknown {\n if (!text) {\n return null;\n }\n\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Whether a thrown value is the request running out of time.\n *\n * `AbortSignal.timeout` aborts with a `TimeoutError`, but which layer surfaces it depends on the\n * runtime, so the cause is worth checking too.\n */\nfunction isTimeout(cause: unknown): boolean {\n const names = [nameOf(cause), nameOf((cause as { cause?: unknown } | null)?.cause)];\n\n return names.includes('TimeoutError') || names.includes('AbortError');\n}\n\nfunction nameOf(value: unknown): string | null {\n if (typeof value !== 'object' || value === null || !('name' in value)) {\n return null;\n }\n\n const { name } = value;\n\n return typeof name === 'string' ? name : null;\n}\n\nfunction describe(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\nfunction readEnv(name: string): string | undefined {\n return typeof process === 'undefined' ? undefined : process.env[name];\n}\n\n/** A random event id with no `dregs-` prefix, which is reserved for server-generated ids. */\nfunction randomEventId(): string {\n return globalThis.crypto.randomUUID().replace(/-/g, '');\n}\n\n/**\n * Formats a timestamp the way the API's `Instant` parser expects.\n *\n * A string is re-parsed rather than passed through, so an unusable one is rejected here instead\n * of coming back as a puzzling 400.\n */\nfunction toIsoUtc(value: Date | string): string {\n const moment = value instanceof Date ? value : new Date(value);\n\n if (Number.isNaN(moment.getTime())) {\n throw new TypeError(`The timestamp ${JSON.stringify(value)} is not a valid date.`);\n }\n\n // Whole seconds lose the `.000`, which is the form the API's own examples use; anything\n // finer keeps its milliseconds.\n return moment.toISOString().replace(/\\.000Z$/, 'Z');\n}\n\nfunction userAgent(): string {\n const runtime =\n typeof process !== 'undefined' && process.versions?.node\n ? `node ${process.versions.node}`\n : 'unknown';\n\n return `dregs-node/${VERSION} (${runtime})`;\n}\n"]}
|