@shieldlabs-ai/node 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +486 -2
- package/dist/edge.cjs +1595 -0
- package/dist/edge.cjs.map +1 -0
- package/dist/edge.js +1569 -0
- package/dist/edge.js.map +1 -0
- package/dist/index.cjs +1585 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +594 -0
- package/dist/index.d.ts +594 -0
- package/dist/index.js +1559 -0
- package/dist/index.js.map +1 -0
- package/package.json +138 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1559 @@
|
|
|
1
|
+
import { timingSafeEqual, createHmac } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
|
|
5
|
+
// src/constants.ts
|
|
6
|
+
var NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
7
|
+
var SIGNALS = Object.freeze({
|
|
8
|
+
TOR: "tor",
|
|
9
|
+
JAVASCRIPT_DISABLED: "javascript_disabled",
|
|
10
|
+
OS_MISMATCH: "os_mismatch",
|
|
11
|
+
ANTIDETECT_BROWSER: "antidetect_browser",
|
|
12
|
+
PROXY_ROUTED_ANTIDETECT: "proxy_routed_antidetect",
|
|
13
|
+
PORT_SCAN_ROUTED_VIA_PROXY: "port_scan_routed_via_proxy",
|
|
14
|
+
BROWSER_AUTOMATION: "browser_automation",
|
|
15
|
+
STUN_NOT_CHECKED: "stun_not_checked",
|
|
16
|
+
STUN_LATE_CORRECTION: "stun_late_correction",
|
|
17
|
+
OS_NOT_DETECTED: "os_not_detected",
|
|
18
|
+
BROWSER_VPN_PROXY: "browser_vpn_proxy",
|
|
19
|
+
VPN: "vpn",
|
|
20
|
+
PRIVACY_RELAY: "privacy_relay",
|
|
21
|
+
PROXY: "proxy",
|
|
22
|
+
DATACENTER_IP: "datacenter_ip",
|
|
23
|
+
ABUSER: "abuser",
|
|
24
|
+
TIMEZONE_MISMATCH: "timezone_mismatch",
|
|
25
|
+
RATE_LIMITED: "rate_limited"
|
|
26
|
+
});
|
|
27
|
+
var RISK_BANDS = Object.freeze({
|
|
28
|
+
trusted: Object.freeze({ min: 0, max: 29 }),
|
|
29
|
+
suspicious: Object.freeze({ min: 30, max: 59 }),
|
|
30
|
+
dangerous: Object.freeze({ min: 60, max: 100 })
|
|
31
|
+
});
|
|
32
|
+
var RISK_BAND_NAMES = ["trusted", "suspicious", "dangerous"];
|
|
33
|
+
var LOOKUP_TYPES = [
|
|
34
|
+
"ip",
|
|
35
|
+
"user_hid",
|
|
36
|
+
"visitor_id",
|
|
37
|
+
"request_id",
|
|
38
|
+
"device_id",
|
|
39
|
+
"session_id",
|
|
40
|
+
"cookie_id"
|
|
41
|
+
];
|
|
42
|
+
var FLAG_KEYS = [
|
|
43
|
+
"vpn",
|
|
44
|
+
"privacy_relay",
|
|
45
|
+
"browser_vpn_proxy",
|
|
46
|
+
"tor",
|
|
47
|
+
"proxy",
|
|
48
|
+
"datacenter_ip",
|
|
49
|
+
"abuser",
|
|
50
|
+
"os_mismatch",
|
|
51
|
+
"os_not_detected",
|
|
52
|
+
"timezone_mismatch",
|
|
53
|
+
"anti_detect_browser",
|
|
54
|
+
"browser_automation",
|
|
55
|
+
"ip_mismatch",
|
|
56
|
+
"incognito",
|
|
57
|
+
"search_bot",
|
|
58
|
+
"suspicious_paid_click",
|
|
59
|
+
"javascript_disabled",
|
|
60
|
+
"stun_not_checked",
|
|
61
|
+
"check_incomplete"
|
|
62
|
+
];
|
|
63
|
+
var KNOWN_SCHEMA_VERSION = "2026-06-01";
|
|
64
|
+
var DEFAULT_HISTORY_BASE_URL = "https://account.shieldlabs.ai";
|
|
65
|
+
var DEFAULT_MANAGEMENT_BASE_URL = "https://api.shieldlabs.ai";
|
|
66
|
+
|
|
67
|
+
// src/errors.ts
|
|
68
|
+
var ShieldLabsError = class extends Error {
|
|
69
|
+
name = "ShieldLabsError";
|
|
70
|
+
constructor(message, options) {
|
|
71
|
+
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var EMPTY_HEADERS = { get: () => null };
|
|
75
|
+
var ApiError = class extends ShieldLabsError {
|
|
76
|
+
name = "ApiError";
|
|
77
|
+
/** HTTP status code. */
|
|
78
|
+
status;
|
|
79
|
+
/** Parsed response body: JSON value, raw text, or null when the body was empty. */
|
|
80
|
+
body;
|
|
81
|
+
/** Response headers. */
|
|
82
|
+
headers;
|
|
83
|
+
constructor(message, options) {
|
|
84
|
+
super(message, { cause: options.cause });
|
|
85
|
+
this.status = options.status;
|
|
86
|
+
this.body = options.body === void 0 ? null : options.body;
|
|
87
|
+
this.headers = options.headers ?? EMPTY_HEADERS;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var BadRequestError = class extends ApiError {
|
|
91
|
+
name = "BadRequestError";
|
|
92
|
+
};
|
|
93
|
+
var AuthenticationError = class extends ApiError {
|
|
94
|
+
name = "AuthenticationError";
|
|
95
|
+
};
|
|
96
|
+
var QuotaExceededError = class extends ApiError {
|
|
97
|
+
name = "QuotaExceededError";
|
|
98
|
+
};
|
|
99
|
+
var NotFoundError = class extends ApiError {
|
|
100
|
+
name = "NotFoundError";
|
|
101
|
+
};
|
|
102
|
+
var RateLimitError = class extends ApiError {
|
|
103
|
+
name = "RateLimitError";
|
|
104
|
+
/** Seconds to wait, from the Retry-After header, when the server sent one. */
|
|
105
|
+
retryAfter;
|
|
106
|
+
constructor(message, options) {
|
|
107
|
+
super(message, options);
|
|
108
|
+
this.retryAfter = options.retryAfter;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var ServerError = class extends ApiError {
|
|
112
|
+
name = "ServerError";
|
|
113
|
+
};
|
|
114
|
+
var ConnectionError = class extends ShieldLabsError {
|
|
115
|
+
name = "ConnectionError";
|
|
116
|
+
};
|
|
117
|
+
var TimeoutError = class extends ShieldLabsError {
|
|
118
|
+
name = "TimeoutError";
|
|
119
|
+
};
|
|
120
|
+
var SignatureVerificationError = class extends ShieldLabsError {
|
|
121
|
+
name = "SignatureVerificationError";
|
|
122
|
+
};
|
|
123
|
+
var WebhookParseError = class extends ShieldLabsError {
|
|
124
|
+
name = "WebhookParseError";
|
|
125
|
+
};
|
|
126
|
+
var ValidationError = class extends ShieldLabsError {
|
|
127
|
+
name = "ValidationError";
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/normalize.ts
|
|
131
|
+
function isPlainObject(value) {
|
|
132
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
|
+
}
|
|
134
|
+
function hasOwn(obj, key) {
|
|
135
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
136
|
+
}
|
|
137
|
+
function own(obj, key) {
|
|
138
|
+
return hasOwn(obj, key) ? obj[key] : void 0;
|
|
139
|
+
}
|
|
140
|
+
function stringField(obj, key) {
|
|
141
|
+
const value = own(obj, key);
|
|
142
|
+
return typeof value === "string" ? value : "";
|
|
143
|
+
}
|
|
144
|
+
function isTruthy(value) {
|
|
145
|
+
if (value === void 0 || value === null) return false;
|
|
146
|
+
if (typeof value === "boolean") return value;
|
|
147
|
+
if (typeof value === "number") return value !== 0;
|
|
148
|
+
if (typeof value === "string") return value.length > 0;
|
|
149
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
150
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
var WHITESPACE = /* @__PURE__ */ new Set([
|
|
154
|
+
9,
|
|
155
|
+
10,
|
|
156
|
+
11,
|
|
157
|
+
12,
|
|
158
|
+
13,
|
|
159
|
+
28,
|
|
160
|
+
29,
|
|
161
|
+
30,
|
|
162
|
+
31,
|
|
163
|
+
32,
|
|
164
|
+
133,
|
|
165
|
+
160,
|
|
166
|
+
5760,
|
|
167
|
+
8192,
|
|
168
|
+
8193,
|
|
169
|
+
8194,
|
|
170
|
+
8195,
|
|
171
|
+
8196,
|
|
172
|
+
8197,
|
|
173
|
+
8198,
|
|
174
|
+
8199,
|
|
175
|
+
8200,
|
|
176
|
+
8201,
|
|
177
|
+
8202,
|
|
178
|
+
8232,
|
|
179
|
+
8233,
|
|
180
|
+
8239,
|
|
181
|
+
8287,
|
|
182
|
+
12288
|
|
183
|
+
]);
|
|
184
|
+
function stripWhitespace(text) {
|
|
185
|
+
let start = 0;
|
|
186
|
+
let end = text.length;
|
|
187
|
+
while (start < end && WHITESPACE.has(text.charCodeAt(start))) start++;
|
|
188
|
+
while (end > start && WHITESPACE.has(text.charCodeAt(end - 1))) end--;
|
|
189
|
+
return text.slice(start, end);
|
|
190
|
+
}
|
|
191
|
+
function stripUnderscores(text) {
|
|
192
|
+
let start = 0;
|
|
193
|
+
let end = text.length;
|
|
194
|
+
while (start < end && text[start] === "_") start++;
|
|
195
|
+
while (end > start && text[end - 1] === "_") end--;
|
|
196
|
+
return text.slice(start, end);
|
|
197
|
+
}
|
|
198
|
+
var EXACT_SLUGS = /* @__PURE__ */ new Map([
|
|
199
|
+
["Is tor", "tor"],
|
|
200
|
+
["Is VPN", "vpn"],
|
|
201
|
+
["Is privacy relay", "privacy_relay"],
|
|
202
|
+
["Is proxy", "proxy"],
|
|
203
|
+
["Is datacenter", "datacenter_ip"],
|
|
204
|
+
["Is abuser", "abuser"],
|
|
205
|
+
["Stun is not checked", "stun_not_checked"],
|
|
206
|
+
["Stun passed (late arrival, corrected)", "stun_late_correction"],
|
|
207
|
+
["UA OS is not detected", "os_not_detected"],
|
|
208
|
+
["Network OS is not detected", "os_not_detected"],
|
|
209
|
+
["Browser timezone \u2260 IP-timezone", "timezone_mismatch"],
|
|
210
|
+
["Browser VPN/Proxy", "browser_vpn_proxy"],
|
|
211
|
+
["Browser Automation", "browser_automation"],
|
|
212
|
+
["Port scan routed via proxy (antidetect browser pattern)", "proxy_routed_antidetect"],
|
|
213
|
+
["User has been banned 1H, to many requests", "rate_limited"]
|
|
214
|
+
]);
|
|
215
|
+
var PREFIX_SLUGS = [
|
|
216
|
+
["Antidetect browser", "antidetect_browser"],
|
|
217
|
+
["Os_mismatch", "os_mismatch"],
|
|
218
|
+
["OS mismatch2", "os_mismatch2"],
|
|
219
|
+
["TCP handshake", "tcp_handshake_v2"],
|
|
220
|
+
["Latency test", "ws_tcp_latency"],
|
|
221
|
+
["JavaScript disabled", "javascript_disabled"]
|
|
222
|
+
];
|
|
223
|
+
var STICKY_PREFIX = "Sticky verdict: ";
|
|
224
|
+
var LETTER_OR_DIGIT = /^[\p{L}\p{Nd}]$/u;
|
|
225
|
+
function fallbackSlug(description) {
|
|
226
|
+
let text = stripWhitespace(description);
|
|
227
|
+
const paren = text.indexOf("(");
|
|
228
|
+
if (paren >= 0) text = stripWhitespace(text.slice(0, paren));
|
|
229
|
+
let out = "";
|
|
230
|
+
let previousWasSeparator = false;
|
|
231
|
+
for (const ch of text) {
|
|
232
|
+
if (ch === " " || ch === "-" || ch === "/") {
|
|
233
|
+
if (!previousWasSeparator && out.length > 0) {
|
|
234
|
+
out += "_";
|
|
235
|
+
previousWasSeparator = true;
|
|
236
|
+
}
|
|
237
|
+
} else if (ch === "\u2260") {
|
|
238
|
+
out += "_neq_";
|
|
239
|
+
previousWasSeparator = false;
|
|
240
|
+
} else if (LETTER_OR_DIGIT.test(ch)) {
|
|
241
|
+
out += ch.toLowerCase();
|
|
242
|
+
previousWasSeparator = false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const slug = stripUnderscores(out);
|
|
246
|
+
return slug === "" ? "unknown" : slug;
|
|
247
|
+
}
|
|
248
|
+
function signalSlug(description) {
|
|
249
|
+
const exact = EXACT_SLUGS.get(description);
|
|
250
|
+
if (exact !== void 0) return exact;
|
|
251
|
+
for (const [prefix, slug] of PREFIX_SLUGS) {
|
|
252
|
+
if (description.startsWith(prefix)) return slug;
|
|
253
|
+
}
|
|
254
|
+
if (description.startsWith(STICKY_PREFIX)) {
|
|
255
|
+
const colon = description.indexOf(":");
|
|
256
|
+
const rest = colon >= 0 && colon + 1 < description.length ? stripWhitespace(description.slice(colon + 1)) : description;
|
|
257
|
+
return fallbackSlug(rest);
|
|
258
|
+
}
|
|
259
|
+
return fallbackSlug(description);
|
|
260
|
+
}
|
|
261
|
+
var HISTORY_TIME = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(?:Z|[+-]\d{2}:?\d{2})?$/;
|
|
262
|
+
var RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})\n?$/;
|
|
263
|
+
function isLeapYear(year) {
|
|
264
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
265
|
+
}
|
|
266
|
+
function daysInMonth(year, month) {
|
|
267
|
+
if (month === 2) return isLeapYear(year) ? 29 : 28;
|
|
268
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
269
|
+
}
|
|
270
|
+
function pad(value, width) {
|
|
271
|
+
return String(value).padStart(width, "0");
|
|
272
|
+
}
|
|
273
|
+
function formatUtc(match, offsetMinutes) {
|
|
274
|
+
const [year, month, day, hour, minute, second] = [1, 2, 3, 4, 5, 6].map(
|
|
275
|
+
(i) => Number(match[i])
|
|
276
|
+
);
|
|
277
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month) || hour > 23 || minute > 59 || second > 59) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const local = /* @__PURE__ */ new Date(0);
|
|
281
|
+
local.setUTCFullYear(year, month - 1, day);
|
|
282
|
+
local.setUTCHours(hour, minute, second, 0);
|
|
283
|
+
const utc = new Date(local.getTime() - offsetMinutes * 6e4);
|
|
284
|
+
const utcYear = utc.getUTCFullYear();
|
|
285
|
+
if (utcYear < 1 || utcYear > 9999) return null;
|
|
286
|
+
const millis = (match[7] ?? "0").slice(0, 3).padEnd(3, "0");
|
|
287
|
+
return `${pad(utcYear, 4)}-${pad(utc.getUTCMonth() + 1, 2)}-${pad(utc.getUTCDate(), 2)}T${pad(utc.getUTCHours(), 2)}:${pad(utc.getUTCMinutes(), 2)}:${pad(utc.getUTCSeconds(), 2)}.${millis}Z`;
|
|
288
|
+
}
|
|
289
|
+
function parseHistoryTime(value) {
|
|
290
|
+
if (typeof value !== "string") return null;
|
|
291
|
+
const text = stripWhitespace(value);
|
|
292
|
+
if (text === "") return null;
|
|
293
|
+
const match = text.match(HISTORY_TIME);
|
|
294
|
+
return match ? formatUtc(match, 0) : null;
|
|
295
|
+
}
|
|
296
|
+
function parseRfc3339(value) {
|
|
297
|
+
if (typeof value !== "string") return null;
|
|
298
|
+
const match = value.match(RFC3339);
|
|
299
|
+
if (!match) return null;
|
|
300
|
+
const zone = match[8] ?? "Z";
|
|
301
|
+
let offsetMinutes = 0;
|
|
302
|
+
if (zone !== "Z") {
|
|
303
|
+
const sign = zone.startsWith("+") ? 1 : -1;
|
|
304
|
+
offsetMinutes = sign * (Number(zone.slice(1, 3)) * 60 + Number(zone.slice(4, 6)));
|
|
305
|
+
}
|
|
306
|
+
return formatUtc(match, offsetMinutes);
|
|
307
|
+
}
|
|
308
|
+
var HISTORY_FLAG_COLUMNS = {
|
|
309
|
+
vpn: "is_vpn",
|
|
310
|
+
privacy_relay: "is_privacy_relay",
|
|
311
|
+
tor: "is_tor",
|
|
312
|
+
proxy: "is_proxy",
|
|
313
|
+
datacenter_ip: "is_datacenter",
|
|
314
|
+
abuser: "is_abuser",
|
|
315
|
+
os_mismatch: "is_os_mismatch",
|
|
316
|
+
os_not_detected: "is_os_not_detected",
|
|
317
|
+
timezone_mismatch: "is_timezone_mismatch",
|
|
318
|
+
anti_detect_browser: "is_antidetect",
|
|
319
|
+
browser_automation: "is_browser_automation",
|
|
320
|
+
incognito: "is_incognito",
|
|
321
|
+
search_bot: "is_search_bot",
|
|
322
|
+
suspicious_paid_click: "is_suspicious_paid_click",
|
|
323
|
+
javascript_disabled: "is_js_disabled",
|
|
324
|
+
stun_not_checked: "is_stun_not_checked",
|
|
325
|
+
check_incomplete: "check_incomplete"
|
|
326
|
+
};
|
|
327
|
+
function normalizeIp(value) {
|
|
328
|
+
if (typeof value !== "string") return "";
|
|
329
|
+
const ip = stripWhitespace(value);
|
|
330
|
+
return ip === "0.0.0.0" ? "" : ip;
|
|
331
|
+
}
|
|
332
|
+
function normalizeUserHid(value) {
|
|
333
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
334
|
+
if (typeof value === "string") return value;
|
|
335
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
function normalizeScore(obj, key) {
|
|
339
|
+
if (!hasOwn(obj, key)) return 0;
|
|
340
|
+
const value = obj[key];
|
|
341
|
+
return typeof value === "number" ? value : Number.NaN;
|
|
342
|
+
}
|
|
343
|
+
function parseScoreDetails(value) {
|
|
344
|
+
const text = isTruthy(value) ? value : "[]";
|
|
345
|
+
if (typeof text !== "string") return [];
|
|
346
|
+
try {
|
|
347
|
+
const parsed = JSON.parse(text);
|
|
348
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
349
|
+
} catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function ipInfo(value) {
|
|
354
|
+
const obj = isPlainObject(value) ? value : {};
|
|
355
|
+
return { ip: normalizeIp(own(obj, "ip")), country: stringField(obj, "country") };
|
|
356
|
+
}
|
|
357
|
+
var TRAFFIC_KEYS = [
|
|
358
|
+
"channel",
|
|
359
|
+
"referrer_domain",
|
|
360
|
+
"landing_url",
|
|
361
|
+
"click_id_type",
|
|
362
|
+
"utm_source",
|
|
363
|
+
"utm_medium",
|
|
364
|
+
"utm_campaign",
|
|
365
|
+
"utm_content",
|
|
366
|
+
"utm_term"
|
|
367
|
+
];
|
|
368
|
+
function fromHistoryRow(row) {
|
|
369
|
+
const leakSource = stripWhitespace(stringField(row, "webrtc_leak_source"));
|
|
370
|
+
let localIp;
|
|
371
|
+
let localCountry;
|
|
372
|
+
if (leakSource !== "" && leakSource !== "none") {
|
|
373
|
+
localIp = normalizeIp(own(row, "webrtc_leak_ip"));
|
|
374
|
+
localCountry = stringField(row, "webrtc_leak_country");
|
|
375
|
+
} else {
|
|
376
|
+
localIp = normalizeIp(own(row, "web_rtc_ip"));
|
|
377
|
+
localCountry = stringField(row, "web_rtc_country");
|
|
378
|
+
}
|
|
379
|
+
const publicIp = normalizeIp(own(row, "ip"));
|
|
380
|
+
const signals = [];
|
|
381
|
+
let ipLeakDetail = false;
|
|
382
|
+
for (const entry of parseScoreDetails(own(row, "score_details"))) {
|
|
383
|
+
if (!isPlainObject(entry)) continue;
|
|
384
|
+
const description = stringField(entry, "Description");
|
|
385
|
+
if (description.startsWith("IP \u2260 leakIP")) ipLeakDetail = true;
|
|
386
|
+
const value = own(entry, "Value");
|
|
387
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value === 0) continue;
|
|
388
|
+
signals.push({ name: signalSlug(description), weight: value, description });
|
|
389
|
+
}
|
|
390
|
+
const searchBot = isTruthy(own(row, "is_search_bot"));
|
|
391
|
+
const flags = {};
|
|
392
|
+
for (const key of FLAG_KEYS) {
|
|
393
|
+
if (key === "browser_vpn_proxy") {
|
|
394
|
+
flags[key] = own(row, "connection_type") === "browser_vpn_proxy";
|
|
395
|
+
} else if (key === "ip_mismatch") {
|
|
396
|
+
flags[key] = !searchBot && (ipLeakDetail || publicIp !== "" && localIp !== "" && publicIp !== localIp);
|
|
397
|
+
} else {
|
|
398
|
+
flags[key] = isTruthy(own(row, HISTORY_FLAG_COLUMNS[key]));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const siteDomain = own(row, "site_domain");
|
|
402
|
+
let domain;
|
|
403
|
+
if (isTruthy(siteDomain)) {
|
|
404
|
+
domain = typeof siteDomain === "string" ? siteDomain : "";
|
|
405
|
+
} else {
|
|
406
|
+
domain = stringField(row, "domain");
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
request_id: stringField(row, "request_id"),
|
|
410
|
+
visitor_id: stringField(row, "visitor_id"),
|
|
411
|
+
device_id: stringField(row, "device_id"),
|
|
412
|
+
session_id: stringField(row, "session_id"),
|
|
413
|
+
cookie_id: stringField(row, "cookie_id"),
|
|
414
|
+
user_hid: normalizeUserHid(own(row, "user_hid")),
|
|
415
|
+
domain,
|
|
416
|
+
public_ip: { ip: publicIp, country: stringField(row, "country") },
|
|
417
|
+
local_ip: { ip: localIp, country: localCountry },
|
|
418
|
+
connection_type: stringField(row, "connection_type"),
|
|
419
|
+
os: stringField(row, "os"),
|
|
420
|
+
browser: stringField(row, "browser"),
|
|
421
|
+
device_type: stringField(row, "device_type"),
|
|
422
|
+
traffic_source: {
|
|
423
|
+
channel: stringField(row, "traffic_channel"),
|
|
424
|
+
referrer_domain: stringField(row, "referrer_domain"),
|
|
425
|
+
landing_url: stringField(row, "entry_url"),
|
|
426
|
+
click_id_type: stringField(row, "click_id_type"),
|
|
427
|
+
utm_source: stringField(row, "utm_source"),
|
|
428
|
+
utm_medium: stringField(row, "utm_medium"),
|
|
429
|
+
utm_campaign: stringField(row, "utm_campaign"),
|
|
430
|
+
utm_content: stringField(row, "utm_content"),
|
|
431
|
+
utm_term: stringField(row, "utm_term")
|
|
432
|
+
},
|
|
433
|
+
risk_score: normalizeScore(row, "score"),
|
|
434
|
+
signals,
|
|
435
|
+
detection_flags: flags,
|
|
436
|
+
observed_at: parseHistoryTime(own(row, "created_at")),
|
|
437
|
+
source: "history",
|
|
438
|
+
raw: row
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function fromWebhookData(data) {
|
|
442
|
+
const rawFlags = own(data, "detection_flags");
|
|
443
|
+
const flagSource = isPlainObject(rawFlags) ? rawFlags : {};
|
|
444
|
+
const flags = {};
|
|
445
|
+
for (const key of FLAG_KEYS) flags[key] = isTruthy(own(flagSource, key));
|
|
446
|
+
const rawTraffic = own(data, "traffic_source");
|
|
447
|
+
const trafficSource = isPlainObject(rawTraffic) ? rawTraffic : {};
|
|
448
|
+
const traffic = {};
|
|
449
|
+
for (const key of TRAFFIC_KEYS) traffic[key] = stringField(trafficSource, key);
|
|
450
|
+
const rawSignals = own(data, "signals");
|
|
451
|
+
const signals = Array.isArray(rawSignals) ? rawSignals.filter(isPlainObject).map((signal) => {
|
|
452
|
+
const weight = own(signal, "weight");
|
|
453
|
+
return {
|
|
454
|
+
name: stringField(signal, "name"),
|
|
455
|
+
weight: typeof weight === "number" ? weight : 0,
|
|
456
|
+
description: null
|
|
457
|
+
};
|
|
458
|
+
}) : [];
|
|
459
|
+
return {
|
|
460
|
+
request_id: stringField(data, "request_id"),
|
|
461
|
+
visitor_id: stringField(data, "visitor_id"),
|
|
462
|
+
device_id: stringField(data, "device_id"),
|
|
463
|
+
session_id: stringField(data, "session_id"),
|
|
464
|
+
cookie_id: stringField(data, "cookie_id"),
|
|
465
|
+
user_hid: normalizeUserHid(own(data, "user_hid")),
|
|
466
|
+
domain: stringField(data, "domain"),
|
|
467
|
+
public_ip: ipInfo(own(data, "public_ip")),
|
|
468
|
+
local_ip: ipInfo(own(data, "local_ip")),
|
|
469
|
+
connection_type: stringField(data, "connection_type"),
|
|
470
|
+
os: stringField(data, "os"),
|
|
471
|
+
browser: stringField(data, "browser"),
|
|
472
|
+
device_type: stringField(data, "device_type"),
|
|
473
|
+
traffic_source: traffic,
|
|
474
|
+
risk_score: normalizeScore(data, "risk_score"),
|
|
475
|
+
signals,
|
|
476
|
+
detection_flags: flags,
|
|
477
|
+
observed_at: parseRfc3339(own(data, "observed_at")),
|
|
478
|
+
source: "webhook",
|
|
479
|
+
raw: data
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function profileFromResponse(body) {
|
|
483
|
+
const weight = own(body, "Weight");
|
|
484
|
+
return {
|
|
485
|
+
domain: stringField(body, "Domain"),
|
|
486
|
+
remaining_identifications: typeof weight === "number" && Number.isFinite(weight) ? weight : 0,
|
|
487
|
+
public_key_masked: stringField(body, "PublicKey"),
|
|
488
|
+
secret_key_masked: stringField(body, "Secret"),
|
|
489
|
+
created_at: parseRfc3339(own(body, "CreatedAt")),
|
|
490
|
+
raw: body
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/version.ts
|
|
495
|
+
var VERSION = "1.0.0";
|
|
496
|
+
|
|
497
|
+
// src/runtime.ts
|
|
498
|
+
function detectRuntime() {
|
|
499
|
+
const g = globalThis;
|
|
500
|
+
if (typeof g.Bun?.version === "string") return `bun/${g.Bun.version}`;
|
|
501
|
+
if (typeof g.Deno?.version?.deno === "string") return `deno/${g.Deno.version.deno}`;
|
|
502
|
+
if (typeof g.EdgeRuntime === "string") return "edge-runtime";
|
|
503
|
+
if (g.navigator?.userAgent === "Cloudflare-Workers") return "workerd";
|
|
504
|
+
if (typeof g.process?.versions?.node === "string") return `node/${g.process.versions.node}`;
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
var cachedUserAgent;
|
|
508
|
+
function userAgent() {
|
|
509
|
+
if (cachedUserAgent === void 0) {
|
|
510
|
+
const runtime = detectRuntime();
|
|
511
|
+
cachedUserAgent = runtime ? `shieldlabs-node/${VERSION} ${runtime}` : `shieldlabs-node/${VERSION}`;
|
|
512
|
+
}
|
|
513
|
+
return cachedUserAgent;
|
|
514
|
+
}
|
|
515
|
+
function abortReason(signal) {
|
|
516
|
+
if (signal.reason !== void 0) return signal.reason;
|
|
517
|
+
const error = new Error("The operation was aborted.");
|
|
518
|
+
error.name = "AbortError";
|
|
519
|
+
return error;
|
|
520
|
+
}
|
|
521
|
+
function throwIfAborted(signal) {
|
|
522
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
523
|
+
}
|
|
524
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
525
|
+
function sleep(ms, signal) {
|
|
526
|
+
return new Promise((resolve, reject) => {
|
|
527
|
+
if (signal?.aborted) {
|
|
528
|
+
reject(abortReason(signal));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
let timer;
|
|
532
|
+
const onAbort = () => {
|
|
533
|
+
clearTimeout(timer);
|
|
534
|
+
reject(abortReason(signal));
|
|
535
|
+
};
|
|
536
|
+
const wait = (left) => {
|
|
537
|
+
timer = setTimeout(
|
|
538
|
+
() => {
|
|
539
|
+
if (left > MAX_TIMER_DELAY_MS) {
|
|
540
|
+
wait(left - MAX_TIMER_DELAY_MS);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
signal?.removeEventListener("abort", onAbort);
|
|
544
|
+
resolve();
|
|
545
|
+
},
|
|
546
|
+
Math.min(left, MAX_TIMER_DELAY_MS)
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
wait(ms);
|
|
550
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
var warned = /* @__PURE__ */ new Set();
|
|
554
|
+
var MAX_WARNINGS = 32;
|
|
555
|
+
function warnOnce(key, message) {
|
|
556
|
+
if (warned.has(key) || warned.size >= MAX_WARNINGS) return;
|
|
557
|
+
warned.add(key);
|
|
558
|
+
console.warn(`[shieldlabs] ${message}`);
|
|
559
|
+
}
|
|
560
|
+
function warnIfBrowserPage(client, key) {
|
|
561
|
+
const g = globalThis;
|
|
562
|
+
if (g.window !== void 0 && g.document !== void 0) {
|
|
563
|
+
warnOnce(
|
|
564
|
+
`browser-page:${client}`,
|
|
565
|
+
`${client} was created in a web page, where its ${key} is visible to every visitor. Use @shieldlabs-ai/node on your server only; in the page, use the browser SDK (@shieldlabs-ai/js).`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/webhooks.ts
|
|
571
|
+
var SIGNATURE_PREFIX = "sha256=";
|
|
572
|
+
var HEX_DIGEST = /^[0-9a-fA-F]{64}$/;
|
|
573
|
+
var MISMATCH = "The webhook signature does not match the payload. Check the endpoint signing secret (including its whsec_ prefix) and verify the raw body bytes, not re-serialized JSON.";
|
|
574
|
+
var encoder = new TextEncoder();
|
|
575
|
+
function parseSignatureHeader(header) {
|
|
576
|
+
let value = header;
|
|
577
|
+
if (Array.isArray(value)) {
|
|
578
|
+
if (value.length !== 1) return null;
|
|
579
|
+
value = value[0];
|
|
580
|
+
}
|
|
581
|
+
if (typeof value !== "string") return null;
|
|
582
|
+
const trimmed = value.trim();
|
|
583
|
+
if (!trimmed.startsWith(SIGNATURE_PREFIX)) return null;
|
|
584
|
+
const digest = trimmed.slice(SIGNATURE_PREFIX.length);
|
|
585
|
+
return HEX_DIGEST.test(digest) ? digest.toLowerCase() : null;
|
|
586
|
+
}
|
|
587
|
+
function secretList(secret) {
|
|
588
|
+
const list = Array.isArray(secret) ? secret : [secret];
|
|
589
|
+
return list.filter((item) => typeof item === "string" && item !== "");
|
|
590
|
+
}
|
|
591
|
+
function isArrayBuffer(value) {
|
|
592
|
+
return value instanceof ArrayBuffer || Object.prototype.toString.call(value) === "[object ArrayBuffer]";
|
|
593
|
+
}
|
|
594
|
+
function payloadBytes(payload) {
|
|
595
|
+
if (typeof payload === "string") return encoder.encode(payload);
|
|
596
|
+
if (payload instanceof Uint8Array) return payload;
|
|
597
|
+
if (ArrayBuffer.isView(payload)) {
|
|
598
|
+
return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
599
|
+
}
|
|
600
|
+
if (isArrayBuffer(payload)) return new Uint8Array(payload);
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
function hexToBytes(hex) {
|
|
604
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
605
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
606
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
607
|
+
}
|
|
608
|
+
return bytes;
|
|
609
|
+
}
|
|
610
|
+
function bytesToHex(bytes) {
|
|
611
|
+
let hex = "";
|
|
612
|
+
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
613
|
+
return hex;
|
|
614
|
+
}
|
|
615
|
+
function constantTimeEqual(a, b) {
|
|
616
|
+
if (a.length !== b.length) return false;
|
|
617
|
+
let diff = 0;
|
|
618
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
619
|
+
return diff === 0;
|
|
620
|
+
}
|
|
621
|
+
function prepare(payload, header, secret) {
|
|
622
|
+
const bytes = payloadBytes(payload);
|
|
623
|
+
if (bytes === null) {
|
|
624
|
+
return {
|
|
625
|
+
ok: false,
|
|
626
|
+
reason: "The payload must be the raw request body (a string, Uint8Array or ArrayBuffer). A parsed object cannot be verified because re-serializing changes the bytes."
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const digest = parseSignatureHeader(header);
|
|
630
|
+
if (digest === null) {
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
reason: "The X-Shield-Signature header is missing or malformed (expected sha256=<64 hex characters>)."
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const secrets = secretList(secret);
|
|
637
|
+
if (secrets.length === 0) {
|
|
638
|
+
return { ok: false, reason: "No webhook signing secret was provided." };
|
|
639
|
+
}
|
|
640
|
+
return { ok: true, bytes, expected: hexToBytes(digest), secrets };
|
|
641
|
+
}
|
|
642
|
+
var decoder = new TextDecoder("utf-8");
|
|
643
|
+
function preview(text) {
|
|
644
|
+
return text.length > 40 ? `${text.slice(0, 37)}...` : text;
|
|
645
|
+
}
|
|
646
|
+
function parseEvent(bytes) {
|
|
647
|
+
const text = decoder.decode(bytes);
|
|
648
|
+
let parsed;
|
|
649
|
+
try {
|
|
650
|
+
parsed = JSON.parse(text);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
throw new WebhookParseError("The webhook body is not valid JSON.", { cause: error });
|
|
653
|
+
}
|
|
654
|
+
if (!isPlainObject(parsed)) {
|
|
655
|
+
throw new WebhookParseError("The webhook body is not a JSON object.");
|
|
656
|
+
}
|
|
657
|
+
const eventType = parsed.event_type;
|
|
658
|
+
if (typeof eventType !== "string" || eventType === "") {
|
|
659
|
+
throw new WebhookParseError("The webhook body has no event_type.");
|
|
660
|
+
}
|
|
661
|
+
const schemaVersion = typeof parsed.schema_version === "string" ? parsed.schema_version : "";
|
|
662
|
+
const createdAt = typeof parsed.created_at === "string" ? parsed.created_at : "";
|
|
663
|
+
if (schemaVersion !== KNOWN_SCHEMA_VERSION) {
|
|
664
|
+
warnOnce(
|
|
665
|
+
`schema-version:${schemaVersion}`,
|
|
666
|
+
`Received a webhook with schema_version "${preview(schemaVersion)}" while this SDK version targets "${KNOWN_SCHEMA_VERSION}". The event was parsed; update @shieldlabs-ai/node for typed support of newer fields.`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (eventType === "identification.scored") {
|
|
670
|
+
if (!isPlainObject(parsed.data)) {
|
|
671
|
+
throw new WebhookParseError("The identification.scored event has no data object.");
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
event_type: "identification.scored",
|
|
675
|
+
schema_version: schemaVersion,
|
|
676
|
+
created_at: createdAt,
|
|
677
|
+
data: fromWebhookData(parsed.data),
|
|
678
|
+
raw: parsed
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (eventType === "webhook.ping") {
|
|
682
|
+
return {
|
|
683
|
+
event_type: "webhook.ping",
|
|
684
|
+
schema_version: schemaVersion,
|
|
685
|
+
created_at: createdAt,
|
|
686
|
+
raw: parsed
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
event_type: eventType,
|
|
691
|
+
schema_version: schemaVersion,
|
|
692
|
+
created_at: createdAt,
|
|
693
|
+
raw: parsed
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function syncUnavailable(fn, alternative) {
|
|
697
|
+
return new ShieldLabsError(
|
|
698
|
+
`${fn}() is synchronous and needs node:crypto, which the edge build of @shieldlabs-ai/node does not load. Use await ${alternative}() instead.`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
function createWebhooks(backend2) {
|
|
702
|
+
const equal = backend2.equal ?? constantTimeEqual;
|
|
703
|
+
function verifySync(payload, header, secret, hmac) {
|
|
704
|
+
const prepared = prepare(payload, header, secret);
|
|
705
|
+
if (!prepared.ok) return prepared;
|
|
706
|
+
for (const key of prepared.secrets) {
|
|
707
|
+
if (equal(hmac(key, prepared.bytes), prepared.expected)) return prepared;
|
|
708
|
+
}
|
|
709
|
+
return { ok: false, reason: MISMATCH };
|
|
710
|
+
}
|
|
711
|
+
async function verifyAsync(payload, header, secret) {
|
|
712
|
+
const prepared = prepare(payload, header, secret);
|
|
713
|
+
if (!prepared.ok) return prepared;
|
|
714
|
+
for (const key of prepared.secrets) {
|
|
715
|
+
if (equal(await backend2.hmacAsync(key, prepared.bytes), prepared.expected)) return prepared;
|
|
716
|
+
}
|
|
717
|
+
return { ok: false, reason: MISMATCH };
|
|
718
|
+
}
|
|
719
|
+
function requireSync(fn, alternative) {
|
|
720
|
+
if (backend2.hmacSync === null) throw syncUnavailable(fn, alternative);
|
|
721
|
+
return backend2.hmacSync;
|
|
722
|
+
}
|
|
723
|
+
return Object.freeze({
|
|
724
|
+
verifySignature(payload, signatureHeader, secret) {
|
|
725
|
+
const hmac = requireSync("webhooks.verifySignature", "webhooks.verifySignatureAsync");
|
|
726
|
+
return verifySync(payload, signatureHeader, secret, hmac).ok;
|
|
727
|
+
},
|
|
728
|
+
constructEvent(payload, signatureHeader, secret) {
|
|
729
|
+
const hmac = requireSync("webhooks.constructEvent", "webhooks.constructEventAsync");
|
|
730
|
+
const result = verifySync(payload, signatureHeader, secret, hmac);
|
|
731
|
+
if (!result.ok) throw new SignatureVerificationError(result.reason);
|
|
732
|
+
return parseEvent(result.bytes);
|
|
733
|
+
},
|
|
734
|
+
async verifySignatureAsync(payload, signatureHeader, secret) {
|
|
735
|
+
return (await verifyAsync(payload, signatureHeader, secret)).ok;
|
|
736
|
+
},
|
|
737
|
+
async constructEventAsync(payload, signatureHeader, secret) {
|
|
738
|
+
const result = await verifyAsync(payload, signatureHeader, secret);
|
|
739
|
+
if (!result.ok) throw new SignatureVerificationError(result.reason);
|
|
740
|
+
return parseEvent(result.bytes);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function validateUserHidInput(userId, secret) {
|
|
745
|
+
if (typeof userId !== "string" || userId === "") {
|
|
746
|
+
throw new ValidationError("userId must be a non-empty string.");
|
|
747
|
+
}
|
|
748
|
+
if (typeof secret !== "string" || secret === "") {
|
|
749
|
+
throw new ValidationError("secret must be a non-empty string.");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function computeUserHid(backend2, userId, secret) {
|
|
753
|
+
if (backend2.hmacSync === null) throw syncUnavailable("userHid", "userHidAsync");
|
|
754
|
+
validateUserHidInput(userId, secret);
|
|
755
|
+
return bytesToHex(backend2.hmacSync(secret, encoder.encode(userId)));
|
|
756
|
+
}
|
|
757
|
+
async function computeUserHidAsync(backend2, userId, secret) {
|
|
758
|
+
validateUserHidInput(userId, secret);
|
|
759
|
+
return bytesToHex(await backend2.hmacAsync(secret, encoder.encode(userId)));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/http.ts
|
|
763
|
+
var BACKOFF_BASE_MS = 500;
|
|
764
|
+
var BACKOFF_CAP_MS = 8e3;
|
|
765
|
+
var RETRY_AFTER_CAP_MS = 1e4;
|
|
766
|
+
var RATE_LIMIT_FLOOR_MS = 1e3;
|
|
767
|
+
var MAX_DETAIL_LENGTH = 200;
|
|
768
|
+
function isLoopbackHost(hostname) {
|
|
769
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "[::1]" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname);
|
|
770
|
+
}
|
|
771
|
+
function normalizeBaseUrl(input, name, stripApiSuffix, allowInsecureHttp = false) {
|
|
772
|
+
if (typeof input !== "string" || input.trim() === "") {
|
|
773
|
+
throw new ValidationError(`${name} must be a non-empty URL.`);
|
|
774
|
+
}
|
|
775
|
+
let url;
|
|
776
|
+
try {
|
|
777
|
+
url = new URL(input.trim());
|
|
778
|
+
} catch {
|
|
779
|
+
throw new ValidationError(`${name} is not a valid URL.`);
|
|
780
|
+
}
|
|
781
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
782
|
+
throw new ValidationError(`${name} must be an https URL.`);
|
|
783
|
+
}
|
|
784
|
+
if (url.username !== "" || url.password !== "") {
|
|
785
|
+
throw new ValidationError(`${name} must not contain credentials.`);
|
|
786
|
+
}
|
|
787
|
+
if (url.search !== "" || url.hash !== "") {
|
|
788
|
+
throw new ValidationError(`${name} must not contain a query string or a fragment.`);
|
|
789
|
+
}
|
|
790
|
+
if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
|
|
791
|
+
if (!allowInsecureHttp) {
|
|
792
|
+
throw new ValidationError(
|
|
793
|
+
`${name} must be an https URL: with plain http the key would travel unencrypted. Plain http is accepted for localhost, 127.0.0.1 and [::1]; for a test server on another host, pass allowInsecureHttp: true.`
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
warnOnce(
|
|
797
|
+
`insecure-base-url:${url.origin}`,
|
|
798
|
+
`${name} uses plain http, so credentials travel unencrypted. Use https outside local testing.`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
802
|
+
if (stripApiSuffix && /\/api$/i.test(path)) path = path.slice(0, -4).replace(/\/+$/, "");
|
|
803
|
+
return `${url.origin}${path}`;
|
|
804
|
+
}
|
|
805
|
+
function parseRetryAfter(value, nowMs = Date.now()) {
|
|
806
|
+
if (value === null || value === void 0) return void 0;
|
|
807
|
+
const text = value.trim();
|
|
808
|
+
if (/^\d+(?:\.\d+)?$/.test(text)) return Number(text);
|
|
809
|
+
if (!/[a-z]{3}/i.test(text)) return void 0;
|
|
810
|
+
const date = Date.parse(text);
|
|
811
|
+
if (Number.isNaN(date)) return void 0;
|
|
812
|
+
return Math.max(0, Math.ceil((date - nowMs) / 1e3));
|
|
813
|
+
}
|
|
814
|
+
function truncate(text) {
|
|
815
|
+
return text.length > MAX_DETAIL_LENGTH ? `${text.slice(0, MAX_DETAIL_LENGTH - 3)}...` : text;
|
|
816
|
+
}
|
|
817
|
+
function errorDetail(body, parsedJson, text) {
|
|
818
|
+
if (parsedJson) {
|
|
819
|
+
if (isPlainObject(body) && typeof body.error === "string" && body.error !== "") {
|
|
820
|
+
return truncate(body.error);
|
|
821
|
+
}
|
|
822
|
+
if (typeof body === "string" && body !== "") return truncate(body);
|
|
823
|
+
return void 0;
|
|
824
|
+
}
|
|
825
|
+
const trimmed = text.trim();
|
|
826
|
+
if (trimmed === "" || trimmed.startsWith("<")) return void 0;
|
|
827
|
+
return truncate(trimmed);
|
|
828
|
+
}
|
|
829
|
+
function hintFor(apiName, status) {
|
|
830
|
+
if (status === 401 || status === 403) {
|
|
831
|
+
return apiName === "History API" ? " Check that apiKey is the Private API Key (sec_...) of this domain." : " Check the Secret Key and that domain matches the registered domain exactly.";
|
|
832
|
+
}
|
|
833
|
+
if (status === 404) return " Check the base URL.";
|
|
834
|
+
if (status === 429 && apiName === "Management API") {
|
|
835
|
+
return " The Management API allows about 15 requests per minute per IP, then blocks the IP for 10 minutes.";
|
|
836
|
+
}
|
|
837
|
+
return "";
|
|
838
|
+
}
|
|
839
|
+
function createApiError(apiName, status, text, headers) {
|
|
840
|
+
let body = null;
|
|
841
|
+
let parsedJson = false;
|
|
842
|
+
if (text !== "") {
|
|
843
|
+
try {
|
|
844
|
+
body = JSON.parse(text);
|
|
845
|
+
parsedJson = true;
|
|
846
|
+
} catch {
|
|
847
|
+
body = text;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const detail = errorDetail(body, parsedJson, text);
|
|
851
|
+
const message = `${apiName} request failed with HTTP ${status}${detail ? ` (${detail})` : ""}.${hintFor(apiName, status)}`;
|
|
852
|
+
const options = { status, body, headers };
|
|
853
|
+
if (status === 400) return new BadRequestError(message, options);
|
|
854
|
+
if (status === 401 || status === 403) return new AuthenticationError(message, options);
|
|
855
|
+
if (status === 402) return new QuotaExceededError(message, options);
|
|
856
|
+
if (status === 404) return new NotFoundError(message, options);
|
|
857
|
+
if (status === 429) {
|
|
858
|
+
let retryAfter;
|
|
859
|
+
try {
|
|
860
|
+
retryAfter = parseRetryAfter(headers.get("retry-after"));
|
|
861
|
+
} catch {
|
|
862
|
+
retryAfter = void 0;
|
|
863
|
+
}
|
|
864
|
+
return new RateLimitError(message, { ...options, retryAfter });
|
|
865
|
+
}
|
|
866
|
+
if (status >= 500 && status <= 599) return new ServerError(message, options);
|
|
867
|
+
return new ApiError(message, options);
|
|
868
|
+
}
|
|
869
|
+
function messageOf(error) {
|
|
870
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
871
|
+
}
|
|
872
|
+
function causeDetail(error) {
|
|
873
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
874
|
+
if (typeof cause !== "object" || cause === null) return "";
|
|
875
|
+
const { code, message } = cause;
|
|
876
|
+
const codeText = typeof code === "string" ? code : "";
|
|
877
|
+
const text = typeof message === "string" ? message.trim() : "";
|
|
878
|
+
if (text !== "" && codeText !== "" && !text.includes(codeText)) {
|
|
879
|
+
return ` (${codeText}: ${truncate(text)})`;
|
|
880
|
+
}
|
|
881
|
+
if (text !== "") return ` (${truncate(text)})`;
|
|
882
|
+
return codeText === "" ? "" : ` (${codeText})`;
|
|
883
|
+
}
|
|
884
|
+
function printableTexts(error) {
|
|
885
|
+
const texts = [];
|
|
886
|
+
let current = error;
|
|
887
|
+
for (let depth = 0; depth < 5 && current !== void 0 && current !== null; depth++) {
|
|
888
|
+
if (typeof current !== "object") {
|
|
889
|
+
if (typeof current === "string") texts.push(current);
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
const { message, stack } = current;
|
|
893
|
+
if (typeof message === "string") texts.push(message);
|
|
894
|
+
if (typeof stack === "string") texts.push(stack);
|
|
895
|
+
try {
|
|
896
|
+
texts.push(JSON.stringify(current) ?? "");
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
current = current.cause;
|
|
900
|
+
}
|
|
901
|
+
return texts;
|
|
902
|
+
}
|
|
903
|
+
function mentionsSecret(texts, secrets) {
|
|
904
|
+
return secrets.some((secret) => secret !== "" && texts.some((text) => text.includes(secret)));
|
|
905
|
+
}
|
|
906
|
+
function raceWithAbort(promise, signal) {
|
|
907
|
+
return new Promise((resolve, reject) => {
|
|
908
|
+
if (signal.aborted) {
|
|
909
|
+
reject(abortReason(signal));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const onAbort = () => reject(abortReason(signal));
|
|
913
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
914
|
+
promise.then(
|
|
915
|
+
(value) => {
|
|
916
|
+
signal.removeEventListener("abort", onAbort);
|
|
917
|
+
resolve(value);
|
|
918
|
+
},
|
|
919
|
+
(error) => {
|
|
920
|
+
signal.removeEventListener("abort", onAbort);
|
|
921
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
922
|
+
}
|
|
923
|
+
);
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
function globalFetch() {
|
|
927
|
+
const candidate = globalThis.fetch;
|
|
928
|
+
if (typeof candidate !== "function") {
|
|
929
|
+
throw new ValidationError(
|
|
930
|
+
"No fetch implementation is available. Use Node.js 18 or later, or pass the fetch option."
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
const fetchFn = candidate;
|
|
934
|
+
return (url, init) => fetchFn.call(globalThis, url, init);
|
|
935
|
+
}
|
|
936
|
+
var Transport = class {
|
|
937
|
+
#options;
|
|
938
|
+
constructor(options) {
|
|
939
|
+
this.#options = options;
|
|
940
|
+
}
|
|
941
|
+
/** The client's per-attempt timeout in milliseconds. */
|
|
942
|
+
get timeout() {
|
|
943
|
+
return this.#options.timeout;
|
|
944
|
+
}
|
|
945
|
+
async getJson(path, options = {}) {
|
|
946
|
+
const maxRetries = options.maxRetries ?? this.#options.maxRetries;
|
|
947
|
+
let url = `${this.#options.baseUrl}${path}`;
|
|
948
|
+
if (options.query) url += `?${new URLSearchParams(options.query).toString()}`;
|
|
949
|
+
for (let attempt = 0; ; attempt++) {
|
|
950
|
+
throwIfAborted(options.signal);
|
|
951
|
+
try {
|
|
952
|
+
return await this.#attempt(url, options.signal, options.timeout);
|
|
953
|
+
} catch (error) {
|
|
954
|
+
if (attempt >= maxRetries || !this.#isRetryable(error)) throw error;
|
|
955
|
+
await sleep(this.#retryDelay(error, attempt), options.signal);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
#isRetryable(error) {
|
|
960
|
+
if (error instanceof ConnectionError || error instanceof TimeoutError) return true;
|
|
961
|
+
if (error instanceof RateLimitError) return this.#options.retryRateLimited;
|
|
962
|
+
return error instanceof ServerError;
|
|
963
|
+
}
|
|
964
|
+
#retryDelay(error, attempt) {
|
|
965
|
+
if (error instanceof ApiError) {
|
|
966
|
+
const retryAfter = error instanceof RateLimitError ? error.retryAfter : parseRetryAfter(error.headers.get("retry-after"));
|
|
967
|
+
if (retryAfter !== void 0) return Math.min(retryAfter * 1e3, RETRY_AFTER_CAP_MS);
|
|
968
|
+
}
|
|
969
|
+
const nominal = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
970
|
+
const delay = nominal / 2 + Math.random() * (nominal / 2);
|
|
971
|
+
return error instanceof RateLimitError ? Math.max(delay, RATE_LIMIT_FLOOR_MS) : delay;
|
|
972
|
+
}
|
|
973
|
+
#connectionError(error) {
|
|
974
|
+
const { apiName, secrets } = this.#options;
|
|
975
|
+
const message = `${apiName} request failed: ${messageOf(error)}${causeDetail(error)}`;
|
|
976
|
+
if (mentionsSecret([message, ...printableTexts(error)], secrets)) {
|
|
977
|
+
return new ConnectionError(
|
|
978
|
+
`${apiName} request failed before a response was received. The underlying error was withheld because it contained a credential.`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
return new ConnectionError(message, { cause: error });
|
|
982
|
+
}
|
|
983
|
+
async #attempt(url, signal, timeoutCap) {
|
|
984
|
+
const { apiName } = this.#options;
|
|
985
|
+
const timeout = Math.min(this.#options.timeout, timeoutCap ?? Number.POSITIVE_INFINITY);
|
|
986
|
+
const fetchImpl = this.#options.fetch ?? globalFetch();
|
|
987
|
+
const controller = new AbortController();
|
|
988
|
+
let timedOut = false;
|
|
989
|
+
const timer = setTimeout(() => {
|
|
990
|
+
timedOut = true;
|
|
991
|
+
controller.abort();
|
|
992
|
+
}, timeout);
|
|
993
|
+
const onAbort = () => controller.abort();
|
|
994
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
995
|
+
let response;
|
|
996
|
+
let text;
|
|
997
|
+
try {
|
|
998
|
+
const init = {
|
|
999
|
+
method: "GET",
|
|
1000
|
+
headers: {
|
|
1001
|
+
Accept: "application/json",
|
|
1002
|
+
"User-Agent": userAgent(),
|
|
1003
|
+
...this.#options.headers
|
|
1004
|
+
},
|
|
1005
|
+
signal: controller.signal
|
|
1006
|
+
};
|
|
1007
|
+
response = await raceWithAbort(
|
|
1008
|
+
Promise.resolve().then(() => fetchImpl(url, init)),
|
|
1009
|
+
controller.signal
|
|
1010
|
+
);
|
|
1011
|
+
text = await raceWithAbort(
|
|
1012
|
+
Promise.resolve().then(() => response.text()),
|
|
1013
|
+
controller.signal
|
|
1014
|
+
);
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1017
|
+
if (timedOut) {
|
|
1018
|
+
throw new TimeoutError(`${apiName} request timed out after ${timeout} ms.`, {
|
|
1019
|
+
cause: error
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (error instanceof ShieldLabsError) throw error;
|
|
1023
|
+
throw this.#connectionError(error);
|
|
1024
|
+
} finally {
|
|
1025
|
+
clearTimeout(timer);
|
|
1026
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1027
|
+
}
|
|
1028
|
+
const { status, headers } = response;
|
|
1029
|
+
if (status >= 200 && status < 300) {
|
|
1030
|
+
try {
|
|
1031
|
+
return { body: JSON.parse(text), status, headers };
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
throw new ApiError(
|
|
1034
|
+
`${apiName} returned a response that is not valid JSON (HTTP ${status}).`,
|
|
1035
|
+
{
|
|
1036
|
+
status,
|
|
1037
|
+
body: text,
|
|
1038
|
+
headers,
|
|
1039
|
+
cause: error
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
throw createApiError(apiName, status, text, headers);
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
// src/validation.ts
|
|
1049
|
+
var UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
1050
|
+
var IPV4_OCTET = "(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)";
|
|
1051
|
+
var IPV4 = new RegExp(`^${IPV4_OCTET}(?:\\.${IPV4_OCTET}){3}$`);
|
|
1052
|
+
function describe(value) {
|
|
1053
|
+
const text = typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
1054
|
+
return text.length > 40 ? `${text.slice(0, 37)}...` : text;
|
|
1055
|
+
}
|
|
1056
|
+
var PLAIN_IN_PATH = /%(?:24|26|2B|2C|3A|3B|3D|40)/g;
|
|
1057
|
+
var ESCAPED_IN_PATH = /[!'()*]/g;
|
|
1058
|
+
function encodePathSegment(value) {
|
|
1059
|
+
let escaped;
|
|
1060
|
+
try {
|
|
1061
|
+
escaped = encodeURIComponent(value);
|
|
1062
|
+
} catch {
|
|
1063
|
+
throw new ValidationError(
|
|
1064
|
+
"user_hid must be valid Unicode text (it has an unpaired surrogate)."
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
return escaped.replace(PLAIN_IN_PATH, (match) => decodeURIComponent(match)).replace(ESCAPED_IN_PATH, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
1068
|
+
}
|
|
1069
|
+
function validateLookup(type, value) {
|
|
1070
|
+
if (typeof type !== "string" || !LOOKUP_TYPES.includes(type)) {
|
|
1071
|
+
throw new ValidationError(
|
|
1072
|
+
`Unknown lookup type ${describe(type)}. Use one of: ${LOOKUP_TYPES.join(", ")}.`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
const lookupType = type;
|
|
1076
|
+
if (typeof value !== "string") {
|
|
1077
|
+
throw new ValidationError(`The ${lookupType} value must be a string.`);
|
|
1078
|
+
}
|
|
1079
|
+
if (lookupType === "user_hid") {
|
|
1080
|
+
if (value === "") throw new ValidationError("user_hid must be a non-empty string.");
|
|
1081
|
+
if (value === "." || value === "..") {
|
|
1082
|
+
throw new ValidationError(
|
|
1083
|
+
'user_hid cannot be "." or "..": a URL path segment cannot carry it.'
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
if (value.includes("/")) {
|
|
1087
|
+
throw new ValidationError(
|
|
1088
|
+
'user_hid cannot contain "/": the History API cannot search a value with a slash. Use URL-safe User HIDs, such as the 64 hex characters of userHid().'
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
return { type: lookupType, value, segment: encodePathSegment(value) };
|
|
1092
|
+
}
|
|
1093
|
+
if (lookupType === "ip") {
|
|
1094
|
+
if (value.includes(":")) {
|
|
1095
|
+
throw new ValidationError(
|
|
1096
|
+
"IPv6 addresses cannot be searched: the History API stores IPv4 addresses only."
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
if (!IPV4.test(value)) {
|
|
1100
|
+
throw new ValidationError("ip must be a dotted IPv4 address such as 203.0.113.24.");
|
|
1101
|
+
}
|
|
1102
|
+
return { type: lookupType, value, segment: value };
|
|
1103
|
+
}
|
|
1104
|
+
if (!UUID.test(value)) {
|
|
1105
|
+
throw new ValidationError(`${lookupType} must be a UUID (8-4-4-4-12 hexadecimal digits).`);
|
|
1106
|
+
}
|
|
1107
|
+
const uuid = value.toLowerCase();
|
|
1108
|
+
return { type: lookupType, value: uuid, segment: uuid };
|
|
1109
|
+
}
|
|
1110
|
+
function validateRequestId(requestId) {
|
|
1111
|
+
if (typeof requestId !== "string" || !UUID.test(requestId)) {
|
|
1112
|
+
throw new ValidationError("requestId must be a UUID (8-4-4-4-12 hexadecimal digits).");
|
|
1113
|
+
}
|
|
1114
|
+
return requestId.toLowerCase();
|
|
1115
|
+
}
|
|
1116
|
+
function validateBoolean(value, name) {
|
|
1117
|
+
if (typeof value !== "boolean") throw new ValidationError(`${name} must be a boolean.`);
|
|
1118
|
+
return value;
|
|
1119
|
+
}
|
|
1120
|
+
function validatePageSize(value, name) {
|
|
1121
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 100) {
|
|
1122
|
+
throw new ValidationError(`${name} must be an integer from 1 to 100.`);
|
|
1123
|
+
}
|
|
1124
|
+
return value;
|
|
1125
|
+
}
|
|
1126
|
+
function validateNonNegativeInteger(value, name) {
|
|
1127
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
1128
|
+
throw new ValidationError(`${name} must be an integer greater than or equal to 0.`);
|
|
1129
|
+
}
|
|
1130
|
+
return value;
|
|
1131
|
+
}
|
|
1132
|
+
function validateMilliseconds(value, name, allowZero = false) {
|
|
1133
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || !allowZero && value === 0) {
|
|
1134
|
+
const bound = allowZero ? "greater than or equal to 0" : "greater than 0";
|
|
1135
|
+
throw new ValidationError(`${name} must be a number of milliseconds ${bound}.`);
|
|
1136
|
+
}
|
|
1137
|
+
return value;
|
|
1138
|
+
}
|
|
1139
|
+
var HEADER_SAFE = /^[\x21-\x7E]+$/;
|
|
1140
|
+
function requireHeaderSafe(text, name) {
|
|
1141
|
+
if (!HEADER_SAFE.test(text)) {
|
|
1142
|
+
throw new ValidationError(
|
|
1143
|
+
`${name} contains characters that cannot be sent in an HTTP header (only visible ASCII characters are allowed).`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function isAscii(text) {
|
|
1148
|
+
for (let index = 0; index < text.length; index++) {
|
|
1149
|
+
if (text.charCodeAt(index) > 127) return false;
|
|
1150
|
+
}
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
function validateCredential(value, name) {
|
|
1154
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1155
|
+
throw new ValidationError(`${name} is required and must be a non-empty string.`);
|
|
1156
|
+
}
|
|
1157
|
+
const credential = value.trim();
|
|
1158
|
+
requireHeaderSafe(credential, name);
|
|
1159
|
+
return credential;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/history.ts
|
|
1163
|
+
var API_KEY_FORMAT = /^sec_[a-z0-9]{8}-[a-z0-9]{8}-[a-z0-9]{8}$/;
|
|
1164
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1165
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
1166
|
+
var DEFAULT_LIMIT = 20;
|
|
1167
|
+
var DEFAULT_PAGE_SIZE = 100;
|
|
1168
|
+
var DEFAULT_WAIT_TIMEOUT_MS = 1e4;
|
|
1169
|
+
var DEFAULT_POLL_INTERVAL_MS = 250;
|
|
1170
|
+
var POLL_WAIT_CAP_MS = 2e3;
|
|
1171
|
+
var POLL_WAIT_FACTORS = [1, 2, 4, 6];
|
|
1172
|
+
var LAST_POLL_WAIT_FACTOR = 8;
|
|
1173
|
+
var MIN_POLL_ATTEMPT_MS = 1e3;
|
|
1174
|
+
function pollWait(index, pollInterval) {
|
|
1175
|
+
const factor = POLL_WAIT_FACTORS[index] ?? LAST_POLL_WAIT_FACTOR;
|
|
1176
|
+
return Math.min(Math.max(POLL_WAIT_CAP_MS, pollInterval), pollInterval * factor);
|
|
1177
|
+
}
|
|
1178
|
+
function parsePage(body, status, headers) {
|
|
1179
|
+
if (!isPlainObject(body) || !Array.isArray(body.data)) {
|
|
1180
|
+
throw new ApiError("History API returned an unexpected response body.", {
|
|
1181
|
+
status,
|
|
1182
|
+
body,
|
|
1183
|
+
headers
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
const data = body.data.filter(isPlainObject).map(fromHistoryRow);
|
|
1187
|
+
const total = typeof body.total === "number" && Number.isFinite(body.total) ? body.total : data.length;
|
|
1188
|
+
return { data, total };
|
|
1189
|
+
}
|
|
1190
|
+
async function fetchPage(transport, lookup, limit, offset, signal, maxRetries, attemptTimeout) {
|
|
1191
|
+
const path = `/api/v1/history/${lookup.type}/${lookup.segment}`;
|
|
1192
|
+
const response = await transport.getJson(path, {
|
|
1193
|
+
query: { limit: String(limit), offset: String(offset) },
|
|
1194
|
+
signal,
|
|
1195
|
+
maxRetries,
|
|
1196
|
+
timeout: attemptTimeout
|
|
1197
|
+
});
|
|
1198
|
+
return parsePage(response.body, response.status, response.headers);
|
|
1199
|
+
}
|
|
1200
|
+
function isTransientPollError(error) {
|
|
1201
|
+
return error instanceof RateLimitError || error instanceof ServerError || error instanceof ConnectionError || error instanceof TimeoutError;
|
|
1202
|
+
}
|
|
1203
|
+
var HistoryResource = class {
|
|
1204
|
+
#transport;
|
|
1205
|
+
constructor(transport) {
|
|
1206
|
+
this.#transport = transport;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* One page of identifications for one identifier, newest first.
|
|
1210
|
+
* `GET /api/v1/history/{type}/{value}`.
|
|
1211
|
+
*/
|
|
1212
|
+
async search(type, value, options = {}) {
|
|
1213
|
+
const lookup = validateLookup(type, value);
|
|
1214
|
+
const limit = validatePageSize(options.limit ?? DEFAULT_LIMIT, "limit");
|
|
1215
|
+
const offset = validateNonNegativeInteger(options.offset ?? 0, "offset");
|
|
1216
|
+
return fetchPage(this.#transport, lookup, limit, offset, options.signal);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Every identification for one identifier, newest first, fetched page by page. Rows repeated
|
|
1220
|
+
* across pages (new rows can shift offsets) are skipped by request ID. Stops at `total`, at an
|
|
1221
|
+
* empty page or after `maxItems`.
|
|
1222
|
+
*/
|
|
1223
|
+
iterate(type, value, options = {}) {
|
|
1224
|
+
const lookup = validateLookup(type, value);
|
|
1225
|
+
const pageSize = validatePageSize(options.pageSize ?? DEFAULT_PAGE_SIZE, "pageSize");
|
|
1226
|
+
const maxItems = options.maxItems === void 0 ? void 0 : validateNonNegativeInteger(options.maxItems, "maxItems");
|
|
1227
|
+
return this.#iterate(lookup, pageSize, maxItems, options.signal);
|
|
1228
|
+
}
|
|
1229
|
+
async *#iterate(lookup, pageSize, maxItems, signal) {
|
|
1230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1231
|
+
let offset = 0;
|
|
1232
|
+
let yielded = 0;
|
|
1233
|
+
for (; ; ) {
|
|
1234
|
+
if (maxItems !== void 0 && yielded >= maxItems) return;
|
|
1235
|
+
const page = await fetchPage(this.#transport, lookup, pageSize, offset, signal);
|
|
1236
|
+
if (page.data.length === 0) return;
|
|
1237
|
+
for (const identification of page.data) {
|
|
1238
|
+
const key = identification.request_id;
|
|
1239
|
+
if (key !== "") {
|
|
1240
|
+
if (seen.has(key)) continue;
|
|
1241
|
+
seen.add(key);
|
|
1242
|
+
}
|
|
1243
|
+
yield identification;
|
|
1244
|
+
yielded++;
|
|
1245
|
+
if (maxItems !== void 0 && yielded >= maxItems) return;
|
|
1246
|
+
}
|
|
1247
|
+
offset += page.data.length;
|
|
1248
|
+
if (offset >= page.total) return;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
var IdentificationsResource = class {
|
|
1253
|
+
#transport;
|
|
1254
|
+
constructor(transport) {
|
|
1255
|
+
this.#transport = transport;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* The identification with this request ID, or null when none is found.
|
|
1259
|
+
*
|
|
1260
|
+
* Scoring is asynchronous: the row appears about 1 to 3 seconds after the browser call and can
|
|
1261
|
+
* be refined for up to about 10 seconds while follow-up checks finish; this returns the first
|
|
1262
|
+
* version it sees. With `wait` (the default), `timeout` is the total budget of the call:
|
|
1263
|
+
*
|
|
1264
|
+
* - The first poll runs at once. The waits between polls are `pollInterval` times 1, 2, 4 and 6,
|
|
1265
|
+
* then 8 times it for every later wait, each capped at max(2 s, `pollInterval`): 250 ms,
|
|
1266
|
+
* 500 ms, 1 s, 1.5 s and then every 2 s by default, and every 3 s for a `pollInterval` of
|
|
1267
|
+
* 3 s. A wait that would pass the deadline is cut short, so the last poll runs at the
|
|
1268
|
+
* deadline.
|
|
1269
|
+
* - Each poll is one HTTP attempt without retries, with the timeout
|
|
1270
|
+
* min(client timeout, max(time left, 1 s)).
|
|
1271
|
+
* - 429, 5xx, connection errors and timeouts do not end the wait. After a 429 the next wait is
|
|
1272
|
+
* the longest of the scheduled wait, 1 s and the Retry-After capped at 10 s (a missing
|
|
1273
|
+
* Retry-After, 0 or a past date counts as 0), cut short at the deadline like any other wait.
|
|
1274
|
+
* When the capped Retry-After is longer than the time left, that RateLimitError is thrown at
|
|
1275
|
+
* once.
|
|
1276
|
+
* - At the deadline, the error of the last poll is thrown if it failed; otherwise the result
|
|
1277
|
+
* is null.
|
|
1278
|
+
* - 400, 401, 403 and 404 (and any other error that another poll cannot fix) are thrown at once.
|
|
1279
|
+
*/
|
|
1280
|
+
async get(requestId, options = {}) {
|
|
1281
|
+
const id = validateRequestId(requestId);
|
|
1282
|
+
const wait = validateBoolean(options.wait ?? true, "wait");
|
|
1283
|
+
const timeout = validateMilliseconds(
|
|
1284
|
+
options.timeout ?? DEFAULT_WAIT_TIMEOUT_MS,
|
|
1285
|
+
"timeout",
|
|
1286
|
+
true
|
|
1287
|
+
);
|
|
1288
|
+
const pollInterval = validateMilliseconds(
|
|
1289
|
+
options.pollInterval ?? DEFAULT_POLL_INTERVAL_MS,
|
|
1290
|
+
"pollInterval"
|
|
1291
|
+
);
|
|
1292
|
+
const { signal } = options;
|
|
1293
|
+
throwIfAborted(signal);
|
|
1294
|
+
const lookup = { type: "request_id", value: id, segment: id };
|
|
1295
|
+
if (!wait) {
|
|
1296
|
+
const page = await fetchPage(this.#transport, lookup, 1, 0, signal);
|
|
1297
|
+
return page.data[0] ?? null;
|
|
1298
|
+
}
|
|
1299
|
+
return this.#poll(lookup, Date.now() + timeout, pollInterval, signal);
|
|
1300
|
+
}
|
|
1301
|
+
async #poll(lookup, deadline, pollInterval, signal) {
|
|
1302
|
+
let atDeadline = false;
|
|
1303
|
+
for (let index = 0; ; index++) {
|
|
1304
|
+
const attemptTimeout = Math.min(
|
|
1305
|
+
this.#transport.timeout,
|
|
1306
|
+
Math.max(deadline - Date.now(), MIN_POLL_ATTEMPT_MS)
|
|
1307
|
+
);
|
|
1308
|
+
let failure;
|
|
1309
|
+
try {
|
|
1310
|
+
const page = await fetchPage(this.#transport, lookup, 1, 0, signal, 0, attemptTimeout);
|
|
1311
|
+
const found = page.data[0];
|
|
1312
|
+
if (found) return found;
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
throwIfAborted(signal);
|
|
1315
|
+
if (!isTransientPollError(error)) throw error;
|
|
1316
|
+
failure = error;
|
|
1317
|
+
}
|
|
1318
|
+
const remaining = deadline - Date.now();
|
|
1319
|
+
if (atDeadline || remaining <= 0) {
|
|
1320
|
+
if (failure !== void 0) throw failure;
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
let delay = pollWait(index, pollInterval);
|
|
1324
|
+
if (failure instanceof RateLimitError) {
|
|
1325
|
+
const retryAfter = Math.min((failure.retryAfter ?? 0) * 1e3, RETRY_AFTER_CAP_MS);
|
|
1326
|
+
if (retryAfter > remaining) throw failure;
|
|
1327
|
+
delay = Math.max(delay, RATE_LIMIT_FLOOR_MS, retryAfter);
|
|
1328
|
+
}
|
|
1329
|
+
if (delay >= remaining) {
|
|
1330
|
+
delay = remaining;
|
|
1331
|
+
atDeadline = true;
|
|
1332
|
+
}
|
|
1333
|
+
await sleep(delay, signal);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
var ShieldLabs = class {
|
|
1338
|
+
/** Single identifications by request ID, with wait-for-verdict polling. */
|
|
1339
|
+
identifications;
|
|
1340
|
+
/** History lookups by identifier. */
|
|
1341
|
+
history;
|
|
1342
|
+
constructor(options) {
|
|
1343
|
+
if (!isPlainObject(options)) {
|
|
1344
|
+
throw new ValidationError("new ShieldLabs() needs an options object with apiKey.");
|
|
1345
|
+
}
|
|
1346
|
+
const apiKey = validateCredential(options.apiKey, "apiKey");
|
|
1347
|
+
warnIfBrowserPage("ShieldLabs", "Private API Key");
|
|
1348
|
+
if (!API_KEY_FORMAT.test(apiKey)) {
|
|
1349
|
+
warnOnce(
|
|
1350
|
+
"api-key-format",
|
|
1351
|
+
"apiKey does not look like a Private API Key (sec_xxxxxxxx-xxxxxxxx-xxxxxxxx). The History API needs the Private API Key, not the Public Key or the Secret Key."
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
const baseUrl = normalizeBaseUrl(
|
|
1355
|
+
options.baseUrl ?? DEFAULT_HISTORY_BASE_URL,
|
|
1356
|
+
"baseUrl",
|
|
1357
|
+
true,
|
|
1358
|
+
validateBoolean(options.allowInsecureHttp ?? false, "allowInsecureHttp")
|
|
1359
|
+
);
|
|
1360
|
+
const timeout = validateMilliseconds(options.timeout ?? DEFAULT_TIMEOUT_MS, "timeout");
|
|
1361
|
+
const maxRetries = validateNonNegativeInteger(
|
|
1362
|
+
options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
1363
|
+
"maxRetries"
|
|
1364
|
+
);
|
|
1365
|
+
if (options.fetch !== void 0 && typeof options.fetch !== "function") {
|
|
1366
|
+
throw new ValidationError("fetch must be a function.");
|
|
1367
|
+
}
|
|
1368
|
+
const transport = new Transport({
|
|
1369
|
+
apiName: "History API",
|
|
1370
|
+
baseUrl,
|
|
1371
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1372
|
+
secrets: [apiKey],
|
|
1373
|
+
timeout,
|
|
1374
|
+
maxRetries,
|
|
1375
|
+
retryRateLimited: true,
|
|
1376
|
+
fetch: options.fetch
|
|
1377
|
+
});
|
|
1378
|
+
this.history = new HistoryResource(transport);
|
|
1379
|
+
this.identifications = new IdentificationsResource(transport);
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
// src/management.ts
|
|
1384
|
+
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
1385
|
+
var DEFAULT_MAX_RETRIES2 = 2;
|
|
1386
|
+
function normalizeDomain(input) {
|
|
1387
|
+
let domain = input.trim().toLowerCase();
|
|
1388
|
+
domain = domain.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
1389
|
+
domain = domain.split(/[/?#]/, 1)[0] ?? "";
|
|
1390
|
+
return domain.replace(/^www\./, "");
|
|
1391
|
+
}
|
|
1392
|
+
var ShieldLabsManagement = class {
|
|
1393
|
+
/** The normalized domain sent in X-Shield-Domain. */
|
|
1394
|
+
domain;
|
|
1395
|
+
#transport;
|
|
1396
|
+
constructor(options) {
|
|
1397
|
+
if (!isPlainObject(options)) {
|
|
1398
|
+
throw new ValidationError(
|
|
1399
|
+
"new ShieldLabsManagement() needs an options object with secretKey and domain."
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
const secretKey = validateCredential(options.secretKey, "secretKey");
|
|
1403
|
+
warnIfBrowserPage("ShieldLabsManagement", "Secret Key");
|
|
1404
|
+
if (typeof options.domain !== "string") {
|
|
1405
|
+
throw new ValidationError('domain is required and must be a string such as "example.com".');
|
|
1406
|
+
}
|
|
1407
|
+
const domain = normalizeDomain(options.domain);
|
|
1408
|
+
if (domain === "") {
|
|
1409
|
+
throw new ValidationError('domain is required and must be a string such as "example.com".');
|
|
1410
|
+
}
|
|
1411
|
+
if (!isAscii(domain)) {
|
|
1412
|
+
throw new ValidationError("domain must be ASCII: pass the punycode form (xn--...).");
|
|
1413
|
+
}
|
|
1414
|
+
requireHeaderSafe(domain, "domain");
|
|
1415
|
+
const baseUrl = normalizeBaseUrl(
|
|
1416
|
+
options.baseUrl ?? DEFAULT_MANAGEMENT_BASE_URL,
|
|
1417
|
+
"baseUrl",
|
|
1418
|
+
false,
|
|
1419
|
+
validateBoolean(options.allowInsecureHttp ?? false, "allowInsecureHttp")
|
|
1420
|
+
);
|
|
1421
|
+
const timeout = validateMilliseconds(options.timeout ?? DEFAULT_TIMEOUT_MS2, "timeout");
|
|
1422
|
+
const maxRetries = validateNonNegativeInteger(
|
|
1423
|
+
options.maxRetries ?? DEFAULT_MAX_RETRIES2,
|
|
1424
|
+
"maxRetries"
|
|
1425
|
+
);
|
|
1426
|
+
if (options.fetch !== void 0 && typeof options.fetch !== "function") {
|
|
1427
|
+
throw new ValidationError("fetch must be a function.");
|
|
1428
|
+
}
|
|
1429
|
+
this.domain = domain;
|
|
1430
|
+
this.#transport = new Transport({
|
|
1431
|
+
apiName: "Management API",
|
|
1432
|
+
baseUrl,
|
|
1433
|
+
headers: { "X-Shield-Domain": domain, Authorization: `Bearer ${secretKey}` },
|
|
1434
|
+
secrets: [secretKey],
|
|
1435
|
+
timeout,
|
|
1436
|
+
maxRetries,
|
|
1437
|
+
retryRateLimited: false,
|
|
1438
|
+
fetch: options.fetch
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
/** The domain profile: remaining identifications and masked keys. `GET /v1/profile`. */
|
|
1442
|
+
async getProfile(options = {}) {
|
|
1443
|
+
const response = await this.#transport.getJson("/v1/profile", { signal: options.signal });
|
|
1444
|
+
if (!isPlainObject(response.body)) {
|
|
1445
|
+
throw new ApiError("Management API returned an unexpected response body.", {
|
|
1446
|
+
status: response.status,
|
|
1447
|
+
body: response.body,
|
|
1448
|
+
headers: response.headers
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
return profileFromResponse(response.body);
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
// src/risk.ts
|
|
1456
|
+
function riskBand(score) {
|
|
1457
|
+
if (typeof score !== "number" || Number.isNaN(score)) {
|
|
1458
|
+
throw new ValidationError("riskBand() needs a numeric Risk Score.");
|
|
1459
|
+
}
|
|
1460
|
+
if (score > 100) return "rate_limited";
|
|
1461
|
+
if (score >= 60) return "dangerous";
|
|
1462
|
+
if (score >= 30) return "suspicious";
|
|
1463
|
+
return "trusted";
|
|
1464
|
+
}
|
|
1465
|
+
function isRateLimited(score) {
|
|
1466
|
+
return typeof score === "number" && score > 100;
|
|
1467
|
+
}
|
|
1468
|
+
var DEFAULT_MAX_AGE_MS = 3e5;
|
|
1469
|
+
var DEFAULT_BLOCK_BANDS = ["dangerous"];
|
|
1470
|
+
var DEFAULT_BLOCK_FLAGS = ["browser_automation", "javascript_disabled"];
|
|
1471
|
+
function resolveNow(now) {
|
|
1472
|
+
if (now === void 0) return Date.now();
|
|
1473
|
+
const value = now instanceof Date ? now.getTime() : now;
|
|
1474
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1475
|
+
throw new ValidationError("now must be a valid Date or a number of epoch milliseconds.");
|
|
1476
|
+
}
|
|
1477
|
+
return value;
|
|
1478
|
+
}
|
|
1479
|
+
function isThenable(value) {
|
|
1480
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
1481
|
+
}
|
|
1482
|
+
function evaluateIdentification(identification, options = {}) {
|
|
1483
|
+
const maxAge = options.maxAge ?? DEFAULT_MAX_AGE_MS;
|
|
1484
|
+
if (typeof maxAge !== "number" || Number.isNaN(maxAge) || maxAge < 0) {
|
|
1485
|
+
throw new ValidationError(
|
|
1486
|
+
"maxAge must be a number of milliseconds greater than or equal to 0, or Infinity to skip the freshness check."
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
const nowMs = resolveNow(options.now);
|
|
1490
|
+
const blockBands = options.blockBands ?? DEFAULT_BLOCK_BANDS;
|
|
1491
|
+
for (const band2 of blockBands) {
|
|
1492
|
+
if (!RISK_BAND_NAMES.includes(band2)) {
|
|
1493
|
+
throw new ValidationError(
|
|
1494
|
+
`Unknown risk band in blockBands. Use one of: ${RISK_BAND_NAMES.join(", ")}.`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const blockFlags = options.blockFlags ?? DEFAULT_BLOCK_FLAGS;
|
|
1499
|
+
for (const flag of blockFlags) {
|
|
1500
|
+
if (!FLAG_KEYS.includes(flag)) {
|
|
1501
|
+
throw new ValidationError(
|
|
1502
|
+
`Unknown detection flag in blockFlags. Use one of: ${FLAG_KEYS.join(", ")}.`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (identification === null || identification === void 0) {
|
|
1507
|
+
return { ok: false, reason: "missing", band: null };
|
|
1508
|
+
}
|
|
1509
|
+
const band = riskBand(identification.risk_score);
|
|
1510
|
+
if (options.isReplay) {
|
|
1511
|
+
const replayed = options.isReplay(identification.request_id);
|
|
1512
|
+
if (isThenable(replayed)) {
|
|
1513
|
+
throw new ValidationError(
|
|
1514
|
+
"isReplay must return a boolean synchronously. Claim the request ID in your store first (an atomic insert-if-absent), then pass the result, for example isReplay: () => !claimed."
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
if (replayed) return { ok: false, reason: "replayed", band };
|
|
1518
|
+
}
|
|
1519
|
+
if (maxAge !== Number.POSITIVE_INFINITY) {
|
|
1520
|
+
const observedMs = identification.observed_at === null ? Number.NaN : Date.parse(identification.observed_at);
|
|
1521
|
+
if (Number.isNaN(observedMs) || nowMs - observedMs > maxAge) {
|
|
1522
|
+
return { ok: false, reason: "stale", band };
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (isRateLimited(identification.risk_score)) {
|
|
1526
|
+
return { ok: false, reason: "rate_limited", band };
|
|
1527
|
+
}
|
|
1528
|
+
if (!identification.device_id || identification.device_id === NIL_UUID) {
|
|
1529
|
+
return { ok: false, reason: "no_device_signals", band };
|
|
1530
|
+
}
|
|
1531
|
+
for (const flag of blockFlags) {
|
|
1532
|
+
if (identification.detection_flags[flag]) {
|
|
1533
|
+
return { ok: false, reason: "blocked_flag", band, flag };
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (band !== "rate_limited" && blockBands.includes(band)) {
|
|
1537
|
+
return { ok: false, reason: "blocked_band", band };
|
|
1538
|
+
}
|
|
1539
|
+
return { ok: true, reason: null, band };
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/index.ts
|
|
1543
|
+
var hmacSync = (key, message) => createHmac("sha256", key).update(message).digest();
|
|
1544
|
+
var backend = {
|
|
1545
|
+
hmacSync,
|
|
1546
|
+
hmacAsync: (key, message) => Promise.resolve().then(() => hmacSync(key, message)),
|
|
1547
|
+
equal: (a, b) => a.length === b.length && timingSafeEqual(a, b)
|
|
1548
|
+
};
|
|
1549
|
+
var webhooks = createWebhooks(backend);
|
|
1550
|
+
function userHid(userId, secret) {
|
|
1551
|
+
return computeUserHid(backend, userId, secret);
|
|
1552
|
+
}
|
|
1553
|
+
function userHidAsync(userId, secret) {
|
|
1554
|
+
return computeUserHidAsync(backend, userId, secret);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
export { ApiError, AuthenticationError, BadRequestError, ConnectionError, NIL_UUID, NotFoundError, QuotaExceededError, RISK_BANDS, RateLimitError, SIGNALS, ServerError, ShieldLabs, ShieldLabsError, ShieldLabsManagement, SignatureVerificationError, TimeoutError, VERSION, ValidationError, WebhookParseError, evaluateIdentification, isRateLimited, riskBand, userHid, userHidAsync, webhooks };
|
|
1558
|
+
//# sourceMappingURL=index.js.map
|
|
1559
|
+
//# sourceMappingURL=index.js.map
|