diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d01630..b36b966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ follows [Semantic Versioning](https://semver.org/). ## [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/README.md b/README.md index ac3c58b..f2f5745 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ The result is HMAC-SHA256 as 64 lowercase hex characters: stable for a user and - Events: `IdentificationScoredEvent` (`$event->data` is an `Identification`), `WebhookPingEvent` (sent when you verify an endpoint) and `UnknownWebhookEvent` for event types newer than this SDK (acknowledge them with a 2xx). `SignatureVerificationException` means the delivery is not authentic; `WebhookParseException` means a correctly signed body is not a valid event. - Today each identification is delivered once per endpoint: one attempt with a 1-second timeout and no retries. Respond with a 2xx quickly and do slow work afterwards. Future retries will resend identical bytes, so make handlers idempotent on `data.request_id` (`$event->data->request_id`) now. - For guaranteed reads, use the History API: a missed delivery is not sent again, and a History row can be refined after its webhook was sent. -- The "Test" button in the analytics dashboard sends a sample with 17 of the 19 flags and second-precision timestamps. It parses normally; missing flags are false. +- The "Test" button in the analytics dashboard sends a sample event. It parses normally; missing flags are false. - `Webhook::verifySignature()` returns a boolean without parsing, for custom flows. ### Domain profile (Management API) diff --git a/src/Event/WebhookEvent.php b/src/Event/WebhookEvent.php index 23c9bf6..b5ce132 100644 --- a/src/Event/WebhookEvent.php +++ b/src/Event/WebhookEvent.php @@ -13,7 +13,7 @@ abstract class WebhookEvent { /** * @param string $event_type for example "identification.scored" - * @param string $schema_version payload schema version, "2026-06-01" today (other values are accepted) + * @param string $schema_version payload schema version, "2026-10-13" today (other values are accepted) * @param \DateTimeImmutable|null $created_at when the event was created (UTC) * @param array $raw the decoded body as received */ diff --git a/src/Internal/Normalizer.php b/src/Internal/Normalizer.php index 1805fd2..b2ca7be 100644 --- a/src/Internal/Normalizer.php +++ b/src/Internal/Normalizer.php @@ -16,12 +16,12 @@ */ final class Normalizer { - /** The 19 detection flags, in wire order. */ + /** The 20 public detection flags, in wire order. */ public const FLAG_KEYS = [ - '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', + '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', ]; public const TRAFFIC_KEYS = [ @@ -31,23 +31,23 @@ final class Normalizer /** Normalized flag name to History row column. */ private const HISTORY_FLAG_MAP = [ - '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', ]; private const EXACT_SLUGS = [ @@ -62,7 +62,7 @@ final class Normalizer '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', @@ -71,7 +71,7 @@ final class Normalizer private const PREFIX_SLUGS = [ ['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'], @@ -227,8 +227,13 @@ public static function fromHistoryRow(array $row): array $searchBot = self::truthy($row['is_search_bot'] ?? null); $flags = []; foreach (self::FLAG_KEYS as $key) { - if ($key === 'browser_vpn_proxy') { - $flags[$key] = ($row['connection_type'] ?? null) === 'browser_vpn_proxy'; + if ($key === 'vpn') { + $flags[$key] = self::truthy($row['is_vpn'] ?? null) + || self::truthy($row['is_browser_vpn_proxy'] ?? null) + || ($row['connection_type'] ?? null) === 'browser_vpn_proxy'; + } elseif ($key === 'os_mismatch') { + $flags[$key] = self::truthy($row['is_os_mismatch'] ?? null) + || self::truthy($row['is_os_mismatch2'] ?? null); } elseif ($key === 'ip_mismatch') { $flags[$key] = !$searchBot && ($ipLeakDetail || ($publicIp !== '' && $localIp !== '' && $publicIp !== $localIp)); diff --git a/src/Model/DetectionFlags.php b/src/Model/DetectionFlags.php index b95ca20..52601a8 100644 --- a/src/Model/DetectionFlags.php +++ b/src/Model/DetectionFlags.php @@ -7,38 +7,39 @@ use ShieldLabs\Exception\ValidationException; /** - * The 19 stable detection flags. A flag missing from the payload is false. + * The 20 public detection flags. A flag missing from the payload is false. */ final class DetectionFlags implements \JsonSerializable { /** Flag names in wire order. */ public const KEYS = [ - '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', + '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', ]; public function __construct( public readonly bool $vpn = false, - public readonly bool $privacy_relay = false, - public readonly bool $browser_vpn_proxy = false, - public readonly bool $tor = false, public readonly bool $proxy = false, - public readonly bool $datacenter_ip = false, - public readonly bool $abuser = false, - public readonly bool $os_mismatch = false, + public readonly bool $privacy_relay = false, + public readonly bool $check_incomplete = false, + public readonly bool $stun_not_checked = false, public readonly bool $os_not_detected = false, public readonly bool $timezone_mismatch = false, - public readonly bool $anti_detect_browser = false, - public readonly bool $browser_automation = false, + public readonly bool $javascript_disabled = false, + public readonly bool $os_mismatch = false, public readonly bool $ip_mismatch = false, + public readonly bool $ws_tcp_latency = false, + public readonly bool $anti_detect_browser = false, + public readonly bool $device_spoofing = false, + public readonly bool $rate_limited = false, public readonly bool $incognito = false, public readonly bool $search_bot = false, - public readonly bool $suspicious_paid_click = false, - public readonly bool $javascript_disabled = false, - public readonly bool $stun_not_checked = false, - public readonly bool $check_incomplete = false, + public readonly bool $ai_bot = false, + public readonly bool $browser_automation = false, + public readonly bool $ai_browser = false, + public readonly bool $tor = false, ) {} /** @@ -50,24 +51,25 @@ public static function fromArray(array $flags): self return new self( vpn: $value('vpn'), - privacy_relay: $value('privacy_relay'), - browser_vpn_proxy: $value('browser_vpn_proxy'), - tor: $value('tor'), proxy: $value('proxy'), - datacenter_ip: $value('datacenter_ip'), - abuser: $value('abuser'), - os_mismatch: $value('os_mismatch'), + privacy_relay: $value('privacy_relay'), + check_incomplete: $value('check_incomplete'), + stun_not_checked: $value('stun_not_checked'), os_not_detected: $value('os_not_detected'), timezone_mismatch: $value('timezone_mismatch'), - anti_detect_browser: $value('anti_detect_browser'), - browser_automation: $value('browser_automation'), + javascript_disabled: $value('javascript_disabled'), + os_mismatch: $value('os_mismatch'), ip_mismatch: $value('ip_mismatch'), + ws_tcp_latency: $value('ws_tcp_latency'), + anti_detect_browser: $value('anti_detect_browser'), + device_spoofing: $value('device_spoofing'), + rate_limited: $value('rate_limited'), incognito: $value('incognito'), search_bot: $value('search_bot'), - suspicious_paid_click: $value('suspicious_paid_click'), - javascript_disabled: $value('javascript_disabled'), - stun_not_checked: $value('stun_not_checked'), - check_incomplete: $value('check_incomplete'), + ai_bot: $value('ai_bot'), + browser_automation: $value('browser_automation'), + ai_browser: $value('ai_browser'), + tor: $value('tor'), ); } @@ -103,24 +105,25 @@ public function toArray(): array { return [ 'vpn' => $this->vpn, - 'privacy_relay' => $this->privacy_relay, - 'browser_vpn_proxy' => $this->browser_vpn_proxy, - 'tor' => $this->tor, 'proxy' => $this->proxy, - 'datacenter_ip' => $this->datacenter_ip, - 'abuser' => $this->abuser, - 'os_mismatch' => $this->os_mismatch, + 'privacy_relay' => $this->privacy_relay, + 'check_incomplete' => $this->check_incomplete, + 'stun_not_checked' => $this->stun_not_checked, 'os_not_detected' => $this->os_not_detected, 'timezone_mismatch' => $this->timezone_mismatch, - 'anti_detect_browser' => $this->anti_detect_browser, - 'browser_automation' => $this->browser_automation, + 'javascript_disabled' => $this->javascript_disabled, + 'os_mismatch' => $this->os_mismatch, 'ip_mismatch' => $this->ip_mismatch, + 'ws_tcp_latency' => $this->ws_tcp_latency, + 'anti_detect_browser' => $this->anti_detect_browser, + 'device_spoofing' => $this->device_spoofing, + 'rate_limited' => $this->rate_limited, 'incognito' => $this->incognito, 'search_bot' => $this->search_bot, - 'suspicious_paid_click' => $this->suspicious_paid_click, - 'javascript_disabled' => $this->javascript_disabled, - 'stun_not_checked' => $this->stun_not_checked, - 'check_incomplete' => $this->check_incomplete, + 'ai_bot' => $this->ai_bot, + 'browser_automation' => $this->browser_automation, + 'ai_browser' => $this->ai_browser, + 'tor' => $this->tor, ]; } diff --git a/src/Webhook.php b/src/Webhook.php index 32d031c..5239d0e 100644 --- a/src/Webhook.php +++ b/src/Webhook.php @@ -29,7 +29,7 @@ final class Webhook { public const SIGNATURE_HEADER = 'X-Shield-Signature'; - public const SCHEMA_VERSION = '2026-06-01'; + public const SCHEMA_VERSION = '2026-10-13'; private const SIGNATURE_PREFIX = 'sha256='; diff --git a/tests/Unit/ModelTest.php b/tests/Unit/ModelTest.php index 5beb0f1..5814d2a 100644 --- a/tests/Unit/ModelTest.php +++ b/tests/Unit/ModelTest.php @@ -85,7 +85,7 @@ public function testDetectionFlagsLookup(): void self::assertSame(['vpn', 'check_incomplete'], $flags->active()); self::assertSame(DetectionFlags::KEYS, array_keys($flags->toArray())); self::assertSame($flags->toArray(), $flags->jsonSerialize()); - self::assertCount(19, DetectionFlags::KEYS); + self::assertCount(20, DetectionFlags::KEYS); $this->expectException(ValidationException::class); $flags->get('bot'); diff --git a/tests/Unit/NormalizerEdgeCaseTest.php b/tests/Unit/NormalizerEdgeCaseTest.php index 10c07e6..82b58db 100644 --- a/tests/Unit/NormalizerEdgeCaseTest.php +++ b/tests/Unit/NormalizerEdgeCaseTest.php @@ -114,8 +114,8 @@ public function testLocalIpPrefersTheLeakSource(): void public function testBrowserVpnProxyComesFromTheConnectionType(): void { - self::assertTrue(Identification::fromHistoryRow(['connection_type' => 'browser_vpn_proxy'])->detection_flags->browser_vpn_proxy); - self::assertFalse(Identification::fromHistoryRow(['connection_type' => 'vpn'])->detection_flags->browser_vpn_proxy); + self::assertTrue(Identification::fromHistoryRow(['connection_type' => 'browser_vpn_proxy'])->detection_flags->vpn); + self::assertFalse(Identification::fromHistoryRow(['connection_type' => 'vpn'])->detection_flags->vpn); } public function testSiteDomainWinsOverTheRequestHost(): void @@ -126,9 +126,9 @@ public function testSiteDomainWinsOverTheRequestHost(): void public function testFlagTruthinessFollowsJson(): void { - $identification = Identification::fromHistoryRow(['is_vpn' => 'false', 'is_tor' => 0, 'is_proxy' => '', 'is_abuser' => 1, 'is_datacenter' => null]); + $identification = Identification::fromHistoryRow(['is_vpn' => 'false', 'is_tor' => 0, 'is_proxy' => '', 'is_incognito' => 1, 'is_datacenter' => null]); - self::assertSame(['vpn', 'abuser'], $identification->detection_flags->active()); + self::assertSame(['vpn', 'incognito'], $identification->detection_flags->active()); } public function testToleratesWrongTypes(): void diff --git a/tests/Unit/WebhookEventTest.php b/tests/Unit/WebhookEventTest.php index c188ce7..d71109b 100644 --- a/tests/Unit/WebhookEventTest.php +++ b/tests/Unit/WebhookEventTest.php @@ -49,12 +49,12 @@ public function testParsesTheScoredEventFromTheExactBytes(): void self::assertInstanceOf(IdentificationScoredEvent::class, $event); self::assertSame('identification.scored', $event->event_type); - self::assertSame('2026-06-01', $event->schema_version); + self::assertSame('2026-10-13', $event->schema_version); self::assertSame('2026-09-30T12:34:57.482913+00:00', $event->created_at?->format('Y-m-d\TH:i:s.uP')); self::assertSame(self::expected('webhook_scored'), $event->data->toArray()); self::assertSame('https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123', $event->data->traffic_source->landing_url); self::assertSame(RiskBand::Dangerous, $event->data->riskBand()); - self::assertSame(['proxy', 'datacenter_ip', 'anti_detect_browser', 'ip_mismatch', 'suspicious_paid_click'], $event->data->detection_flags->active()); + self::assertSame(['proxy', 'ip_mismatch', 'anti_detect_browser'], $event->data->detection_flags->active()); self::assertIsArray($event->raw['data']); self::assertSame($event->raw['data'], $event->data->raw); } @@ -113,7 +113,7 @@ public function testParsesTheDashboardTestDeliveryWithSeventeenFlags(): void $body = Fixtures::raw('webhook-test-delivery.json'); $decoded = Fixtures::json('webhook-test-delivery.json'); \assert(\is_array($decoded['data']) && \is_array($decoded['data']['detection_flags'])); - self::assertCount(17, $decoded['data']['detection_flags']); + self::assertCount(20, $decoded['data']['detection_flags']); $event = Webhook::constructEvent($body, self::sign($body), self::SECRET); 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" }