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 project are documented in this file. The format foll

## [Unreleased]

### Changed

- `DetectionFlags` 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`.

### Added

- `sync.sh` downloads the OpenAPI description and `generate.sh` rebuilds `generated/` from it. The supported client is unchanged.
Expand Down
39 changes: 24 additions & 15 deletions src/shieldlabs/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,27 +153,28 @@ def to_dict(self) -> dict[str, Any]:

@dataclass(frozen=True)
class DetectionFlags:
"""The 19 detection flags. Branch on these and on ``risk_score``."""
"""The 20 public detection flags. Branch on these and on ``risk_score``."""

vpn: bool = False
privacy_relay: bool = False
browser_vpn_proxy: bool = False
tor: bool = False
proxy: bool = False
datacenter_ip: bool = False
abuser: bool = False
os_mismatch: bool = False
privacy_relay: bool = False
check_incomplete: bool = False
stun_not_checked: bool = False
os_not_detected: bool = False
timezone_mismatch: bool = False
anti_detect_browser: bool = False
browser_automation: bool = False
javascript_disabled: bool = False
os_mismatch: bool = False
ip_mismatch: bool = False
ws_tcp_latency: bool = False
anti_detect_browser: bool = False
device_spoofing: bool = False
rate_limited: bool = False
incognito: bool = False
search_bot: bool = False
suspicious_paid_click: bool = False
javascript_disabled: bool = False
stun_not_checked: bool = False
check_incomplete: bool = False
ai_bot: bool = False
browser_automation: bool = False
ai_browser: bool = False
tor: bool = False

@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> DetectionFlags:
Expand Down Expand Up @@ -311,8 +312,16 @@ def from_history_row(cls, row: Mapping[str, Any]) -> Identification:
search_bot = bool(row.get("is_search_bot", False))
flags: dict[str, bool] = {}
for key in FLAG_KEYS:
if key == "browser_vpn_proxy":
flags[key] = row.get("connection_type") == "browser_vpn_proxy"
if key == "vpn":
flags[key] = bool(
row.get("is_vpn", False)
or row.get("is_browser_vpn_proxy", False)
or row.get("connection_type") == "browser_vpn_proxy"
)
elif key == "os_mismatch":
flags[key] = bool(
row.get("is_os_mismatch", False) or row.get("is_os_mismatch2", False)
)
elif key == "ip_mismatch":
differs = public_ip != "" and local_ip != "" and public_ip != local_ip
flags[key] = (not search_bot) and (ip_leak_detail or differs)
Expand Down
55 changes: 28 additions & 27 deletions src/shieldlabs/_normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,47 +39,48 @@

FLAG_KEYS: tuple[str, ...] = (
"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",
)
"""The 19 detection flags, in wire order."""
"""The 20 public detection flags, in wire order."""

HISTORY_FLAG_MAP: dict[str, str] = {
"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",
}
"""Detection flag to History row column (``browser_vpn_proxy`` and ``ip_mismatch`` are derived)."""
"""History column for each flag. ``vpn``, ``os_mismatch`` and ``ip_mismatch`` are derived."""

TRAFFIC_KEYS: tuple[str, ...] = (
"channel",
Expand All @@ -105,7 +106,7 @@
"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 @@ -114,7 +115,7 @@
_PREFIX_SLUGS: tuple[tuple[str, str], ...] = (
("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
4 changes: 2 additions & 2 deletions src/shieldlabs/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
SIGNATURE_HEADER = "X-Shield-Signature"
"""Name of the header that carries the signature."""

SCHEMA_VERSION = "2026-06-01"
SCHEMA_VERSION = "2026-10-13"
"""Webhook ``schema_version`` this SDK was built for. Other values are parsed with a warning."""

_SIGNATURE_PREFIX = "sha256="
Expand All @@ -65,7 +65,7 @@ class IdentificationScoredEvent:

Attributes:
event_type: Always ``"identification.scored"``.
schema_version: Envelope schema version, for example ``"2026-06-01"``.
schema_version: Envelope schema version, for example ``"2026-10-13"``.
created_at: When the event was created (aware UTC datetime), ``None`` if unparsable.
data: The normalized identification.
raw: The whole parsed envelope.
Expand Down
Loading
Loading