From 3cfd326c2824a7db642e9e75c4c652e3ff5b4ad3 Mon Sep 17 00:00:00 2001 From: Konstantin Serebryakov <97249355+KonstantinSerebryakov@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:12:31 +0300 Subject: [PATCH] feat: match detection flags to webhook schema 2026-10-13 The public flag set is 20 booleans: vpn includes an in-browser VPN or proxy, and os_mismatch covers either OS-mismatch check. Co-authored-by: Cursor --- CHANGELOG.md | 4 + src/shieldlabs/_models.py | 39 ++- src/shieldlabs/_normalize.py | 55 ++-- src/shieldlabs/webhooks.py | 4 +- tests/data/normalization-cases.json | 281 +++++++++--------- tests/data/signal-slug-cases.json | 4 +- tests/data/webhook-identification-scored.json | 27 +- .../webhook-identification-scored.raw.txt | 2 +- tests/data/webhook-rate-limited.json | 27 +- tests/data/webhook-test-delivery.json | 33 +- tests/test_normalization.py | 10 +- tests/test_package.py | 2 +- tests/test_webhooks.py | 11 +- 13 files changed, 265 insertions(+), 234 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6427d3..1ea66d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/shieldlabs/_models.py b/src/shieldlabs/_models.py index 74172d2..170eee7 100644 --- a/src/shieldlabs/_models.py +++ b/src/shieldlabs/_models.py @@ -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: @@ -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) diff --git a/src/shieldlabs/_normalize.py b/src/shieldlabs/_normalize.py index a77d68d..1036bc9 100644 --- a/src/shieldlabs/_normalize.py +++ b/src/shieldlabs/_normalize.py @@ -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", @@ -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", @@ -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"), diff --git a/src/shieldlabs/webhooks.py b/src/shieldlabs/webhooks.py index cb6d2a8..82399d9 100644 --- a/src/shieldlabs/webhooks.py +++ b/src/shieldlabs/webhooks.py @@ -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=" @@ -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. diff --git a/tests/data/normalization-cases.json b/tests/data/normalization-cases.json index 0ccaff3..1449e9c 100644 --- a/tests/data/normalization-cases.json +++ b/tests/data/normalization-cases.json @@ -115,24 +115,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": true, - "datacenter_ip": true, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": true, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": true, + "ws_tcp_latency": false, + "anti_detect_browser": true, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": true, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T12:34:56.123Z", "source": "history" @@ -233,24 +234,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": false, - "datacenter_ip": false, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": true, - "anti_detect_browser": false, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, "incognito": true, "search_bot": false, - "suspicious_paid_click": false, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T12:40:01.007Z", "source": "history" @@ -371,24 +373,25 @@ ], "detection_flags": { "vpn": true, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": false, - "datacenter_ip": false, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": true, + "stun_not_checked": true, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": false, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": true, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": false, - "javascript_disabled": false, - "stun_not_checked": true, - "check_incomplete": true + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T13:05:12.000Z", "source": "history" @@ -489,24 +492,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": false, - "datacenter_ip": false, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": false, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": false, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T13:10:00.500Z", "source": "history" @@ -605,24 +609,25 @@ "signals": [], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": false, - "datacenter_ip": false, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": false, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": true, - "suspicious_paid_click": false, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T13:20:30.250Z", "source": "history" @@ -679,24 +684,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": true, - "datacenter_ip": true, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": true, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": true, + "ws_tcp_latency": false, + "anti_detect_browser": true, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": true, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T12:34:57.482913041Z" }, @@ -751,24 +757,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": true, - "datacenter_ip": true, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": true, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": true, + "ws_tcp_latency": false, + "anti_detect_browser": true, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": true, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T12:34:57.482Z", "source": "webhook" @@ -817,24 +824,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": false, - "datacenter_ip": false, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": false, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": false, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T13:10:00.5Z" }, @@ -879,24 +887,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": false, - "datacenter_ip": false, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": false, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": false, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T13:10:00.500Z", "source": "webhook" @@ -910,23 +919,26 @@ "connection_type": "proxy", "cookie_id": "2c9d1e8f-4b7a-4c3e-9d2f-1a8b7c6d5e4f", "detection_flags": { - "abuser": true, - "anti_detect_browser": false, - "check_incomplete": false, - "datacenter_ip": true, - "incognito": false, - "ip_mismatch": false, - "javascript_disabled": false, - "os_mismatch": false, - "os_not_detected": false, - "privacy_relay": false, + "vpn": false, "proxy": true, + "privacy_relay": false, + "check_incomplete": false, "stun_not_checked": false, - "suspicious_paid_click": false, + "os_not_detected": false, "timezone_mismatch": false, - "tor": false, - "vpn": false, - "browser_vpn_proxy": false + "javascript_disabled": false, + "os_mismatch": false, + "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, + "incognito": false, + "search_bot": false, + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "device_id": "6f1e2d3c-4b5a-5968-8776-655443322110", "device_type": "desktop", @@ -1023,24 +1035,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": true, - "datacenter_ip": true, - "abuser": true, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": false, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": false, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T12:34:56.000Z", "source": "webhook" diff --git a/tests/data/signal-slug-cases.json b/tests/data/signal-slug-cases.json index 2a73957..bb3cc8a 100644 --- a/tests/data/signal-slug-cases.json +++ b/tests/data/signal-slug-cases.json @@ -47,7 +47,7 @@ }, { "description": "Browser VPN/Proxy", - "slug": "browser_vpn_proxy" + "slug": "vpn" }, { "description": "Browser Automation", @@ -75,7 +75,7 @@ }, { "description": "OS mismatch2 (TCP behaviour: ttl)", - "slug": "os_mismatch2" + "slug": "os_mismatch" }, { "description": "TCP handshake (window)", diff --git a/tests/data/webhook-identification-scored.json b/tests/data/webhook-identification-scored.json index f2de2d2..b42704a 100644 --- a/tests/data/webhook-identification-scored.json +++ b/tests/data/webhook-identification-scored.json @@ -1,6 +1,6 @@ { "event_type": "identification.scored", - "schema_version": "2026-06-01", + "schema_version": "2026-10-13", "created_at": "2026-09-30T12:34:57.482913041Z", "data": { "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", @@ -50,24 +50,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": true, - "datacenter_ip": true, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": true, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": true, + "ws_tcp_latency": false, + "anti_detect_browser": true, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": true, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T12:34:57.482913041Z" } diff --git a/tests/data/webhook-identification-scored.raw.txt b/tests/data/webhook-identification-scored.raw.txt index a4da089..77d7650 100644 --- a/tests/data/webhook-identification-scored.raw.txt +++ b/tests/data/webhook-identification-scored.raw.txt @@ -1 +1 @@ -{"event_type":"identification.scored","schema_version":"2026-06-01","created_at":"2026-09-30T12:34:57.482913041Z","data":{"request_id":"02f1d973-84db-4156-a7f7-e799e6bf389b","visitor_id":"bde0e249-20d8-4544-838c-ed9a0b6d7a36","device_id":"ac7c303d-971b-41d1-8e25-cd5b46b46aed","session_id":"bde78778-efd2-4c49-952f-1f11b9c05f35","cookie_id":"4449bb58-590c-444c-ae1f-d1ddc768dbdd","user_hid":"9f86d081884c7d659a2feaa0c55ad015","domain":"example.com","public_ip":{"ip":"203.0.113.24","country":"Netherlands"},"local_ip":{"ip":"198.51.100.23","country":"Germany"},"connection_type":"proxy","os":"Windows","browser":"Chrome","device_type":"desktop","traffic_source":{"channel":"Google Ads","referrer_domain":"google.com","landing_url":"https://shop.example.com/signup?utm_source=google\u0026utm_medium=cpc\u0026gclid=abc123","click_id_type":"gclid","utm_source":"google","utm_medium":"cpc","utm_campaign":"","utm_content":"","utm_term":""},"risk_score":80,"signals":[{"name":"proxy","weight":10},{"name":"datacenter_ip","weight":10},{"name":"antidetect_browser","weight":60}],"detection_flags":{"vpn":false,"privacy_relay":false,"browser_vpn_proxy":false,"tor":false,"proxy":true,"datacenter_ip":true,"abuser":false,"os_mismatch":false,"os_not_detected":false,"timezone_mismatch":false,"anti_detect_browser":true,"browser_automation":false,"ip_mismatch":true,"incognito":false,"search_bot":false,"suspicious_paid_click":true,"javascript_disabled":false,"stun_not_checked":false,"check_incomplete":false},"observed_at":"2026-09-30T12:34:57.482913041Z"}} \ No newline at end of file +{"event_type":"identification.scored","schema_version":"2026-10-13","created_at":"2026-09-30T12:34:57.482913041Z","data":{"request_id":"02f1d973-84db-4156-a7f7-e799e6bf389b","visitor_id":"bde0e249-20d8-4544-838c-ed9a0b6d7a36","device_id":"ac7c303d-971b-41d1-8e25-cd5b46b46aed","session_id":"bde78778-efd2-4c49-952f-1f11b9c05f35","cookie_id":"4449bb58-590c-444c-ae1f-d1ddc768dbdd","user_hid":"9f86d081884c7d659a2feaa0c55ad015","domain":"example.com","public_ip":{"ip":"203.0.113.24","country":"Netherlands"},"local_ip":{"ip":"198.51.100.23","country":"Germany"},"connection_type":"proxy","os":"Windows","browser":"Chrome","device_type":"desktop","traffic_source":{"channel":"Google Ads","referrer_domain":"google.com","landing_url":"https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123","click_id_type":"gclid","utm_source":"google","utm_medium":"cpc","utm_campaign":"","utm_content":"","utm_term":""},"risk_score":80,"signals":[{"name":"proxy","weight":10},{"name":"datacenter_ip","weight":10},{"name":"antidetect_browser","weight":60}],"detection_flags":{"vpn":false,"proxy":true,"privacy_relay":false,"check_incomplete":false,"stun_not_checked":false,"os_not_detected":false,"timezone_mismatch":false,"javascript_disabled":false,"os_mismatch":false,"ip_mismatch":true,"ws_tcp_latency":false,"anti_detect_browser":true,"device_spoofing":false,"rate_limited":false,"incognito":false,"search_bot":false,"ai_bot":false,"browser_automation":false,"ai_browser":false,"tor":false},"observed_at":"2026-09-30T12:34:57.482913041Z"}} \ No newline at end of file diff --git a/tests/data/webhook-rate-limited.json b/tests/data/webhook-rate-limited.json index b9edf47..c72c8de 100644 --- a/tests/data/webhook-rate-limited.json +++ b/tests/data/webhook-rate-limited.json @@ -1,6 +1,6 @@ { "event_type": "identification.scored", - "schema_version": "2026-06-01", + "schema_version": "2026-10-13", "created_at": "2026-09-30T13:10:00.5Z", "data": { "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", @@ -42,24 +42,25 @@ ], "detection_flags": { "vpn": false, - "privacy_relay": false, - "browser_vpn_proxy": false, - "tor": false, "proxy": false, - "datacenter_ip": false, - "abuser": false, - "os_mismatch": false, + "privacy_relay": false, + "check_incomplete": false, + "stun_not_checked": false, "os_not_detected": false, "timezone_mismatch": false, - "anti_detect_browser": false, - "browser_automation": false, + "javascript_disabled": false, + "os_mismatch": false, "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, "incognito": false, "search_bot": false, - "suspicious_paid_click": false, - "javascript_disabled": false, - "stun_not_checked": false, - "check_incomplete": false + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "observed_at": "2026-09-30T13:10:00.5Z" } diff --git a/tests/data/webhook-test-delivery.json b/tests/data/webhook-test-delivery.json index 8e214ce..8809d3f 100644 --- a/tests/data/webhook-test-delivery.json +++ b/tests/data/webhook-test-delivery.json @@ -5,23 +5,26 @@ "connection_type": "proxy", "cookie_id": "2c9d1e8f-4b7a-4c3e-9d2f-1a8b7c6d5e4f", "detection_flags": { - "abuser": true, - "anti_detect_browser": false, - "browser_vpn_proxy": false, - "check_incomplete": false, - "datacenter_ip": true, - "incognito": false, - "ip_mismatch": false, - "javascript_disabled": false, - "os_mismatch": false, - "os_not_detected": false, - "privacy_relay": false, + "vpn": false, "proxy": true, + "privacy_relay": false, + "check_incomplete": false, "stun_not_checked": false, - "suspicious_paid_click": false, + "os_not_detected": false, "timezone_mismatch": false, - "tor": false, - "vpn": false + "javascript_disabled": false, + "os_mismatch": false, + "ip_mismatch": false, + "ws_tcp_latency": false, + "anti_detect_browser": false, + "device_spoofing": false, + "rate_limited": false, + "incognito": false, + "search_bot": false, + "ai_bot": false, + "browser_automation": false, + "ai_browser": false, + "tor": false }, "device_id": "6f1e2d3c-4b5a-5968-8776-655443322110", "device_type": "desktop", @@ -68,5 +71,5 @@ "visitor_id": "7a6b5c4d-3e2f-5a1b-9c8d-7e6f5a4b3c2d" }, "event_type": "identification.scored", - "schema_version": "2026-06-01" + "schema_version": "2026-10-13" } diff --git a/tests/test_normalization.py b/tests/test_normalization.py index caec542..f4721a7 100644 --- a/tests/test_normalization.py +++ b/tests/test_normalization.py @@ -69,7 +69,7 @@ def test_normalization_cases(case: dict[str, Any]) -> None: assert identification.observed_at.utcoffset() == timedelta(0) assert isinstance(identification.signals, tuple) assert all(isinstance(signal.weight, int) for signal in identification.signals) - assert len(identification.detection_flags.to_dict()) == 19 + assert len(identification.detection_flags.to_dict()) == 20 @pytest.mark.parametrize("case", NORMALIZATION, ids=[c["name"] for c in NORMALIZATION]) @@ -115,10 +115,8 @@ def test_identification_band_properties() -> None: assert scored.has_device_signals assert scored.detection_flags.active() == ( "proxy", - "datacenter_ip", - "anti_detect_browser", "ip_mismatch", - "suspicious_paid_click", + "anti_detect_browser", ) @@ -181,9 +179,9 @@ def test_history_ip_mismatch_rules() -> None: assert Identification.from_history_row(detail).detection_flags.ip_mismatch -def test_history_browser_vpn_proxy_is_derived_from_connection_type() -> None: +def test_history_browser_vpn_sets_the_vpn_flag() -> None: row = {"connection_type": "browser_vpn_proxy"} - assert Identification.from_history_row(row).detection_flags.browser_vpn_proxy + assert Identification.from_history_row(row).detection_flags.vpn assert Identification.from_history_row(row).connection_type == "browser_vpn_proxy" diff --git a/tests/test_package.py b/tests/test_package.py index 114e282..bba711b 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -87,5 +87,5 @@ def test_version_and_typing_marker() -> None: def test_webhooks_module_surface() -> None: assert shieldlabs.webhooks.SIGNATURE_HEADER == "X-Shield-Signature" - assert shieldlabs.webhooks.SCHEMA_VERSION == "2026-06-01" + assert shieldlabs.webhooks.SCHEMA_VERSION == "2026-10-13" assert shieldlabs.webhooks.IdentificationScoredEvent is shieldlabs.IdentificationScoredEvent diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index 1406e85..61b810a 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -129,9 +129,9 @@ def test_rate_limited_event() -> None: assert event.data.to_dict() == NORMALIZATION["webhook_rate_limited"]["expected"] -def test_dashboard_test_delivery_with_17_flags() -> None: +def test_dashboard_test_delivery_with_public_flags() -> None: delivery = load_json("webhook-test-delivery.json") - assert len(delivery["data"]["detection_flags"]) == 17 + assert len(delivery["data"]["detection_flags"]) == 20 body = json.dumps(delivery, separators=(",", ":"), sort_keys=True).encode() event = webhooks.construct_event(body, sign(SECRET, body), SECRET) assert isinstance(event, IdentificationScoredEvent) @@ -139,8 +139,6 @@ def test_dashboard_test_delivery_with_17_flags() -> None: assert flags.browser_automation is False assert flags.search_bot is False assert flags.proxy - assert flags.datacenter_ip - assert flags.abuser assert event.data.user_hid is None assert event.data.to_dict() == NORMALIZATION["webhook_test_delivery"]["expected"] @@ -184,7 +182,10 @@ def test_unknown_schema_version_warns_but_parses() -> None: def test_known_schema_version_does_not_warn() -> None: - body = load_bytes("webhook-ping.raw.txt") + body = ( + b'{"event_type":"webhook.ping","schema_version":"2026-10-13",' + b'"created_at":"2026-09-30T12:00:00Z"}' + ) with warnings.catch_warnings(): warnings.simplefilter("error") webhooks.construct_event(body, sign(SECRET, body), SECRET)