Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to this package are documented in this file. The format foll

## [Unreleased]

### Changed

- `detection_flags` follows webhook schema `2026-10-13`: 20 public booleans. `vpn` covers a network VPN and an in-browser VPN or proxy. `os_mismatch` covers either operating-system mismatch check. The public set adds `ws_tcp_latency`, `device_spoofing`, `rate_limited`, `ai_bot` and `ai_browser`.

## [1.0.1] - 2026-10-01

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ webhook JSON.
| `traffic_source` | object | `channel`, `referrer_domain`, `landing_url`, `click_id_type`, `utm_*` (`""` when absent) |
| `risk_score` | `number` | 0-100, or 999 for the rate-limit marker |
| `signals` | `{ name, weight, description }[]` | Weighted risk signals; `description` is `null` for webhooks |
| `detection_flags` | object of 19 booleans | `vpn`, `privacy_relay`, `browser_vpn_proxy`, `tor`, `proxy`, `datacenter_ip`, `abuser`, `os_mismatch`, `os_not_detected`, `timezone_mismatch`, `anti_detect_browser`, `browser_automation`, `ip_mismatch`, `incognito`, `search_bot`, `suspicious_paid_click`, `javascript_disabled`, `stun_not_checked`, `check_incomplete` |
| `detection_flags` | object of 20 booleans | `vpn`, `proxy`, `privacy_relay`, `check_incomplete`, `stun_not_checked`, `os_not_detected`, `timezone_mismatch`, `javascript_disabled`, `os_mismatch`, `ip_mismatch`, `ws_tcp_latency`, `anti_detect_browser`, `device_spoofing`, `rate_limited`, `incognito`, `search_bot`, `ai_bot`, `browser_automation`, `ai_browser`, `tor` |
| `observed_at` | `string \| null` | RFC 3339 UTC with milliseconds, for example `2026-09-30T12:34:56.123Z` |
| `source` | `'webhook' \| 'history'` | Which payload it was built from |
| `raw` | object | The original payload, including fields the model omits |
Expand Down
30 changes: 15 additions & 15 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const SIGNALS = Object.freeze({
STUN_NOT_CHECKED: 'stun_not_checked',
STUN_LATE_CORRECTION: 'stun_late_correction',
OS_NOT_DETECTED: 'os_not_detected',
BROWSER_VPN_PROXY: 'browser_vpn_proxy',
VPN: 'vpn',
PRIVACY_RELAY: 'privacy_relay',
PROXY: 'proxy',
Expand Down Expand Up @@ -52,31 +51,32 @@ export const LOOKUP_TYPES: readonly LookupType[] = [

export type FlagKey = keyof DetectionFlags;

/** The 19 detection flag keys, in the order the webhook payload lists them. */
/** The 20 detection flag keys, in the order the webhook payload lists them. */
export const FLAG_KEYS: readonly FlagKey[] = [
'vpn',
'privacy_relay',
'browser_vpn_proxy',
'tor',
'proxy',
'datacenter_ip',
'abuser',
'os_mismatch',
'privacy_relay',
'check_incomplete',
'stun_not_checked',
'os_not_detected',
'timezone_mismatch',
'anti_detect_browser',
'browser_automation',
'javascript_disabled',
'os_mismatch',
'ip_mismatch',
'ws_tcp_latency',
'anti_detect_browser',
'device_spoofing',
'rate_limited',
'incognito',
'search_bot',
'suspicious_paid_click',
'javascript_disabled',
'stun_not_checked',
'check_incomplete',
'ai_bot',
'browser_automation',
'ai_browser',
'tor',
];

/** Webhook schema version this SDK was built for. */
export const KNOWN_SCHEMA_VERSION = '2026-06-01';
export const KNOWN_SCHEMA_VERSION = '2026-10-13';

export const DEFAULT_HISTORY_BASE_URL = 'https://account.shieldlabs.ai';
export const DEFAULT_MANAGEMENT_BASE_URL = 'https://api.shieldlabs.ai';
37 changes: 21 additions & 16 deletions src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const EXACT_SLUGS = new Map<string, string>([
['UA OS is not detected', 'os_not_detected'],
['Network OS is not detected', 'os_not_detected'],
['Browser timezone ≠ IP-timezone', 'timezone_mismatch'],
['Browser VPN/Proxy', 'browser_vpn_proxy'],
['Browser VPN/Proxy', 'vpn'],
['Browser Automation', 'browser_automation'],
['Port scan routed via proxy (antidetect browser pattern)', 'proxy_routed_antidetect'],
['User has been banned 1H, to many requests', 'rate_limited'],
Expand All @@ -99,7 +99,7 @@ const EXACT_SLUGS = new Map<string, string>([
const PREFIX_SLUGS: ReadonlyArray<readonly [prefix: string, slug: string]> = [
['Antidetect browser', 'antidetect_browser'],
['Os_mismatch', 'os_mismatch'],
['OS mismatch2', 'os_mismatch2'],
['OS mismatch2', 'os_mismatch'],
['TCP handshake', 'tcp_handshake_v2'],
['Latency test', 'ws_tcp_latency'],
['JavaScript disabled', 'javascript_disabled'],
Expand Down Expand Up @@ -241,26 +241,26 @@ export function parseRfc3339(value: unknown): string | null {
// Identification
// ---------------------------------------------------------------------------------------------

type HistoryFlagKey = Exclude<FlagKey, 'browser_vpn_proxy' | 'ip_mismatch'>;
type HistoryFlagKey = Exclude<FlagKey, 'vpn' | 'os_mismatch' | 'ip_mismatch'>;

const HISTORY_FLAG_COLUMNS: Readonly<Record<HistoryFlagKey, string>> = {
vpn: 'is_vpn',
privacy_relay: 'is_privacy_relay',
tor: 'is_tor',
proxy: 'is_proxy',
datacenter_ip: 'is_datacenter',
abuser: 'is_abuser',
os_mismatch: 'is_os_mismatch',
privacy_relay: 'is_privacy_relay',
check_incomplete: 'check_incomplete',
stun_not_checked: 'is_stun_not_checked',
os_not_detected: 'is_os_not_detected',
timezone_mismatch: 'is_timezone_mismatch',
javascript_disabled: 'is_js_disabled',
ws_tcp_latency: 'is_latency_test',
anti_detect_browser: 'is_antidetect',
browser_automation: 'is_browser_automation',
device_spoofing: 'is_device_spoof',
rate_limited: 'is_rate_limited',
incognito: 'is_incognito',
search_bot: 'is_search_bot',
suspicious_paid_click: 'is_suspicious_paid_click',
javascript_disabled: 'is_js_disabled',
stun_not_checked: 'is_stun_not_checked',
check_incomplete: 'check_incomplete',
ai_bot: 'is_ai_bot',
browser_automation: 'is_browser_automation',
ai_browser: 'is_ai_browser',
tor: 'is_tor',
};

/** IP sentinel handling: trimmed, and "0.0.0.0" means no IP (""). */
Expand Down Expand Up @@ -342,8 +342,13 @@ export function fromHistoryRow(row: JsonObject): Identification {
const searchBot = isTruthy(own(row, 'is_search_bot'));
const flags = {} as DetectionFlags;
for (const key of FLAG_KEYS) {
if (key === 'browser_vpn_proxy') {
flags[key] = own(row, 'connection_type') === 'browser_vpn_proxy';
if (key === 'vpn') {
flags[key] =
isTruthy(own(row, 'is_vpn')) ||
isTruthy(own(row, 'is_browser_vpn_proxy')) ||
own(row, 'connection_type') === 'browser_vpn_proxy';
} else if (key === 'os_mismatch') {
flags[key] = isTruthy(own(row, 'is_os_mismatch')) || isTruthy(own(row, 'is_os_mismatch2'));
} else if (key === 'ip_mismatch') {
flags[key] =
!searchBot && (ipLeakDetail || (publicIp !== '' && localIp !== '' && publicIp !== localIp));
Expand Down
27 changes: 14 additions & 13 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,28 @@ export interface IdentificationSignal {
description: string | null;
}

/** The 19 stable detection flags. A flag the server did not send is false. */
/** The 20 public detection flags. A flag the server did not send is false. */
export interface DetectionFlags {
vpn: boolean;
privacy_relay: boolean;
browser_vpn_proxy: boolean;
tor: boolean;
proxy: boolean;
datacenter_ip: boolean;
abuser: boolean;
os_mismatch: boolean;
privacy_relay: boolean;
check_incomplete: boolean;
stun_not_checked: boolean;
os_not_detected: boolean;
timezone_mismatch: boolean;
anti_detect_browser: boolean;
browser_automation: boolean;
javascript_disabled: boolean;
os_mismatch: boolean;
ip_mismatch: boolean;
ws_tcp_latency: boolean;
anti_detect_browser: boolean;
device_spoofing: boolean;
rate_limited: boolean;
incognito: boolean;
search_bot: boolean;
suspicious_paid_click: boolean;
javascript_disabled: boolean;
stun_not_checked: boolean;
check_incomplete: boolean;
ai_bot: boolean;
browser_automation: boolean;
ai_browser: boolean;
tor: boolean;
}

/**
Expand Down
Loading
Loading