diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ebf93c..bb19b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project are documented in this file. The format foll ### Changed +- Public risk-signal slugs are `network_check_incomplete`, `banned_ip`, `private_relay` and `latency_test`. Webhook flag parsers still accept `stun_not_checked`, `rate_limited`, `privacy_relay` and `ws_tcp_latency`. The score marker band stays `rate_limited`. History columns stay `is_stun_not_checked`, `is_rate_limited`, `is_privacy_relay` and `is_latency_test`. - `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 diff --git a/generated/.openapi-generator/FILES b/generated/.openapi-generator/FILES index 4a175b3..99d2b01 100644 --- a/generated/.openapi-generator/FILES +++ b/generated/.openapi-generator/FILES @@ -8,19 +8,39 @@ shieldlabs_generated/api_response.py shieldlabs_generated/configuration.py shieldlabs_generated/exceptions.py shieldlabs_generated/models/__init__.py +shieldlabs_generated/models/client_identity.py +shieldlabs_generated/models/client_identity_assessments_inner.py +shieldlabs_generated/models/client_identity_claims_inner.py +shieldlabs_generated/models/client_identity_evidence_inner.py +shieldlabs_generated/models/client_identity_verified_inner.py shieldlabs_generated/models/detection_flags.py +shieldlabs_generated/models/detection_flags20261012.py shieldlabs_generated/models/domain_profile.py shieldlabs_generated/models/error_body.py shieldlabs_generated/models/health_status.py shieldlabs_generated/models/history_page.py shieldlabs_generated/models/history_row.py +shieldlabs_generated/models/hre.py +shieldlabs_generated/models/hre_result.py shieldlabs_generated/models/identification_scored_data.py +shieldlabs_generated/models/identification_scored_data_detection_flags.py shieldlabs_generated/models/identification_scored_event.py shieldlabs_generated/models/ip_info.py +shieldlabs_generated/models/legacy_detection_flags.py shieldlabs_generated/models/legacy_snapshot.py +shieldlabs_generated/models/multiaccount_changed_data.py +shieldlabs_generated/models/multiaccount_changed_data_evidence_summary.py +shieldlabs_generated/models/multiaccount_changed_data_source.py +shieldlabs_generated/models/multiaccount_changed_event.py +shieldlabs_generated/models/risk_event.py shieldlabs_generated/models/score_detail.py shieldlabs_generated/models/signal.py shieldlabs_generated/models/traffic_source.py +shieldlabs_generated/models/webhook_client_identity.py +shieldlabs_generated/models/webhook_client_identity_assessments_inner.py +shieldlabs_generated/models/webhook_client_identity_claims_inner.py +shieldlabs_generated/models/webhook_client_identity_evidence_inner.py +shieldlabs_generated/models/webhook_client_identity_verified_inner.py shieldlabs_generated/models/webhook_ping_event.py shieldlabs_generated/rest.py shieldlabs_generated_README.md diff --git a/generated/shieldlabs_generated/__init__.py b/generated/shieldlabs_generated/__init__.py index 37cc875..0fbba45 100644 --- a/generated/shieldlabs_generated/__init__.py +++ b/generated/shieldlabs_generated/__init__.py @@ -31,19 +31,39 @@ "ApiKeyError", "ApiAttributeError", "ApiException", + "ClientIdentity", + "ClientIdentityAssessmentsInner", + "ClientIdentityClaimsInner", + "ClientIdentityEvidenceInner", + "ClientIdentityVerifiedInner", "DetectionFlags", + "DetectionFlags20261012", "DomainProfile", "ErrorBody", + "HRE", + "HREResult", "HealthStatus", "HistoryPage", "HistoryRow", "IdentificationScoredData", + "IdentificationScoredDataDetectionFlags", "IdentificationScoredEvent", "IpInfo", + "LegacyDetectionFlags", "LegacySnapshot", + "MultiaccountChangedData", + "MultiaccountChangedDataEvidenceSummary", + "MultiaccountChangedDataSource", + "MultiaccountChangedEvent", + "RiskEvent", "ScoreDetail", "Signal", "TrafficSource", + "WebhookClientIdentity", + "WebhookClientIdentityAssessmentsInner", + "WebhookClientIdentityClaimsInner", + "WebhookClientIdentityEvidenceInner", + "WebhookClientIdentityVerifiedInner", "WebhookPingEvent", ] @@ -64,18 +84,38 @@ from shieldlabs_generated.exceptions import ApiException as ApiException # import models into sdk package +from shieldlabs_generated.models.client_identity import ClientIdentity as ClientIdentity +from shieldlabs_generated.models.client_identity_assessments_inner import ClientIdentityAssessmentsInner as ClientIdentityAssessmentsInner +from shieldlabs_generated.models.client_identity_claims_inner import ClientIdentityClaimsInner as ClientIdentityClaimsInner +from shieldlabs_generated.models.client_identity_evidence_inner import ClientIdentityEvidenceInner as ClientIdentityEvidenceInner +from shieldlabs_generated.models.client_identity_verified_inner import ClientIdentityVerifiedInner as ClientIdentityVerifiedInner from shieldlabs_generated.models.detection_flags import DetectionFlags as DetectionFlags +from shieldlabs_generated.models.detection_flags20261012 import DetectionFlags20261012 as DetectionFlags20261012 from shieldlabs_generated.models.domain_profile import DomainProfile as DomainProfile from shieldlabs_generated.models.error_body import ErrorBody as ErrorBody +from shieldlabs_generated.models.hre import HRE as HRE +from shieldlabs_generated.models.hre_result import HREResult as HREResult from shieldlabs_generated.models.health_status import HealthStatus as HealthStatus from shieldlabs_generated.models.history_page import HistoryPage as HistoryPage from shieldlabs_generated.models.history_row import HistoryRow as HistoryRow from shieldlabs_generated.models.identification_scored_data import IdentificationScoredData as IdentificationScoredData +from shieldlabs_generated.models.identification_scored_data_detection_flags import IdentificationScoredDataDetectionFlags as IdentificationScoredDataDetectionFlags from shieldlabs_generated.models.identification_scored_event import IdentificationScoredEvent as IdentificationScoredEvent from shieldlabs_generated.models.ip_info import IpInfo as IpInfo +from shieldlabs_generated.models.legacy_detection_flags import LegacyDetectionFlags as LegacyDetectionFlags from shieldlabs_generated.models.legacy_snapshot import LegacySnapshot as LegacySnapshot +from shieldlabs_generated.models.multiaccount_changed_data import MultiaccountChangedData as MultiaccountChangedData +from shieldlabs_generated.models.multiaccount_changed_data_evidence_summary import MultiaccountChangedDataEvidenceSummary as MultiaccountChangedDataEvidenceSummary +from shieldlabs_generated.models.multiaccount_changed_data_source import MultiaccountChangedDataSource as MultiaccountChangedDataSource +from shieldlabs_generated.models.multiaccount_changed_event import MultiaccountChangedEvent as MultiaccountChangedEvent +from shieldlabs_generated.models.risk_event import RiskEvent as RiskEvent from shieldlabs_generated.models.score_detail import ScoreDetail as ScoreDetail from shieldlabs_generated.models.signal import Signal as Signal from shieldlabs_generated.models.traffic_source import TrafficSource as TrafficSource +from shieldlabs_generated.models.webhook_client_identity import WebhookClientIdentity as WebhookClientIdentity +from shieldlabs_generated.models.webhook_client_identity_assessments_inner import WebhookClientIdentityAssessmentsInner as WebhookClientIdentityAssessmentsInner +from shieldlabs_generated.models.webhook_client_identity_claims_inner import WebhookClientIdentityClaimsInner as WebhookClientIdentityClaimsInner +from shieldlabs_generated.models.webhook_client_identity_evidence_inner import WebhookClientIdentityEvidenceInner as WebhookClientIdentityEvidenceInner +from shieldlabs_generated.models.webhook_client_identity_verified_inner import WebhookClientIdentityVerifiedInner as WebhookClientIdentityVerifiedInner from shieldlabs_generated.models.webhook_ping_event import WebhookPingEvent as WebhookPingEvent diff --git a/generated/shieldlabs_generated/api/history_api_api.py b/generated/shieldlabs_generated/api/history_api_api.py index 71dfff7..e47b8a6 100644 --- a/generated/shieldlabs_generated/api/history_api_api.py +++ b/generated/shieldlabs_generated/api/history_api_api.py @@ -42,7 +42,7 @@ def __init__(self, api_client=None) -> None: @validate_call def search_history( self, - search_type: Annotated[StrictStr, Field(description="Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], + search_type: Annotated[StrictStr, Field(description="Identifier to search by: - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. `request_id` is a legacy alias of `identification_id` and is still accepted. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier, validated on your side before sending: - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[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}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`.")], limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side.")] = None, offset: Annotated[Optional[Annotated[int, Field(strict=True, ge=0)]], Field(description="Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`.")] = None, @@ -61,9 +61,9 @@ def search_history( ) -> HistoryPage: """Search identifications - Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up network checks finish, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. + Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row becomes available asynchronously, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. - :param search_type: Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) + :param search_type: Identifier to search by: - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. `request_id` is a legacy alias of `identification_id` and is still accepted. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) :type search_type: str :param value: Value of the identifier, validated on your side before sending: - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[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}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`. (required) :type value: str @@ -127,7 +127,7 @@ def search_history( @validate_call def search_history_with_http_info( self, - search_type: Annotated[StrictStr, Field(description="Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], + search_type: Annotated[StrictStr, Field(description="Identifier to search by: - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. `request_id` is a legacy alias of `identification_id` and is still accepted. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier, validated on your side before sending: - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[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}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`.")], limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side.")] = None, offset: Annotated[Optional[Annotated[int, Field(strict=True, ge=0)]], Field(description="Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`.")] = None, @@ -146,9 +146,9 @@ def search_history_with_http_info( ) -> ApiResponse[HistoryPage]: """Search identifications - Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up network checks finish, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. + Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row becomes available asynchronously, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. - :param search_type: Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) + :param search_type: Identifier to search by: - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. `request_id` is a legacy alias of `identification_id` and is still accepted. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) :type search_type: str :param value: Value of the identifier, validated on your side before sending: - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[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}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`. (required) :type value: str @@ -212,7 +212,7 @@ def search_history_with_http_info( @validate_call def search_history_without_preload_content( self, - search_type: Annotated[StrictStr, Field(description="Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], + search_type: Annotated[StrictStr, Field(description="Identifier to search by: - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. `request_id` is a legacy alias of `identification_id` and is still accepted. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier, validated on your side before sending: - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[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}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`.")], limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side.")] = None, offset: Annotated[Optional[Annotated[int, Field(strict=True, ge=0)]], Field(description="Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`.")] = None, @@ -231,9 +231,9 @@ def search_history_without_preload_content( ) -> RESTResponseType: """Search identifications - Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up network checks finish, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. + Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row becomes available asynchronously, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. - :param search_type: Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) + :param search_type: Identifier to search by: - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. `request_id` is a legacy alias of `identification_id` and is still accepted. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) :type search_type: str :param value: Value of the identifier, validated on your side before sending: - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[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}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`. (required) :type value: str diff --git a/generated/shieldlabs_generated/api/management_api_api.py b/generated/shieldlabs_generated/api/management_api_api.py index 6461fba..22ba77c 100644 --- a/generated/shieldlabs_generated/api/management_api_api.py +++ b/generated/shieldlabs_generated/api/management_api_api.py @@ -328,7 +328,7 @@ def _get_domain_profile_serialize( def search_history_deprecated( self, x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], - type: Annotated[StrictStr, Field(description="Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], + type: Annotated[StrictStr, Field(description="Identifier to search by. `request_id` is a legacy alias of `identification_id`. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text.")], limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`.")] = None, _request_timeout: Union[ @@ -350,7 +350,7 @@ def search_history_deprecated( :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) :type x_shield_domain: str - :param type: Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) + :param type: Identifier to search by. `request_id` is a legacy alias of `identification_id`. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) :type type: str :param value: Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. (required) :type value: str @@ -415,7 +415,7 @@ def search_history_deprecated( def search_history_deprecated_with_http_info( self, x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], - type: Annotated[StrictStr, Field(description="Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], + type: Annotated[StrictStr, Field(description="Identifier to search by. `request_id` is a legacy alias of `identification_id`. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text.")], limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`.")] = None, _request_timeout: Union[ @@ -437,7 +437,7 @@ def search_history_deprecated_with_http_info( :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) :type x_shield_domain: str - :param type: Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) + :param type: Identifier to search by. `request_id` is a legacy alias of `identification_id`. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) :type type: str :param value: Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. (required) :type value: str @@ -502,7 +502,7 @@ def search_history_deprecated_with_http_info( def search_history_deprecated_without_preload_content( self, x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], - type: Annotated[StrictStr, Field(description="Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], + type: Annotated[StrictStr, Field(description="Identifier to search by. `request_id` is a legacy alias of `identification_id`. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text.")], limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`.")] = None, _request_timeout: Union[ @@ -524,7 +524,7 @@ def search_history_deprecated_without_preload_content( :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) :type x_shield_domain: str - :param type: Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) + :param type: Identifier to search by. `request_id` is a legacy alias of `identification_id`. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) :type type: str :param value: Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. (required) :type value: str diff --git a/generated/shieldlabs_generated/models/__init__.py b/generated/shieldlabs_generated/models/__init__.py index 9560d31..9632291 100644 --- a/generated/shieldlabs_generated/models/__init__.py +++ b/generated/shieldlabs_generated/models/__init__.py @@ -14,18 +14,38 @@ """ # noqa: E501 # import models into model package +from shieldlabs_generated.models.client_identity import ClientIdentity +from shieldlabs_generated.models.client_identity_assessments_inner import ClientIdentityAssessmentsInner +from shieldlabs_generated.models.client_identity_claims_inner import ClientIdentityClaimsInner +from shieldlabs_generated.models.client_identity_evidence_inner import ClientIdentityEvidenceInner +from shieldlabs_generated.models.client_identity_verified_inner import ClientIdentityVerifiedInner from shieldlabs_generated.models.detection_flags import DetectionFlags +from shieldlabs_generated.models.detection_flags20261012 import DetectionFlags20261012 from shieldlabs_generated.models.domain_profile import DomainProfile from shieldlabs_generated.models.error_body import ErrorBody +from shieldlabs_generated.models.hre import HRE +from shieldlabs_generated.models.hre_result import HREResult from shieldlabs_generated.models.health_status import HealthStatus from shieldlabs_generated.models.history_page import HistoryPage from shieldlabs_generated.models.history_row import HistoryRow from shieldlabs_generated.models.identification_scored_data import IdentificationScoredData +from shieldlabs_generated.models.identification_scored_data_detection_flags import IdentificationScoredDataDetectionFlags from shieldlabs_generated.models.identification_scored_event import IdentificationScoredEvent from shieldlabs_generated.models.ip_info import IpInfo +from shieldlabs_generated.models.legacy_detection_flags import LegacyDetectionFlags from shieldlabs_generated.models.legacy_snapshot import LegacySnapshot +from shieldlabs_generated.models.multiaccount_changed_data import MultiaccountChangedData +from shieldlabs_generated.models.multiaccount_changed_data_evidence_summary import MultiaccountChangedDataEvidenceSummary +from shieldlabs_generated.models.multiaccount_changed_data_source import MultiaccountChangedDataSource +from shieldlabs_generated.models.multiaccount_changed_event import MultiaccountChangedEvent +from shieldlabs_generated.models.risk_event import RiskEvent from shieldlabs_generated.models.score_detail import ScoreDetail from shieldlabs_generated.models.signal import Signal from shieldlabs_generated.models.traffic_source import TrafficSource +from shieldlabs_generated.models.webhook_client_identity import WebhookClientIdentity +from shieldlabs_generated.models.webhook_client_identity_assessments_inner import WebhookClientIdentityAssessmentsInner +from shieldlabs_generated.models.webhook_client_identity_claims_inner import WebhookClientIdentityClaimsInner +from shieldlabs_generated.models.webhook_client_identity_evidence_inner import WebhookClientIdentityEvidenceInner +from shieldlabs_generated.models.webhook_client_identity_verified_inner import WebhookClientIdentityVerifiedInner from shieldlabs_generated.models.webhook_ping_event import WebhookPingEvent diff --git a/generated/shieldlabs_generated/models/client_identity.py b/generated/shieldlabs_generated/models/client_identity.py new file mode 100644 index 0000000..f69f292 --- /dev/null +++ b/generated/shieldlabs_generated/models/client_identity.py @@ -0,0 +1,156 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing_extensions import Annotated +from shieldlabs_generated.models.client_identity_assessments_inner import ClientIdentityAssessmentsInner +from shieldlabs_generated.models.client_identity_claims_inner import ClientIdentityClaimsInner +from shieldlabs_generated.models.client_identity_evidence_inner import ClientIdentityEvidenceInner +from shieldlabs_generated.models.client_identity_verified_inner import ClientIdentityVerifiedInner +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class ClientIdentity(BaseModel): + """ + Stored ingest attribution, independent of risk score and browser automation. Claimed names are unverified. A verified provider does not verify the agent name, service or AI mode. Read proof scope from each attribution and its referenced evidence. Unknown string values and fields must be accepted. + """ # noqa: E501 + schema_version: StrictStr = Field(description="Open version identifier. Version 1 is currently emitted.") + classification_revision: Annotated[int, Field(strict=True, ge=1)] + classifier_version: StrictStr + registry_revision: StrictStr + availability: StrictStr = Field(description="available or input_unavailable. Missing object means no stored identity data; it does not mean human.") + observed_at: datetime + decided_at: datetime + claims: List[ClientIdentityClaimsInner] + verified: List[ClientIdentityVerifiedInner] + assessments: List[ClientIdentityAssessmentsInner] + evidence: List[ClientIdentityEvidenceInner] + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["schema_version", "classification_revision", "classifier_version", "registry_revision", "availability", "observed_at", "decided_at", "claims", "verified", "assessments", "evidence"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ClientIdentity from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in claims (list) + _items = [] + if self.claims: + for _item_claims in self.claims: + if _item_claims: + _items.append(_item_claims.to_dict()) + _dict['claims'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in verified (list) + _items = [] + if self.verified: + for _item_verified in self.verified: + if _item_verified: + _items.append(_item_verified.to_dict()) + _dict['verified'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in assessments (list) + _items = [] + if self.assessments: + for _item_assessments in self.assessments: + if _item_assessments: + _items.append(_item_assessments.to_dict()) + _dict['assessments'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in evidence (list) + _items = [] + if self.evidence: + for _item_evidence in self.evidence: + if _item_evidence: + _items.append(_item_evidence.to_dict()) + _dict['evidence'] = _items + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ClientIdentity from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "schema_version": obj.get("schema_version"), + "classification_revision": obj.get("classification_revision"), + "classifier_version": obj.get("classifier_version"), + "registry_revision": obj.get("registry_revision"), + "availability": obj.get("availability"), + "observed_at": obj.get("observed_at"), + "decided_at": obj.get("decided_at"), + "claims": [ClientIdentityClaimsInner.from_dict(_item) for _item in obj["claims"]] if obj.get("claims") is not None else None, + "verified": [ClientIdentityVerifiedInner.from_dict(_item) for _item in obj["verified"]] if obj.get("verified") is not None else None, + "assessments": [ClientIdentityAssessmentsInner.from_dict(_item) for _item in obj["assessments"]] if obj.get("assessments") is not None else None, + "evidence": [ClientIdentityEvidenceInner.from_dict(_item) for _item in obj["evidence"]] if obj.get("evidence") is not None else None + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/client_identity_assessments_inner.py b/generated/shieldlabs_generated/models/client_identity_assessments_inner.py new file mode 100644 index 0000000..030f5e8 --- /dev/null +++ b/generated/shieldlabs_generated/models/client_identity_assessments_inner.py @@ -0,0 +1,106 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class ClientIdentityAssessmentsInner(BaseModel): + """ + ClientIdentityAssessmentsInner + """ # noqa: E501 + candidate_profile_id: Optional[StrictStr] = None + status: StrictStr + reason: StrictStr + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["candidate_profile_id", "status", "reason"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ClientIdentityAssessmentsInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ClientIdentityAssessmentsInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "candidate_profile_id": obj.get("candidate_profile_id"), + "status": obj.get("status"), + "reason": obj.get("reason") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/client_identity_claims_inner.py b/generated/shieldlabs_generated/models/client_identity_claims_inner.py new file mode 100644 index 0000000..a5d15e0 --- /dev/null +++ b/generated/shieldlabs_generated/models/client_identity_claims_inner.py @@ -0,0 +1,114 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class ClientIdentityClaimsInner(BaseModel): + """ + ClientIdentityClaimsInner + """ # noqa: E501 + profile_id: StrictStr + provider_id: StrictStr + provider_name: StrictStr + agent_name: StrictStr + client_kind: StrictStr + purpose: StrictStr + source: StrictStr + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["profile_id", "provider_id", "provider_name", "agent_name", "client_kind", "purpose", "source"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ClientIdentityClaimsInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ClientIdentityClaimsInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "profile_id": obj.get("profile_id"), + "provider_id": obj.get("provider_id"), + "provider_name": obj.get("provider_name"), + "agent_name": obj.get("agent_name"), + "client_kind": obj.get("client_kind"), + "purpose": obj.get("purpose"), + "source": obj.get("source") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/client_identity_evidence_inner.py b/generated/shieldlabs_generated/models/client_identity_evidence_inner.py new file mode 100644 index 0000000..d1b7d59 --- /dev/null +++ b/generated/shieldlabs_generated/models/client_identity_evidence_inner.py @@ -0,0 +1,121 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class ClientIdentityEvidenceInner(BaseModel): + """ + ClientIdentityEvidenceInner + """ # noqa: E501 + id: StrictStr + method: StrictStr + source_id: StrictStr + source_revision: StrictStr + checked_at: datetime + evaluated_at: datetime + covered_attributes: List[StrictStr] + covered_components: Optional[List[StrictStr]] = None + request_binding: Optional[StrictStr] = None + replay_policy: Optional[StrictStr] = None + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["id", "method", "source_id", "source_revision", "checked_at", "evaluated_at", "covered_attributes", "covered_components", "request_binding", "replay_policy"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ClientIdentityEvidenceInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ClientIdentityEvidenceInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "id": obj.get("id"), + "method": obj.get("method"), + "source_id": obj.get("source_id"), + "source_revision": obj.get("source_revision"), + "checked_at": obj.get("checked_at"), + "evaluated_at": obj.get("evaluated_at"), + "covered_attributes": obj.get("covered_attributes"), + "covered_components": obj.get("covered_components"), + "request_binding": obj.get("request_binding"), + "replay_policy": obj.get("replay_policy") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/client_identity_verified_inner.py b/generated/shieldlabs_generated/models/client_identity_verified_inner.py new file mode 100644 index 0000000..48a3eba --- /dev/null +++ b/generated/shieldlabs_generated/models/client_identity_verified_inner.py @@ -0,0 +1,106 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class ClientIdentityVerifiedInner(BaseModel): + """ + ClientIdentityVerifiedInner + """ # noqa: E501 + subject: StrictStr = Field(description="The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes.") + value_id: StrictStr + evidence_ids: List[StrictStr] + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["subject", "value_id", "evidence_ids"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ClientIdentityVerifiedInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ClientIdentityVerifiedInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "subject": obj.get("subject"), + "value_id": obj.get("value_id"), + "evidence_ids": obj.get("evidence_ids") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/detection_flags.py b/generated/shieldlabs_generated/models/detection_flags.py index 775aeb7..bca95b0 100644 --- a/generated/shieldlabs_generated/models/detection_flags.py +++ b/generated/shieldlabs_generated/models/detection_flags.py @@ -26,28 +26,30 @@ class DetectionFlags(BaseModel): """ - Stable yes/no verdicts for the identification. Always all 19 keys. Branch on these flags and on the Risk Score; signal names are for display and logging. When `search_bot` is `true`, `incognito`, `check_incomplete`, `ip_mismatch` and `javascript_disabled` are always `false`. + The 20 public detection signals in scored webhook contract 2026-10-13. os_mismatch is true when either OS detector fired. browser_vpn_proxy contributes to vpn and is not its own flag. Groups and owner attribution are separate from this set. """ # noqa: E501 - vpn: StrictBool = Field(description="A VPN was detected (scored `vpn` signal).") - privacy_relay: StrictBool = Field(description="A privacy relay such as iCloud Private Relay was detected.") - browser_vpn_proxy: StrictBool = Field(description="A VPN or proxy built into the browser or one of its extensions. `true` exactly when `connection_type` is `browser_vpn_proxy`.") - tor: StrictBool = Field(description="The request came through the Tor network.") + vpn: StrictBool = Field(description="A VPN was detected, including a VPN or proxy built into the browser. connection_type may still be browser_vpn_proxy.") proxy: StrictBool = Field(description="A proxy was detected.") - datacenter_ip: StrictBool = Field(description="The public IP belongs to a datacenter or hosting range.") - abuser: StrictBool = Field(description="The public IP has a record of abuse in IP intelligence.") - os_mismatch: StrictBool = Field(description="The operating system seen on the network differs from the one the browser reports.") + private_relay: StrictBool = Field(description="A privacy relay such as iCloud Private Relay was detected. Older payloads used privacy_relay.") + check_incomplete: StrictBool = Field(description="Part of the browser checks timed out, so the verdict rests on partial data. Informational. Weight 0, and present in signals when true.") + network_check_incomplete: StrictBool = Field(description="The browser network (STUN) check did not complete. Cleared again when a late network result arrives. Older payloads used stun_not_checked.") os_not_detected: StrictBool = Field(description="The operating system could not be determined from the User-Agent or the network.") timezone_mismatch: StrictBool = Field(description="The browser timezone differs from the timezone of the IP location.") - anti_detect_browser: StrictBool = Field(description="An anti-detect browser was detected.") - browser_automation: StrictBool = Field(description="Browser automation was detected, for example a WebDriver-controlled browser.") - ip_mismatch: StrictBool = Field(description="The public IP differs from the local IP found by the browser network check. Informational: it does not add to the score.") - incognito: StrictBool = Field(description="The browser runs in a private window.") - search_bot: StrictBool = Field(description="A search-engine crawler. Its Risk Score is always 0.") - suspicious_paid_click: StrictBool = Field(description="The visit came from a paid ad click (Google Ads, Meta, TikTok, Microsoft Ads, LinkedIn, Pinterest or X) and the Risk Score is 60 or more (the 999 marker included).") javascript_disabled: StrictBool = Field(description="JavaScript, or the browser APIs the checks need, were unavailable.") - stun_not_checked: StrictBool = Field(description="The browser network (STUN) check did not complete. Cleared again when a late network result arrives.") - check_incomplete: StrictBool = Field(description="Part of the browser checks timed out, so the verdict rests on partial data. Informational.") - __properties: ClassVar[List[str]] = ["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"] + os_mismatch: StrictBool = Field(description="Either operating-system consistency detector fired. The two detectors are not separate public flags.") + ip_mismatch: StrictBool = Field(description="A qualifying mismatch between the public IP and the browser's network evidence was detected. Nominal weight 45; read the returned Risk Score.") + latency_test: StrictBool = Field(description="The WebSocket-to-TCP latency test fired. Older payloads used ws_tcp_latency.") + anti_detect_browser: StrictBool = Field(description="An anti-detect browser was detected.") + device_spoofing: StrictBool + banned_ip: StrictBool = Field(description="The visitor IP is banned for this window. The matching signal weight is 999. Older payloads used rate_limited.") + incognito: StrictBool = Field(description="The browser runs in a private window. Weight 0, and present in signals when true.") + search_bot: StrictBool = Field(description="A search-engine crawler. Its Risk Score is always 0. Weight 0, and present in signals when true.") + ai_bot: StrictBool = Field(description="A recognized AI bot or crawler. Its Risk Score is always 0. ShieldLabs identifies the provider when available. Weight 0, and present in signals when true.") + browser_automation: StrictBool = Field(description="Browser automation was detected, for example a WebDriver-controlled browser. Catalogue weight is 90 for task 204 scoring releases; historical result versions may retain the earlier weight 60.") + ai_browser: StrictBool = Field(description="A recognized AI browser. Its Risk Score is always 0. ShieldLabs identifies the provider when available. Weight 0, and present in signals when true.") + tor: StrictBool = Field(description="The request came through the Tor network.") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["vpn", "proxy", "private_relay", "check_incomplete", "network_check_incomplete", "os_not_detected", "timezone_mismatch", "javascript_disabled", "os_mismatch", "ip_mismatch", "latency_test", "anti_detect_browser", "device_spoofing", "banned_ip", "incognito", "search_bot", "ai_bot", "browser_automation", "ai_browser", "tor"] model_config = ConfigDict( validate_by_name=True, @@ -79,8 +81,10 @@ def to_dict(self) -> Dict[str, Any]: * `None` is only added to the output dict for nullable fields that were set at model initialization. Other fields with value `None` are ignored. + * Fields in `self.additional_properties` are added to the output dict. """ excluded_fields: Set[str] = set([ + "additional_properties", ]) _dict = self.model_dump( @@ -88,6 +92,11 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + return _dict @classmethod @@ -101,25 +110,31 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "vpn": obj.get("vpn"), - "privacy_relay": obj.get("privacy_relay"), - "browser_vpn_proxy": obj.get("browser_vpn_proxy"), - "tor": obj.get("tor"), "proxy": obj.get("proxy"), - "datacenter_ip": obj.get("datacenter_ip"), - "abuser": obj.get("abuser"), - "os_mismatch": obj.get("os_mismatch"), + "private_relay": obj.get("private_relay"), + "check_incomplete": obj.get("check_incomplete"), + "network_check_incomplete": obj.get("network_check_incomplete"), "os_not_detected": obj.get("os_not_detected"), "timezone_mismatch": obj.get("timezone_mismatch"), - "anti_detect_browser": obj.get("anti_detect_browser"), - "browser_automation": obj.get("browser_automation"), + "javascript_disabled": obj.get("javascript_disabled"), + "os_mismatch": obj.get("os_mismatch"), "ip_mismatch": obj.get("ip_mismatch"), + "latency_test": obj.get("latency_test"), + "anti_detect_browser": obj.get("anti_detect_browser"), + "device_spoofing": obj.get("device_spoofing"), + "banned_ip": obj.get("banned_ip"), "incognito": obj.get("incognito"), "search_bot": obj.get("search_bot"), - "suspicious_paid_click": obj.get("suspicious_paid_click"), - "javascript_disabled": obj.get("javascript_disabled"), - "stun_not_checked": obj.get("stun_not_checked"), - "check_incomplete": obj.get("check_incomplete") + "ai_bot": obj.get("ai_bot"), + "browser_automation": obj.get("browser_automation"), + "ai_browser": obj.get("ai_browser"), + "tor": obj.get("tor") }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + return _obj diff --git a/generated/shieldlabs_generated/models/detection_flags20261012.py b/generated/shieldlabs_generated/models/detection_flags20261012.py new file mode 100644 index 0000000..aace37a --- /dev/null +++ b/generated/shieldlabs_generated/models/detection_flags20261012.py @@ -0,0 +1,142 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class DetectionFlags20261012(BaseModel): + """ + Retained scored webhook contract 2026-10-12. Twenty-one source detection signals, with os_mismatch and os_mismatch2 independent. Current deliveries use DetectionFlags (2026-10-13). + """ # noqa: E501 + vpn: StrictBool = Field(description="A VPN or Browser VPN/Proxy was detected.") + proxy: StrictBool = Field(description="A proxy was detected.") + privacy_relay: StrictBool = Field(description="A privacy relay such as iCloud Private Relay was detected.") + check_incomplete: StrictBool = Field(description="Part of the browser checks timed out, so the verdict rests on partial data. Informational.") + stun_not_checked: StrictBool = Field(description="The browser network (STUN) check did not complete. Cleared again when a late network result arrives.") + os_not_detected: StrictBool = Field(description="The operating system could not be determined from the User-Agent or the network.") + timezone_mismatch: StrictBool = Field(description="The browser timezone differs from the timezone of the IP location.") + javascript_disabled: StrictBool = Field(description="JavaScript, or the browser APIs the checks need, were unavailable.") + os_mismatch: StrictBool = Field(description="The first operating system consistency detector found a mismatch. Independent of os_mismatch2.") + os_mismatch2: StrictBool = Field(description="The second operating system consistency detector (TCP behaviour) found a mismatch. Independent of os_mismatch.") + ip_mismatch: StrictBool = Field(description="A qualifying mismatch between the public IP and the browser's network evidence was detected. Read the returned Risk Score and signal contributions.") + latency_test: StrictBool + anti_detect_browser: StrictBool = Field(description="An anti-detect browser was detected.") + device_spoofing: StrictBool + banned_ip: StrictBool + incognito: StrictBool = Field(description="The browser runs in a private window.") + search_bot: StrictBool = Field(description="A search-engine crawler. Its Risk Score is always 0.") + ai_bot: StrictBool = Field(description="An already accepted crawler has purpose training or user_fetch. ChatGPT-User, Claude-User and Perplexity-User are AI bots, not AI browsers. A User-Agent claim alone does not grant an accepted verdict.") + browser_automation: StrictBool = Field(description="Browser automation was detected, for example a WebDriver-controlled browser. Catalogue weight is 90 for task 204 scoring releases; historical result versions may retain the earlier weight 60.") + ai_browser: StrictBool = Field(description="A browser has separately verified infrastructure or service attribution. A User-Agent claim or provider-only IP proof is insufficient.") + tor: StrictBool = Field(description="The request came through the Tor network.") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["vpn", "proxy", "privacy_relay", "check_incomplete", "stun_not_checked", "os_not_detected", "timezone_mismatch", "javascript_disabled", "os_mismatch", "os_mismatch2", "ip_mismatch", "latency_test", "anti_detect_browser", "device_spoofing", "banned_ip", "incognito", "search_bot", "ai_bot", "browser_automation", "ai_browser", "tor"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of DetectionFlags20261012 from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of DetectionFlags20261012 from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "vpn": obj.get("vpn"), + "proxy": obj.get("proxy"), + "privacy_relay": obj.get("privacy_relay"), + "check_incomplete": obj.get("check_incomplete"), + "stun_not_checked": obj.get("stun_not_checked"), + "os_not_detected": obj.get("os_not_detected"), + "timezone_mismatch": obj.get("timezone_mismatch"), + "javascript_disabled": obj.get("javascript_disabled"), + "os_mismatch": obj.get("os_mismatch"), + "os_mismatch2": obj.get("os_mismatch2"), + "ip_mismatch": obj.get("ip_mismatch"), + "latency_test": obj.get("latency_test"), + "anti_detect_browser": obj.get("anti_detect_browser"), + "device_spoofing": obj.get("device_spoofing"), + "banned_ip": obj.get("banned_ip"), + "incognito": obj.get("incognito"), + "search_bot": obj.get("search_bot"), + "ai_bot": obj.get("ai_bot"), + "browser_automation": obj.get("browser_automation"), + "ai_browser": obj.get("ai_browser"), + "tor": obj.get("tor") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/history_row.py b/generated/shieldlabs_generated/models/history_row.py index 3797dc2..6b97045 100644 --- a/generated/shieldlabs_generated/models/history_row.py +++ b/generated/shieldlabs_generated/models/history_row.py @@ -22,6 +22,7 @@ from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from uuid import UUID +from shieldlabs_generated.models.client_identity import ClientIdentity from typing import Optional, Set from typing_extensions import Self from pydantic_core import to_jsonable_python @@ -30,6 +31,7 @@ class HistoryRow(BaseModel): """ One identification as stored, in its latest version. It describes the same identification as a webhook `data` object, with different field names: | Webhook `data` | History row | |---|---| | `risk_score` | `score` | | `signals` | `score_details` (JSON-encoded string, zero weights included) | | `detection_flags` | the `is_*` columns and `check_incomplete` (each column names its flag) | | `detection_flags.browser_vpn_proxy` | derive it: `connection_type == \"browser_vpn_proxy\"` | | `domain` | `site_domain` when present, otherwise `domain` | | `public_ip` | `ip` (`0.0.0.0` instead of `\"\"`) and `country` | | `local_ip` | `webrtc_leak_ip` and `webrtc_leak_country` when `webrtc_leak_source` is set and not `none`, otherwise `web_rtc_ip` and `web_rtc_country` | | `traffic_source` | `traffic_channel`, `referrer_domain`, `entry_url`, `click_id_type`, `utm_*` (omitted when empty) | | `observed_at` (when scoring finished) | `created_at` (when the identification was made) | The `ip_mismatch` flag has no column. Rows also carry diagnostic network fields (TCP, MTU and STUN measurements) that are not part of the stable contract: ignore fields you do not know. """ # noqa: E501 + client_identity: Optional[ClientIdentity] = None identification_id: UUID = Field(description="Identifies one identification. The browser creates it as a UUID v4 and hands it to your page; it is the join key between the browser, the webhook and the History API. The nil UUID appears only on rate-limit marker rows that arrived with a malformed request ID.") session_id: UUID = Field(description="One visit on one origin (UUID v4 created in the browser), shared by the open tabs of that origin. The next visit after the last tab closes gets a new session ID. The nil UUID appears on rate-limit marker rows.") cookie_id: UUID = Field(description="First-party browser identifier kept by the ShieldLabs agent (UUID v4). A missing or malformed value is stored as the nil UUID.") @@ -44,7 +46,7 @@ class HistoryRow(BaseModel): device_type: StrictStr = Field(description="Device class from the browser. Known values: `desktop`, `mobile`, `tablet` and `unknown` (the class could not be determined). The set is open: keep values added in later versions and treat them as `unknown`.") country: StrictStr = Field(description="Country of `ip` as an English country name, or an empty string.") connection_type: StrictStr = Field(description="How the visitor connected. Known values: - `direct`: a regular connection; - `mobile`: a mobile carrier network; - `vpn`: a VPN; - `proxy`: a proxy, datacenter or hosting network (search-engine crawlers are reported here too); - `tor`: the Tor network; - `privacy_relay`: a privacy relay such as iCloud Private Relay; - `browser_vpn_proxy`: a VPN or proxy built into the browser or one of its extensions; - `unknown`: not enough data. The value can say `vpn` while `detection_flags.vpn` is `false` (IP intelligence classified the network, but the scored VPN check did not fire). Branch on `detection_flags` for decisions. The set is open: keep values added in later versions and treat them as `unknown`.") - score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"rate_limited\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.") + score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"banned_ip\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.") score_details: StrictStr = Field(description="The entries behind `score` as a JSON-encoded **string** holding an array of `{\"Value\": , \"Description\": }`. Parse it before use. Scored entries come first, followed by informational entries with `Value` 0, which can be long. Empty string when no details were stored. The webhook `signals` are the entries with a non-zero `Value`, in the same order, with each description turned into a signal name (for example `Is proxy` becomes `proxy`). Descriptions are free text for display: never branch on them.") created_at: Annotated[str, Field(strict=True)] = Field(description="Time of the identification as `YYYY-MM-DD HH:MM:SS.mmm` in UTC, without a zone designator (not RFC 3339). Older rows can lack the milliseconds.") ver: StrictInt = Field(description="Version of the row in Unix milliseconds. It increases every time the row is refined, for example when late network data re-scores it after the webhook was sent.") @@ -84,7 +86,7 @@ class HistoryRow(BaseModel): referrer_domain: Optional[StrictStr] = Field(default=None, description="Registrable domain of the referrer without `www.`; the crawler name (for example `GoogleBot`) for search-engine crawlers. Omitted when empty.") click_id_type: Optional[StrictStr] = Field(default=None, description="Ad click identifier type found in the landing URL. Omitted when empty.") additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["identification_id", "session_id", "cookie_id", "domain", "site_domain", "user_hid", "device_id", "visitor_id", "ip", "os", "browser", "device_type", "country", "connection_type", "score", "score_details", "created_at", "ver", "web_rtc_ip", "web_rtc_country", "web_rtc_connection_type", "webrtc_leak_ip", "webrtc_leak_country", "webrtc_leak_connection_type", "webrtc_leak_source", "is_vpn", "is_tor", "is_proxy", "is_datacenter", "is_abuser", "is_privacy_relay", "is_stun_not_checked", "check_incomplete", "is_antidetect", "is_os_mismatch", "is_os_not_detected", "is_timezone_mismatch", "is_js_disabled", "is_browser_automation", "is_incognito", "is_search_bot", "is_suspicious_paid_click", "entry_url", "utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "traffic_channel", "traffic_channel_group", "traffic_reason", "referrer_domain", "click_id_type"] + __properties: ClassVar[List[str]] = ["client_identity", "identification_id", "session_id", "cookie_id", "domain", "site_domain", "user_hid", "device_id", "visitor_id", "ip", "os", "browser", "device_type", "country", "connection_type", "score", "score_details", "created_at", "ver", "web_rtc_ip", "web_rtc_country", "web_rtc_connection_type", "webrtc_leak_ip", "webrtc_leak_country", "webrtc_leak_connection_type", "webrtc_leak_source", "is_vpn", "is_tor", "is_proxy", "is_datacenter", "is_abuser", "is_privacy_relay", "is_stun_not_checked", "check_incomplete", "is_antidetect", "is_os_mismatch", "is_os_not_detected", "is_timezone_mismatch", "is_js_disabled", "is_browser_automation", "is_incognito", "is_search_bot", "is_suspicious_paid_click", "entry_url", "utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "traffic_channel", "traffic_channel_group", "traffic_reason", "referrer_domain", "click_id_type"] @field_validator('created_at') def created_at_validate_regular_expression(cls, value): @@ -137,6 +139,9 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # override the default output from pydantic by calling `to_dict()` of client_identity + if self.client_identity: + _dict['client_identity'] = self.client_identity.to_dict() # puts key-value pairs in additional_properties in the top level if self.additional_properties is not None: for _key, _value in self.additional_properties.items(): @@ -154,6 +159,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ + "client_identity": ClientIdentity.from_dict(obj["client_identity"]) if obj.get("client_identity") is not None else None, "identification_id": obj.get("identification_id"), "session_id": obj.get("session_id"), "cookie_id": obj.get("cookie_id"), diff --git a/generated/shieldlabs_generated/models/hre.py b/generated/shieldlabs_generated/models/hre.py new file mode 100644 index 0000000..541ffa2 --- /dev/null +++ b/generated/shieldlabs_generated/models/hre.py @@ -0,0 +1,105 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from shieldlabs_generated.models.hre_result import HREResult +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class HRE(BaseModel): + """ + Three completed on-demand handler results. Technical errors block emission; no_history/skipped are explicit not_evaluated results. Anonymous checks are not_applicable. Multiaccount is separate. + """ # noqa: E501 + rules_version: Optional[StrictStr] = None + account_sharing: HREResult + account_takeover: HREResult + impossible_travel: HREResult + __properties: ClassVar[List[str]] = ["rules_version", "account_sharing", "account_takeover", "impossible_travel"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of HRE from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of account_sharing + if self.account_sharing: + _dict['account_sharing'] = self.account_sharing.to_dict() + # override the default output from pydantic by calling `to_dict()` of account_takeover + if self.account_takeover: + _dict['account_takeover'] = self.account_takeover.to_dict() + # override the default output from pydantic by calling `to_dict()` of impossible_travel + if self.impossible_travel: + _dict['impossible_travel'] = self.impossible_travel.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of HRE from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "rules_version": obj.get("rules_version"), + "account_sharing": HREResult.from_dict(obj["account_sharing"]) if obj.get("account_sharing") is not None else None, + "account_takeover": HREResult.from_dict(obj["account_takeover"]) if obj.get("account_takeover") is not None else None, + "impossible_travel": HREResult.from_dict(obj["impossible_travel"]) if obj.get("impossible_travel") is not None else None + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/hre_result.py b/generated/shieldlabs_generated/models/hre_result.py new file mode 100644 index 0000000..622b991 --- /dev/null +++ b/generated/shieldlabs_generated/models/hre_result.py @@ -0,0 +1,110 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class HREResult(BaseModel): + """ + HREResult + """ # noqa: E501 + status: StrictStr + level: Optional[StrictStr] + reason: StrictStr + devices: Optional[Annotated[int, Field(strict=True, ge=0)]] = None + min_devices: Optional[Annotated[int, Field(strict=True, ge=1)]] = None + cluster_id: Optional[StrictStr] = Field(default=None, description="Authoritative cluster ID for this HRE result. Null when no cluster was published for the result, including old stored verdicts; never a device ID. Reused on retries.") + __properties: ClassVar[List[str]] = ["status", "level", "reason", "devices", "min_devices", "cluster_id"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of HREResult from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if level (nullable) is None + # and model_fields_set contains the field + if self.level is None and "level" in self.model_fields_set: + _dict['level'] = None + + # set to None if cluster_id (nullable) is None + # and model_fields_set contains the field + if self.cluster_id is None and "cluster_id" in self.model_fields_set: + _dict['cluster_id'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of HREResult from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "status": obj.get("status"), + "level": obj.get("level"), + "reason": obj.get("reason"), + "devices": obj.get("devices"), + "min_devices": obj.get("min_devices"), + "cluster_id": obj.get("cluster_id") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/identification_scored_data.py b/generated/shieldlabs_generated/models/identification_scored_data.py index b769f98..ac246b4 100644 --- a/generated/shieldlabs_generated/models/identification_scored_data.py +++ b/generated/shieldlabs_generated/models/identification_scored_data.py @@ -23,18 +23,22 @@ from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from uuid import UUID -from shieldlabs_generated.models.detection_flags import DetectionFlags +from shieldlabs_generated.models.hre import HRE +from shieldlabs_generated.models.identification_scored_data_detection_flags import IdentificationScoredDataDetectionFlags from shieldlabs_generated.models.ip_info import IpInfo +from shieldlabs_generated.models.risk_event import RiskEvent from shieldlabs_generated.models.signal import Signal from shieldlabs_generated.models.traffic_source import TrafficSource +from shieldlabs_generated.models.webhook_client_identity import WebhookClientIdentity from typing import Optional, Set from typing_extensions import Self from pydantic_core import to_jsonable_python class IdentificationScoredData(BaseModel): """ - The scored identification. Every key is always present (no key is ever omitted); only `user_hid` can be `null`. + Final identification. risk_score is this scan only. Version 2026-10-13 uses signals for score contributions, including fired signals of weight 0, detection_flags for 20 public signals, and hre for account results. Older bodies remain accepted. """ # noqa: E501 + client_identity: Optional[WebhookClientIdentity] = Field(default=None, description="Optional best-effort attribution. May be omitted or null; every nested field is also optional and nullable. Integrations must work without these values.") identification_id: UUID = Field(description="Identifies one identification. The browser creates it as a UUID v4 and hands it to your page; it is the join key between the browser, the webhook and the History API. The nil UUID appears only on rate-limit marker rows that arrived with a malformed request ID.") visitor_id: UUID = Field(description="Server-side visitor identifier (UUID v5). It is sticky to the device: a new cookie on a known device keeps the existing visitor ID, so clearing cookies usually does not change it. The nil UUID appears on identifications without usable device data, such as rate-limit marker rows.") device_id: UUID = Field(description="Server-side device identifier (UUID v5). It survives cleared cookies and private windows. The nil UUID `00000000-0000-0000-0000-000000000000` means that no usable device signals were collected (for example on rate-limit marker rows): never group identifications by it.") @@ -49,11 +53,18 @@ class IdentificationScoredData(BaseModel): browser: StrictStr = Field(description="Browser name, for example `Chrome`, `Safari`, `Firefox`, `Microsoft Edge`, `Opera`, `Samsung Internet`, `Brave`, `Chrome (iOS)`, `Safari (iOS)` or `Unknown`. Open set: display it, do not branch on it.") device_type: StrictStr = Field(description="Device class from the browser. Known values: `desktop`, `mobile`, `tablet` and `unknown` (the class could not be determined). The set is open: keep values added in later versions and treat them as `unknown`.") traffic_source: TrafficSource - risk_score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"rate_limited\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.") - signals: List[Signal] = Field(description="Weighted risk signals behind `risk_score`, in scoring order. Can be empty. The rate-limit marker carries exactly one entry, `{\"name\":\"rate_limited\",\"weight\":999}`.") - detection_flags: DetectionFlags - observed_at: datetime = Field(description="When scoring finished and the event was built (not the page view time); identical to the envelope `created_at`. RFC 3339 in UTC with up to 9 fractional digits.") - __properties: ClassVar[List[str]] = ["identification_id", "visitor_id", "device_id", "session_id", "cookie_id", "user_hid", "domain", "public_ip", "local_ip", "connection_type", "os", "browser", "device_type", "traffic_source", "risk_score", "signals", "detection_flags", "observed_at"] + risk_score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"banned_ip\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.") + signals: List[Signal] = Field(description="Weighted risk signals behind `risk_score`, in scoring order. Can be empty. The rate-limit marker carries exactly one entry, `{\"name\":\"banned_ip\",\"weight\":999}`.") + detection_flags: IdentificationScoredDataDetectionFlags + observed_at: datetime = Field(description="Original snapshot scan clock, distinct from envelope created_at. RFC 3339 in UTC with up to 9 fractional digits.") + result_version: Optional[Annotated[str, Field(min_length=1, strict=True)]] = Field(default=None, description="Legacy through 2026-10-08 only; omitted by current emitters.") + scoring_version: Optional[StrictStr] = Field(default=None, description="Legacy through 2026-10-08 only; omitted by current emitters.") + risk_events: Optional[List[RiskEvent]] = Field(default=None, description="Legacy 2026-10-06 only; absent from current scored events.") + hre: Optional[HRE] = None + search_bot_owner: Optional[StrictStr] = Field(default=None, description="Owner label from accepted search-bot detection; omitted when unknown or inactive.") + ai_bot_owner: Optional[StrictStr] = Field(default=None, description="Provider company label from accepted AI-bot detection; omitted when unknown or inactive.") + ai_browser_owner: Optional[StrictStr] = Field(default=None, description="Owner label from accepted AI-browser detection; omitted when unknown or inactive.") + __properties: ClassVar[List[str]] = ["client_identity", "identification_id", "visitor_id", "device_id", "session_id", "cookie_id", "user_hid", "domain", "public_ip", "local_ip", "connection_type", "os", "browser", "device_type", "traffic_source", "risk_score", "signals", "detection_flags", "observed_at", "result_version", "scoring_version", "risk_events", "hre", "search_bot_owner", "ai_bot_owner", "ai_browser_owner"] @field_validator('observed_at') def observed_at_validate_regular_expression(cls, value): @@ -104,6 +115,9 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # override the default output from pydantic by calling `to_dict()` of client_identity + if self.client_identity: + _dict['client_identity'] = self.client_identity.to_dict() # override the default output from pydantic by calling `to_dict()` of public_ip if self.public_ip: _dict['public_ip'] = self.public_ip.to_dict() @@ -123,6 +137,16 @@ def to_dict(self) -> Dict[str, Any]: # override the default output from pydantic by calling `to_dict()` of detection_flags if self.detection_flags: _dict['detection_flags'] = self.detection_flags.to_dict() + # override the default output from pydantic by calling `to_dict()` of each item in risk_events (list) + _items = [] + if self.risk_events: + for _item_risk_events in self.risk_events: + if _item_risk_events: + _items.append(_item_risk_events.to_dict()) + _dict['risk_events'] = _items + # override the default output from pydantic by calling `to_dict()` of hre + if self.hre: + _dict['hre'] = self.hre.to_dict() # set to None if user_hid (nullable) is None # and model_fields_set contains the field if self.user_hid is None and "user_hid" in self.model_fields_set: @@ -140,6 +164,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ + "client_identity": WebhookClientIdentity.from_dict(obj["client_identity"]) if obj.get("client_identity") is not None else None, "identification_id": obj.get("identification_id"), "visitor_id": obj.get("visitor_id"), "device_id": obj.get("device_id"), @@ -156,8 +181,15 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "traffic_source": TrafficSource.from_dict(obj["traffic_source"]) if obj.get("traffic_source") is not None else None, "risk_score": obj.get("risk_score"), "signals": [Signal.from_dict(_item) for _item in obj["signals"]] if obj.get("signals") is not None else None, - "detection_flags": DetectionFlags.from_dict(obj["detection_flags"]) if obj.get("detection_flags") is not None else None, - "observed_at": obj.get("observed_at") + "detection_flags": IdentificationScoredDataDetectionFlags.from_dict(obj["detection_flags"]) if obj.get("detection_flags") is not None else None, + "observed_at": obj.get("observed_at"), + "result_version": obj.get("result_version"), + "scoring_version": obj.get("scoring_version"), + "risk_events": [RiskEvent.from_dict(_item) for _item in obj["risk_events"]] if obj.get("risk_events") is not None else None, + "hre": HRE.from_dict(obj["hre"]) if obj.get("hre") is not None else None, + "search_bot_owner": obj.get("search_bot_owner"), + "ai_bot_owner": obj.get("ai_bot_owner"), + "ai_browser_owner": obj.get("ai_browser_owner") }) return _obj diff --git a/generated/shieldlabs_generated/models/identification_scored_data_detection_flags.py b/generated/shieldlabs_generated/models/identification_scored_data_detection_flags.py new file mode 100644 index 0000000..2e32cc7 --- /dev/null +++ b/generated/shieldlabs_generated/models/identification_scored_data_detection_flags.py @@ -0,0 +1,158 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class IdentificationScoredDataDetectionFlags(BaseModel): + """ + Contract 2026-10-13 requires DetectionFlags (exactly 20 public signals). 2026-10-12 keeps its 21-flag schema. Older versions keep LegacyDetectionFlags. + """ # noqa: E501 + vpn: Optional[StrictBool] = None + proxy: Optional[StrictBool] = None + tor: Optional[StrictBool] = None + private_relay: Optional[StrictBool] = None + privacy_relay: Optional[StrictBool] = None + network_check_incomplete: Optional[StrictBool] = None + stun_not_checked: Optional[StrictBool] = None + latency_test: Optional[StrictBool] = None + ws_tcp_latency: Optional[StrictBool] = None + banned_ip: Optional[StrictBool] = None + rate_limited: Optional[StrictBool] = None + check_incomplete: Optional[StrictBool] = None + os_not_detected: Optional[StrictBool] = None + timezone_mismatch: Optional[StrictBool] = None + javascript_disabled: Optional[StrictBool] = None + os_mismatch: Optional[StrictBool] = None + os_mismatch2: Optional[StrictBool] = None + ip_mismatch: Optional[StrictBool] = None + anti_detect_browser: Optional[StrictBool] = None + device_spoofing: Optional[StrictBool] = None + incognito: Optional[StrictBool] = None + search_bot: Optional[StrictBool] = None + ai_bot: Optional[StrictBool] = None + ai_browser: Optional[StrictBool] = None + browser_automation: Optional[StrictBool] = None + browser_vpn_proxy: Optional[StrictBool] = None + datacenter_ip: Optional[StrictBool] = None + abuser: Optional[StrictBool] = None + suspicious_paid_click: Optional[StrictBool] = None + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["vpn", "proxy", "tor", "private_relay", "privacy_relay", "network_check_incomplete", "stun_not_checked", "latency_test", "ws_tcp_latency", "banned_ip", "rate_limited", "check_incomplete", "os_not_detected", "timezone_mismatch", "javascript_disabled", "os_mismatch", "os_mismatch2", "ip_mismatch", "anti_detect_browser", "device_spoofing", "incognito", "search_bot", "ai_bot", "ai_browser", "browser_automation", "browser_vpn_proxy", "datacenter_ip", "abuser", "suspicious_paid_click"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of IdentificationScoredDataDetectionFlags from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of IdentificationScoredDataDetectionFlags from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "vpn": obj.get("vpn"), + "proxy": obj.get("proxy"), + "tor": obj.get("tor"), + "private_relay": obj.get("private_relay"), + "privacy_relay": obj.get("privacy_relay"), + "network_check_incomplete": obj.get("network_check_incomplete"), + "stun_not_checked": obj.get("stun_not_checked"), + "latency_test": obj.get("latency_test"), + "ws_tcp_latency": obj.get("ws_tcp_latency"), + "banned_ip": obj.get("banned_ip"), + "rate_limited": obj.get("rate_limited"), + "check_incomplete": obj.get("check_incomplete"), + "os_not_detected": obj.get("os_not_detected"), + "timezone_mismatch": obj.get("timezone_mismatch"), + "javascript_disabled": obj.get("javascript_disabled"), + "os_mismatch": obj.get("os_mismatch"), + "os_mismatch2": obj.get("os_mismatch2"), + "ip_mismatch": obj.get("ip_mismatch"), + "anti_detect_browser": obj.get("anti_detect_browser"), + "device_spoofing": obj.get("device_spoofing"), + "incognito": obj.get("incognito"), + "search_bot": obj.get("search_bot"), + "ai_bot": obj.get("ai_bot"), + "ai_browser": obj.get("ai_browser"), + "browser_automation": obj.get("browser_automation"), + "browser_vpn_proxy": obj.get("browser_vpn_proxy"), + "datacenter_ip": obj.get("datacenter_ip"), + "abuser": obj.get("abuser"), + "suspicious_paid_click": obj.get("suspicious_paid_click") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/identification_scored_event.py b/generated/shieldlabs_generated/models/identification_scored_event.py index e6177a7..be3d19f 100644 --- a/generated/shieldlabs_generated/models/identification_scored_event.py +++ b/generated/shieldlabs_generated/models/identification_scored_event.py @@ -20,7 +20,7 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from shieldlabs_generated.models.identification_scored_data import IdentificationScoredData from typing import Optional, Set @@ -32,17 +32,12 @@ class IdentificationScoredEvent(BaseModel): Body of an `identification.scored` delivery. The signature is not part of the body: it arrives in the `X-Shield-Signature` header. """ # noqa: E501 event_type: StrictStr = Field(description="Event type. Ignore events whose type you do not know instead of failing.") - schema_version: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Version of the webhook payload contract. Every event sent today carries `2026-06-01`. Accept other values, so that a future version does not break your handler.") - created_at: datetime = Field(description="When the event was built. Equal to `data.observed_at`.") + schema_version: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Webhook contract version. Scored release 2026-10-13 (20 public signals); 2026-10-12 keeps 21 independent source signals; multi-account group release 2026-10-07; parsers also accept legacy 2026-06-01.") + created_at: datetime = Field(description="RFC 3339 timestamp in UTC with up to 9 fractional digits (trailing zeros trimmed), for example `2026-09-30T12:34:57.482913041Z`. Parse it with a parser that accepts nanoseconds.") data: IdentificationScoredData - __properties: ClassVar[List[str]] = ["event_type", "schema_version", "created_at", "data"] - - @field_validator('event_type') - def event_type_validate_enum(cls, value): - """Validates the enum""" - if value not in set(['identification.scored']): - raise ValueError("must be one of enum values ('identification.scored')") - return value + event_id: Optional[Annotated[str, Field(min_length=1, strict=True)]] = Field(default=None, description="Logical site/request/final-result-version/type identity. Stable across retries and endpoints.") + site_id: Optional[Annotated[int, Field(strict=True, ge=1)]] = Field(default=None, description="Site scope when available; legacy domain-only accounts omit it.") + __properties: ClassVar[List[str]] = ["event_type", "schema_version", "created_at", "data", "event_id", "site_id"] @field_validator('created_at') def created_at_validate_regular_expression(cls, value): @@ -111,7 +106,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "event_type": obj.get("event_type"), "schema_version": obj.get("schema_version"), "created_at": obj.get("created_at"), - "data": IdentificationScoredData.from_dict(obj["data"]) if obj.get("data") is not None else None + "data": IdentificationScoredData.from_dict(obj["data"]) if obj.get("data") is not None else None, + "event_id": obj.get("event_id"), + "site_id": obj.get("site_id") }) return _obj diff --git a/generated/shieldlabs_generated/models/legacy_detection_flags.py b/generated/shieldlabs_generated/models/legacy_detection_flags.py new file mode 100644 index 0000000..222232c --- /dev/null +++ b/generated/shieldlabs_generated/models/legacy_detection_flags.py @@ -0,0 +1,141 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class LegacyDetectionFlags(BaseModel): + """ + Detection flags in retained webhook and History payloads before 2026-10-12. Missing extension flags are not evaluated negative results. + """ # noqa: E501 + vpn: StrictBool = Field(description="A VPN or Browser VPN/Proxy was detected.") + privacy_relay: StrictBool = Field(description="A privacy relay such as iCloud Private Relay was detected.") + browser_vpn_proxy: Optional[StrictBool] = Field(default=None, description="A VPN or proxy built into the browser or one of its extensions. `true` exactly when `connection_type` is `browser_vpn_proxy`.") + tor: StrictBool = Field(description="The request came through the Tor network.") + proxy: StrictBool = Field(description="A proxy was detected.") + datacenter_ip: Optional[StrictBool] = Field(default=None, description="The public IP belongs to a datacenter or hosting range.") + abuser: Optional[StrictBool] = Field(default=None, description="The public IP has a record of abuse in IP intelligence.") + os_mismatch: StrictBool = Field(description="Either internal OS consistency check detected a mismatch. The public flag combines browser/network and TCP behaviour checks.") + os_not_detected: StrictBool = Field(description="The operating system could not be determined from the User-Agent or the network.") + timezone_mismatch: StrictBool = Field(description="The browser timezone differs from the timezone of the IP location.") + anti_detect_browser: StrictBool = Field(description="An anti-detect browser was detected.") + browser_automation: StrictBool = Field(description="Browser automation was detected, for example a WebDriver-controlled browser. Catalogue weight is 90 for task 204 scoring releases; historical result versions may retain the earlier weight 60.") + ip_mismatch: StrictBool = Field(description="A qualifying mismatch between the public IP and the browser's network evidence was detected. Read the returned Risk Score and signal contributions.") + incognito: StrictBool = Field(description="The browser runs in a private window.") + search_bot: StrictBool = Field(description="A search-engine crawler. Its Risk Score is always 0.") + suspicious_paid_click: Optional[StrictBool] = Field(default=None, description="The visit came from a paid ad click (Google Ads, Meta, TikTok, Microsoft Ads, LinkedIn, Pinterest or X) and the Risk Score is 60 or more (the 999 marker included).") + javascript_disabled: StrictBool = Field(description="JavaScript, or the browser APIs the checks need, were unavailable.") + stun_not_checked: StrictBool = Field(description="The browser network (STUN) check did not complete. Cleared again when a late network result arrives.") + check_incomplete: StrictBool = Field(description="Part of the browser checks timed out, so the verdict rests on partial data. Informational.") + device_spoofing: Optional[StrictBool] = None + latency_test: Optional[StrictBool] = Field(default=None, description="Current public name of the latency detector. Payloads from schema 2026-10-13 before the slug rename used ws_tcp_latency.") + ws_tcp_latency: Optional[StrictBool] = Field(default=None, description="Previous public name of the latency detector. Current 2026-10-13 payloads use latency_test.") + banned_ip: Optional[StrictBool] = Field(default=None, description="Current public name of the banned-IP flag. Payloads from schema 2026-10-13 before the slug rename used rate_limited.") + rate_limited: Optional[StrictBool] = Field(default=None, description="Previous public name of the banned-IP flag. Current 2026-10-13 payloads use banned_ip.") + ai_bot: Optional[StrictBool] = Field(default=None, description="An accepted bot is classified as AI training or user-requested fetch. ChatGPT-User, Claude-User and Perplexity-User are AI bots, not AI browsers. Weight 0. The flag is optional on older payloads; its absence is not an evaluated negative result. Provider claims alone do not grant zero risk.") + ai_browser: Optional[StrictBool] = Field(default=None, description="A browser is identified by separate verified browser infrastructure. A generic browser User-Agent or a verified provider alone is insufficient. Weight 0. Optional on older payloads; absence is not an evaluated negative.") + os_mismatch2: Optional[StrictBool] = Field(default=None, description="Legacy 2026-10-06 only; current payloads combine this check into os_mismatch.") + __properties: ClassVar[List[str]] = ["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", "device_spoofing", "latency_test", "ws_tcp_latency", "banned_ip", "rate_limited", "ai_bot", "ai_browser", "os_mismatch2"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of LegacyDetectionFlags from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of LegacyDetectionFlags from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "vpn": obj.get("vpn"), + "privacy_relay": obj.get("privacy_relay"), + "browser_vpn_proxy": obj.get("browser_vpn_proxy"), + "tor": obj.get("tor"), + "proxy": obj.get("proxy"), + "datacenter_ip": obj.get("datacenter_ip"), + "abuser": obj.get("abuser"), + "os_mismatch": obj.get("os_mismatch"), + "os_not_detected": obj.get("os_not_detected"), + "timezone_mismatch": obj.get("timezone_mismatch"), + "anti_detect_browser": obj.get("anti_detect_browser"), + "browser_automation": obj.get("browser_automation"), + "ip_mismatch": obj.get("ip_mismatch"), + "incognito": obj.get("incognito"), + "search_bot": obj.get("search_bot"), + "suspicious_paid_click": obj.get("suspicious_paid_click"), + "javascript_disabled": obj.get("javascript_disabled"), + "stun_not_checked": obj.get("stun_not_checked"), + "check_incomplete": obj.get("check_incomplete"), + "device_spoofing": obj.get("device_spoofing"), + "latency_test": obj.get("latency_test"), + "ws_tcp_latency": obj.get("ws_tcp_latency"), + "banned_ip": obj.get("banned_ip"), + "rate_limited": obj.get("rate_limited"), + "ai_bot": obj.get("ai_bot"), + "ai_browser": obj.get("ai_browser"), + "os_mismatch2": obj.get("os_mismatch2") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/legacy_snapshot.py b/generated/shieldlabs_generated/models/legacy_snapshot.py index c6be89c..da295d3 100644 --- a/generated/shieldlabs_generated/models/legacy_snapshot.py +++ b/generated/shieldlabs_generated/models/legacy_snapshot.py @@ -47,7 +47,7 @@ class LegacySnapshot(BaseModel): device_type: StrictStr = Field(description="Device class from the browser. Known values: `desktop`, `mobile`, `tablet` and `unknown` (the class could not be determined). The set is open: keep values added in later versions and treat them as `unknown`.", alias="DeviceType") country: StrictStr = Field(description="Country of `IP` as an English country name, or an empty string.", alias="Country") user_hid: StrictStr = Field(description="User HID as passed to the agent; `anonymous` for anonymous checks.", alias="UserHID") - score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"rate_limited\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.", alias="Score") + score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"banned_ip\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.", alias="Score") details: List[ScoreDetail] = Field(description="Every entry behind `Score`, informational entries with `Value` 0 included (unlike the History API, this is a parsed array, not a string).", alias="Details") last_request_time: datetime = Field(description="Time of the identification, RFC 3339 with fractional seconds.", alias="LastRequestTime") additional_properties: Dict[str, Any] = {} diff --git a/generated/shieldlabs_generated/models/multiaccount_changed_data.py b/generated/shieldlabs_generated/models/multiaccount_changed_data.py new file mode 100644 index 0000000..42ae8a2 --- /dev/null +++ b/generated/shieldlabs_generated/models/multiaccount_changed_data.py @@ -0,0 +1,214 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from uuid import UUID +from shieldlabs_generated.models.multiaccount_changed_data_evidence_summary import MultiaccountChangedDataEvidenceSummary +from shieldlabs_generated.models.multiaccount_changed_data_source import MultiaccountChangedDataSource +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class MultiaccountChangedData(BaseModel): + """ + MultiaccountChangedData + """ # noqa: E501 + kind: Optional[Any] + domain: Annotated[str, Field(min_length=1, strict=True)] + action: StrictStr + status: Optional[Any] = Field(description="The result comes from a complete, verified detector source cut. Pending or unavailable work does not produce this event.") + detected: StrictBool + level: Optional[StrictStr] + cluster_id: UUID = Field(description="Persistent site-qualified group identity. Unambiguous expansion or contraction preserves this ID; ambiguous split/merge starts new identities.") + revision: Annotated[str, Field(strict=True)] = Field(description="Publication version of this transition, as a decimal string to preserve UInt64 precision.") + previous_revision: Annotated[str, Field(strict=True)] = Field(description="Previous transition version while the group was active; null for a newly detected or reappearing group.") + user_hids: Annotated[List[Annotated[str, Field(min_length=1, strict=True)]], Field(min_length=3)] = Field(description="Complete sorted group membership. Users from another site are never included. This list is not truncated.") + added_user_hids: List[Annotated[str, Field(min_length=1, strict=True)]] = Field(description="Sorted members added since the previous transition; all current members for detected.") + removed_user_hids: List[Annotated[str, Field(min_length=1, strict=True)]] = Field(description="Sorted members removed since the previous transition; all previous members for resolved.") + users_count: Annotated[int, Field(strict=True, ge=3)] = Field(description="Equal to the length of user_hids.") + members_complete: Optional[Any] + evaluated_at: datetime = Field(description="RFC 3339 timestamp in UTC with up to 9 fractional digits (trailing zeros trimmed), for example `2026-09-30T12:34:57.482913041Z`. Parse it with a parser that accepts nanoseconds.") + rules_version: Annotated[str, Field(min_length=1, strict=True)] + source: MultiaccountChangedDataSource + evidence_summary: MultiaccountChangedDataEvidenceSummary + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["kind", "domain", "action", "status", "detected", "level", "cluster_id", "revision", "previous_revision", "user_hids", "added_user_hids", "removed_user_hids", "users_count", "members_complete", "evaluated_at", "rules_version", "source", "evidence_summary"] + + @field_validator('action') + def action_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['detected', 'updated', 'resolved']): + raise ValueError("must be one of enum values ('detected', 'updated', 'resolved')") + return value + + @field_validator('level') + def level_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set(['medium', 'high']): + raise ValueError("must be one of enum values ('medium', 'high')") + return value + + @field_validator('revision') + def revision_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^[1-9][0-9]{0,19}$", value): + raise ValueError(r"must validate the regular expression /^[1-9][0-9]{0,19}$/") + return value + + @field_validator('previous_revision') + def previous_revision_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^(0|[1-9][0-9]{0,19})$", value): + raise ValueError(r"must validate the regular expression /^(0|[1-9][0-9]{0,19})$/") + return value + + @field_validator('evaluated_at') + def evaluated_at_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?Z$", value): + raise ValueError(r"must validate the regular expression /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?Z$/") + return value + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MultiaccountChangedData from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of source + if self.source: + _dict['source'] = self.source.to_dict() + # override the default output from pydantic by calling `to_dict()` of evidence_summary + if self.evidence_summary: + _dict['evidence_summary'] = self.evidence_summary.to_dict() + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + # set to None if kind (nullable) is None + # and model_fields_set contains the field + if self.kind is None and "kind" in self.model_fields_set: + _dict['kind'] = None + + # set to None if status (nullable) is None + # and model_fields_set contains the field + if self.status is None and "status" in self.model_fields_set: + _dict['status'] = None + + # set to None if level (nullable) is None + # and model_fields_set contains the field + if self.level is None and "level" in self.model_fields_set: + _dict['level'] = None + + # set to None if members_complete (nullable) is None + # and model_fields_set contains the field + if self.members_complete is None and "members_complete" in self.model_fields_set: + _dict['members_complete'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MultiaccountChangedData from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "kind": obj.get("kind"), + "domain": obj.get("domain"), + "action": obj.get("action"), + "status": obj.get("status"), + "detected": obj.get("detected"), + "level": obj.get("level"), + "cluster_id": obj.get("cluster_id"), + "revision": obj.get("revision"), + "previous_revision": obj.get("previous_revision"), + "user_hids": obj.get("user_hids"), + "added_user_hids": obj.get("added_user_hids"), + "removed_user_hids": obj.get("removed_user_hids"), + "users_count": obj.get("users_count"), + "members_complete": obj.get("members_complete"), + "evaluated_at": obj.get("evaluated_at"), + "rules_version": obj.get("rules_version"), + "source": MultiaccountChangedDataSource.from_dict(obj["source"]) if obj.get("source") is not None else None, + "evidence_summary": MultiaccountChangedDataEvidenceSummary.from_dict(obj["evidence_summary"]) if obj.get("evidence_summary") is not None else None + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/multiaccount_changed_data_evidence_summary.py b/generated/shieldlabs_generated/models/multiaccount_changed_data_evidence_summary.py new file mode 100644 index 0000000..163d145 --- /dev/null +++ b/generated/shieldlabs_generated/models/multiaccount_changed_data_evidence_summary.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class MultiaccountChangedDataEvidenceSummary(BaseModel): + """ + MultiaccountChangedDataEvidenceSummary + """ # noqa: E501 + basis_types: List[StrictStr] + rules: List[StrictStr] + detail_truncated: StrictBool = Field(description="Whether the detector's underlying detailed evidence was truncated. Group membership remains complete.") + __properties: ClassVar[List[str]] = ["basis_types", "rules", "detail_truncated"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MultiaccountChangedDataEvidenceSummary from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MultiaccountChangedDataEvidenceSummary from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "basis_types": obj.get("basis_types"), + "rules": obj.get("rules"), + "detail_truncated": obj.get("detail_truncated") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/multiaccount_changed_data_source.py b/generated/shieldlabs_generated/models/multiaccount_changed_data_source.py new file mode 100644 index 0000000..7cfc63c --- /dev/null +++ b/generated/shieldlabs_generated/models/multiaccount_changed_data_source.py @@ -0,0 +1,119 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class MultiaccountChangedDataSource(BaseModel): + """ + MultiaccountChangedDataSource + """ # noqa: E501 + publication_epoch: Optional[Any] + publication_version: Annotated[str, Field(strict=True)] = Field(description="The exact immutable publication containing this result and event.") + coverage_id: Annotated[str, Field(strict=True)] = Field(description="Immutable binding to the trusted source completeness proof.") + __properties: ClassVar[List[str]] = ["publication_epoch", "publication_version", "coverage_id"] + + @field_validator('publication_version') + def publication_version_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^[1-9][0-9]{0,19}$", value): + raise ValueError(r"must validate the regular expression /^[1-9][0-9]{0,19}$/") + return value + + @field_validator('coverage_id') + def coverage_id_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^[a-f0-9]{64}$", value): + raise ValueError(r"must validate the regular expression /^[a-f0-9]{64}$/") + return value + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MultiaccountChangedDataSource from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if publication_epoch (nullable) is None + # and model_fields_set contains the field + if self.publication_epoch is None and "publication_epoch" in self.model_fields_set: + _dict['publication_epoch'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MultiaccountChangedDataSource from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "publication_epoch": obj.get("publication_epoch"), + "publication_version": obj.get("publication_version"), + "coverage_id": obj.get("coverage_id") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/multiaccount_changed_event.py b/generated/shieldlabs_generated/models/multiaccount_changed_event.py new file mode 100644 index 0000000..a62e2ff --- /dev/null +++ b/generated/shieldlabs_generated/models/multiaccount_changed_event.py @@ -0,0 +1,148 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from shieldlabs_generated.models.multiaccount_changed_data import MultiaccountChangedData +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class MultiaccountChangedEvent(BaseModel): + """ + An immutable opt-in notification of a complete multi-account group transition. + """ # noqa: E501 + event_id: Annotated[str, Field(strict=True)] = Field(description="Stable logical event ID. Delivery retries preserve this ID and the signed body.") + event_type: Optional[Any] + schema_version: Optional[Any] + site_id: Annotated[int, Field(strict=True, ge=1)] + created_at: datetime = Field(description="RFC 3339 timestamp in UTC with up to 9 fractional digits (trailing zeros trimmed), for example `2026-09-30T12:34:57.482913041Z`. Parse it with a parser that accepts nanoseconds.") + data: MultiaccountChangedData + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["event_id", "event_type", "schema_version", "site_id", "created_at", "data"] + + @field_validator('event_id') + def event_id_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^[a-f0-9]{40}$", value): + raise ValueError(r"must validate the regular expression /^[a-f0-9]{40}$/") + return value + + @field_validator('created_at') + def created_at_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?Z$", value): + raise ValueError(r"must validate the regular expression /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?Z$/") + return value + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MultiaccountChangedEvent from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + # set to None if event_type (nullable) is None + # and model_fields_set contains the field + if self.event_type is None and "event_type" in self.model_fields_set: + _dict['event_type'] = None + + # set to None if schema_version (nullable) is None + # and model_fields_set contains the field + if self.schema_version is None and "schema_version" in self.model_fields_set: + _dict['schema_version'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MultiaccountChangedEvent from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "event_id": obj.get("event_id"), + "event_type": obj.get("event_type"), + "schema_version": obj.get("schema_version"), + "site_id": obj.get("site_id"), + "created_at": obj.get("created_at"), + "data": MultiaccountChangedData.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/risk_event.py b/generated/shieldlabs_generated/models/risk_event.py new file mode 100644 index 0000000..401d461 --- /dev/null +++ b/generated/shieldlabs_generated/models/risk_event.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class RiskEvent(BaseModel): + """ + RiskEvent + """ # noqa: E501 + code: StrictStr = Field(description="Open catalogue of independent source signals. ai_bot and ai_browser carry weight 0; search_bot remains separate. browser_automation carries weight 90 in task 204 releases. UI Good bot and Bad bot groups are not risk event codes. Keep unknown codes and use the payload scoring_version/result_version for historical interpretation.") + detected: StrictBool = Field(description="Final scoring flag. false does not assert that every underlying probe completed.") + weight: StrictInt = Field(description="Catalogue weight, not an additive score. Banned IP 999 is a marker.") + contribution: StrictInt = Field(description="Matching score details, may contain corrections. Never recompute risk_score by summing.") + status: StrictStr = Field(description="The final scoring flag has been evaluated. Probe incompleteness is reported by dedicated risk events.") + __properties: ClassVar[List[str]] = ["code", "detected", "weight", "contribution", "status"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RiskEvent from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RiskEvent from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "code": obj.get("code"), + "detected": obj.get("detected"), + "weight": obj.get("weight"), + "contribution": obj.get("contribution"), + "status": obj.get("status") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/signal.py b/generated/shieldlabs_generated/models/signal.py index 38e490e..02edd4f 100644 --- a/generated/shieldlabs_generated/models/signal.py +++ b/generated/shieldlabs_generated/models/signal.py @@ -27,10 +27,10 @@ class Signal(BaseModel): """ - One weighted risk signal behind the Risk Score. Only signals with a non-zero weight are listed, in scoring order. The same name can appear twice, and weights can be negative. + One public risk signal behind the Risk Score. Fired signals are listed in scoring order, including weight 0 (incognito, search_bot, ai_bot, ai_browser, check_incomplete). Debug rows are omitted. The same name can appear twice, and weights can be negative. """ # noqa: E501 - name: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Signal name. The set is open: new names can appear at any time, so keep unknown names and use them for display and logging only. Known names: - `tor`: the request came through Tor; - `vpn`: a VPN was detected; - `privacy_relay`: a privacy relay such as iCloud Private Relay; - `proxy`: a proxy was detected; - `datacenter_ip`: the IP belongs to a datacenter or hosting range; - `abuser`: the IP has a record of abuse; - `browser_vpn_proxy`: a VPN or proxy inside the browser; - `antidetect_browser`: an anti-detect browser (the matching flag is `anti_detect_browser`); - `proxy_routed_antidetect`: the network check was routed through a proxy in a way typical for anti-detect browsers; - `port_scan_routed_via_proxy`: `proxy_routed_antidetect` carried forward from an earlier identification of the same device and IP; - `browser_automation`: browser automation, for example a WebDriver-controlled browser; - `javascript_disabled`: JavaScript or the browser APIs the checks need were unavailable; - `os_mismatch`: the operating system seen on the network differs from the one the browser reports; - `os_not_detected`: the operating system could not be determined; - `timezone_mismatch`: the browser timezone differs from the IP location timezone; - `stun_not_checked`: the network (STUN) check did not complete; - `stun_late_correction`: a late network result arrived; negative weight that cancels `stun_not_checked`; - `rate_limited`: the rate-limit marker, weight 999. A verdict carried forward from an earlier identification of the same device and IP (for example `antidetect_browser`) keeps a name derived from the original signal and can carry a partial weight.") - weight: StrictInt = Field(description="Points the signal contributed. Can be negative (`stun_late_correction` is -30) and is 999 for `rate_limited`. Weights can change between releases; never add them up yourself.") + name: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Signal name. The set is open: new names can appear at any time, so keep unknown names and use them for display and logging only. Known names: - `tor`: the request came through Tor; - `vpn`: a VPN was detected, including a browser VPN or proxy; - `private_relay`: a privacy relay such as iCloud Private Relay; - `proxy`: a proxy was detected; - `datacenter_ip`: the IP belongs to a datacenter or hosting range; - `abuser`: the IP has a record of abuse; - `browser_vpn_proxy`: a VPN or proxy inside the browser; - `antidetect_browser`: an anti-detect browser (the matching flag is `anti_detect_browser`); - `proxy_routed_antidetect`: the network check was routed through a proxy in a way typical for anti-detect browsers; - `port_scan_routed_via_proxy`: `proxy_routed_antidetect` carried forward from an earlier identification of the same device and IP; - `browser_automation`: browser automation, for example a WebDriver-controlled browser (90 in task 204 scoring releases); - `javascript_disabled`: JavaScript or the browser APIs the checks need were unavailable; - `os_mismatch`: either operating-system consistency detector fired; - `latency_test`: the WebSocket-to-TCP latency test fired; - `os_not_detected`: the operating system could not be determined; - `timezone_mismatch`: the browser timezone differs from the IP location timezone; - `network_check_incomplete`: the network (STUN) check did not complete; - `stun_late_correction`: a late network result arrived; negative weight that cancels `network_check_incomplete`; - `banned_ip`: the rate-limit marker, weight 999. A verdict carried forward from an earlier identification of the same device and IP (for example `antidetect_browser`) keeps a name derived from the original signal and can carry a partial weight.") + weight: StrictInt = Field(description="Points the signal contributed. Can be negative (`stun_late_correction` is -30) and is 999 for `banned_ip`. Weights can change between releases; never add them up yourself.") __properties: ClassVar[List[str]] = ["name", "weight"] model_config = ConfigDict( diff --git a/generated/shieldlabs_generated/models/webhook_client_identity.py b/generated/shieldlabs_generated/models/webhook_client_identity.py new file mode 100644 index 0000000..1fedb0d --- /dev/null +++ b/generated/shieldlabs_generated/models/webhook_client_identity.py @@ -0,0 +1,211 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from shieldlabs_generated.models.webhook_client_identity_assessments_inner import WebhookClientIdentityAssessmentsInner +from shieldlabs_generated.models.webhook_client_identity_claims_inner import WebhookClientIdentityClaimsInner +from shieldlabs_generated.models.webhook_client_identity_evidence_inner import WebhookClientIdentityEvidenceInner +from shieldlabs_generated.models.webhook_client_identity_verified_inner import WebhookClientIdentityVerifiedInner +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class WebhookClientIdentity(BaseModel): + """ + Optional best-effort client attribution for webhooks. The object and every nested field may be omitted or null. Availability and completeness are not guaranteed; integrations must work without this block or any of its fields. Missing or null values do not mean human or a negative verification result. Claimed names are unverified; a verified provider does not prove a service or agent. Unknown fields and string values must be accepted. + """ # noqa: E501 + schema_version: Optional[StrictStr] = Field(default=None, description="Open version identifier. Version 1 is currently emitted.") + classification_revision: Optional[Annotated[int, Field(strict=True, ge=1)]] = None + classifier_version: Optional[StrictStr] = None + registry_revision: Optional[StrictStr] = None + availability: Optional[StrictStr] = Field(default=None, description="available or input_unavailable. Missing object means no stored identity data; it does not mean human.") + observed_at: Optional[datetime] = None + decided_at: Optional[datetime] = None + claims: Optional[List[WebhookClientIdentityClaimsInner]] = None + verified: Optional[List[WebhookClientIdentityVerifiedInner]] = None + assessments: Optional[List[WebhookClientIdentityAssessmentsInner]] = None + evidence: Optional[List[WebhookClientIdentityEvidenceInner]] = None + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["schema_version", "classification_revision", "classifier_version", "registry_revision", "availability", "observed_at", "decided_at", "claims", "verified", "assessments", "evidence"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of WebhookClientIdentity from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in claims (list) + _items = [] + if self.claims: + for _item_claims in self.claims: + if _item_claims: + _items.append(_item_claims.to_dict()) + _dict['claims'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in verified (list) + _items = [] + if self.verified: + for _item_verified in self.verified: + if _item_verified: + _items.append(_item_verified.to_dict()) + _dict['verified'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in assessments (list) + _items = [] + if self.assessments: + for _item_assessments in self.assessments: + if _item_assessments: + _items.append(_item_assessments.to_dict()) + _dict['assessments'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in evidence (list) + _items = [] + if self.evidence: + for _item_evidence in self.evidence: + if _item_evidence: + _items.append(_item_evidence.to_dict()) + _dict['evidence'] = _items + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + # set to None if schema_version (nullable) is None + # and model_fields_set contains the field + if self.schema_version is None and "schema_version" in self.model_fields_set: + _dict['schema_version'] = None + + # set to None if classification_revision (nullable) is None + # and model_fields_set contains the field + if self.classification_revision is None and "classification_revision" in self.model_fields_set: + _dict['classification_revision'] = None + + # set to None if classifier_version (nullable) is None + # and model_fields_set contains the field + if self.classifier_version is None and "classifier_version" in self.model_fields_set: + _dict['classifier_version'] = None + + # set to None if registry_revision (nullable) is None + # and model_fields_set contains the field + if self.registry_revision is None and "registry_revision" in self.model_fields_set: + _dict['registry_revision'] = None + + # set to None if availability (nullable) is None + # and model_fields_set contains the field + if self.availability is None and "availability" in self.model_fields_set: + _dict['availability'] = None + + # set to None if observed_at (nullable) is None + # and model_fields_set contains the field + if self.observed_at is None and "observed_at" in self.model_fields_set: + _dict['observed_at'] = None + + # set to None if decided_at (nullable) is None + # and model_fields_set contains the field + if self.decided_at is None and "decided_at" in self.model_fields_set: + _dict['decided_at'] = None + + # set to None if claims (nullable) is None + # and model_fields_set contains the field + if self.claims is None and "claims" in self.model_fields_set: + _dict['claims'] = None + + # set to None if verified (nullable) is None + # and model_fields_set contains the field + if self.verified is None and "verified" in self.model_fields_set: + _dict['verified'] = None + + # set to None if assessments (nullable) is None + # and model_fields_set contains the field + if self.assessments is None and "assessments" in self.model_fields_set: + _dict['assessments'] = None + + # set to None if evidence (nullable) is None + # and model_fields_set contains the field + if self.evidence is None and "evidence" in self.model_fields_set: + _dict['evidence'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of WebhookClientIdentity from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "schema_version": obj.get("schema_version"), + "classification_revision": obj.get("classification_revision"), + "classifier_version": obj.get("classifier_version"), + "registry_revision": obj.get("registry_revision"), + "availability": obj.get("availability"), + "observed_at": obj.get("observed_at"), + "decided_at": obj.get("decided_at"), + "claims": [WebhookClientIdentityClaimsInner.from_dict(_item) for _item in obj["claims"]] if obj.get("claims") is not None else None, + "verified": [WebhookClientIdentityVerifiedInner.from_dict(_item) for _item in obj["verified"]] if obj.get("verified") is not None else None, + "assessments": [WebhookClientIdentityAssessmentsInner.from_dict(_item) for _item in obj["assessments"]] if obj.get("assessments") is not None else None, + "evidence": [WebhookClientIdentityEvidenceInner.from_dict(_item) for _item in obj["evidence"]] if obj.get("evidence") is not None else None + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/webhook_client_identity_assessments_inner.py b/generated/shieldlabs_generated/models/webhook_client_identity_assessments_inner.py new file mode 100644 index 0000000..86053ad --- /dev/null +++ b/generated/shieldlabs_generated/models/webhook_client_identity_assessments_inner.py @@ -0,0 +1,121 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class WebhookClientIdentityAssessmentsInner(BaseModel): + """ + WebhookClientIdentityAssessmentsInner + """ # noqa: E501 + candidate_profile_id: Optional[StrictStr] = None + status: Optional[StrictStr] = None + reason: Optional[StrictStr] = None + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["candidate_profile_id", "status", "reason"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of WebhookClientIdentityAssessmentsInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + # set to None if candidate_profile_id (nullable) is None + # and model_fields_set contains the field + if self.candidate_profile_id is None and "candidate_profile_id" in self.model_fields_set: + _dict['candidate_profile_id'] = None + + # set to None if status (nullable) is None + # and model_fields_set contains the field + if self.status is None and "status" in self.model_fields_set: + _dict['status'] = None + + # set to None if reason (nullable) is None + # and model_fields_set contains the field + if self.reason is None and "reason" in self.model_fields_set: + _dict['reason'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of WebhookClientIdentityAssessmentsInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "candidate_profile_id": obj.get("candidate_profile_id"), + "status": obj.get("status"), + "reason": obj.get("reason") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/webhook_client_identity_claims_inner.py b/generated/shieldlabs_generated/models/webhook_client_identity_claims_inner.py new file mode 100644 index 0000000..5411962 --- /dev/null +++ b/generated/shieldlabs_generated/models/webhook_client_identity_claims_inner.py @@ -0,0 +1,149 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class WebhookClientIdentityClaimsInner(BaseModel): + """ + WebhookClientIdentityClaimsInner + """ # noqa: E501 + profile_id: Optional[StrictStr] = None + provider_id: Optional[StrictStr] = None + provider_name: Optional[StrictStr] = None + agent_name: Optional[StrictStr] = None + client_kind: Optional[StrictStr] = None + purpose: Optional[StrictStr] = None + source: Optional[StrictStr] = None + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["profile_id", "provider_id", "provider_name", "agent_name", "client_kind", "purpose", "source"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of WebhookClientIdentityClaimsInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + # set to None if profile_id (nullable) is None + # and model_fields_set contains the field + if self.profile_id is None and "profile_id" in self.model_fields_set: + _dict['profile_id'] = None + + # set to None if provider_id (nullable) is None + # and model_fields_set contains the field + if self.provider_id is None and "provider_id" in self.model_fields_set: + _dict['provider_id'] = None + + # set to None if provider_name (nullable) is None + # and model_fields_set contains the field + if self.provider_name is None and "provider_name" in self.model_fields_set: + _dict['provider_name'] = None + + # set to None if agent_name (nullable) is None + # and model_fields_set contains the field + if self.agent_name is None and "agent_name" in self.model_fields_set: + _dict['agent_name'] = None + + # set to None if client_kind (nullable) is None + # and model_fields_set contains the field + if self.client_kind is None and "client_kind" in self.model_fields_set: + _dict['client_kind'] = None + + # set to None if purpose (nullable) is None + # and model_fields_set contains the field + if self.purpose is None and "purpose" in self.model_fields_set: + _dict['purpose'] = None + + # set to None if source (nullable) is None + # and model_fields_set contains the field + if self.source is None and "source" in self.model_fields_set: + _dict['source'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of WebhookClientIdentityClaimsInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "profile_id": obj.get("profile_id"), + "provider_id": obj.get("provider_id"), + "provider_name": obj.get("provider_name"), + "agent_name": obj.get("agent_name"), + "client_kind": obj.get("client_kind"), + "purpose": obj.get("purpose"), + "source": obj.get("source") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/webhook_client_identity_evidence_inner.py b/generated/shieldlabs_generated/models/webhook_client_identity_evidence_inner.py new file mode 100644 index 0000000..756c073 --- /dev/null +++ b/generated/shieldlabs_generated/models/webhook_client_identity_evidence_inner.py @@ -0,0 +1,171 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class WebhookClientIdentityEvidenceInner(BaseModel): + """ + WebhookClientIdentityEvidenceInner + """ # noqa: E501 + id: Optional[StrictStr] = None + method: Optional[StrictStr] = None + source_id: Optional[StrictStr] = None + source_revision: Optional[StrictStr] = None + checked_at: Optional[datetime] = None + evaluated_at: Optional[datetime] = None + covered_attributes: Optional[List[Optional[StrictStr]]] = None + covered_components: Optional[List[Optional[StrictStr]]] = None + request_binding: Optional[StrictStr] = None + replay_policy: Optional[StrictStr] = None + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["id", "method", "source_id", "source_revision", "checked_at", "evaluated_at", "covered_attributes", "covered_components", "request_binding", "replay_policy"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of WebhookClientIdentityEvidenceInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + # set to None if id (nullable) is None + # and model_fields_set contains the field + if self.id is None and "id" in self.model_fields_set: + _dict['id'] = None + + # set to None if method (nullable) is None + # and model_fields_set contains the field + if self.method is None and "method" in self.model_fields_set: + _dict['method'] = None + + # set to None if source_id (nullable) is None + # and model_fields_set contains the field + if self.source_id is None and "source_id" in self.model_fields_set: + _dict['source_id'] = None + + # set to None if source_revision (nullable) is None + # and model_fields_set contains the field + if self.source_revision is None and "source_revision" in self.model_fields_set: + _dict['source_revision'] = None + + # set to None if checked_at (nullable) is None + # and model_fields_set contains the field + if self.checked_at is None and "checked_at" in self.model_fields_set: + _dict['checked_at'] = None + + # set to None if evaluated_at (nullable) is None + # and model_fields_set contains the field + if self.evaluated_at is None and "evaluated_at" in self.model_fields_set: + _dict['evaluated_at'] = None + + # set to None if covered_attributes (nullable) is None + # and model_fields_set contains the field + if self.covered_attributes is None and "covered_attributes" in self.model_fields_set: + _dict['covered_attributes'] = None + + # set to None if covered_components (nullable) is None + # and model_fields_set contains the field + if self.covered_components is None and "covered_components" in self.model_fields_set: + _dict['covered_components'] = None + + # set to None if request_binding (nullable) is None + # and model_fields_set contains the field + if self.request_binding is None and "request_binding" in self.model_fields_set: + _dict['request_binding'] = None + + # set to None if replay_policy (nullable) is None + # and model_fields_set contains the field + if self.replay_policy is None and "replay_policy" in self.model_fields_set: + _dict['replay_policy'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of WebhookClientIdentityEvidenceInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "id": obj.get("id"), + "method": obj.get("method"), + "source_id": obj.get("source_id"), + "source_revision": obj.get("source_revision"), + "checked_at": obj.get("checked_at"), + "evaluated_at": obj.get("evaluated_at"), + "covered_attributes": obj.get("covered_attributes"), + "covered_components": obj.get("covered_components"), + "request_binding": obj.get("request_binding"), + "replay_policy": obj.get("replay_policy") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/webhook_client_identity_verified_inner.py b/generated/shieldlabs_generated/models/webhook_client_identity_verified_inner.py new file mode 100644 index 0000000..dc11b28 --- /dev/null +++ b/generated/shieldlabs_generated/models/webhook_client_identity_verified_inner.py @@ -0,0 +1,121 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class WebhookClientIdentityVerifiedInner(BaseModel): + """ + WebhookClientIdentityVerifiedInner + """ # noqa: E501 + subject: Optional[StrictStr] = Field(default=None, description="The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes.") + value_id: Optional[StrictStr] = None + evidence_ids: Optional[List[Optional[StrictStr]]] = None + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["subject", "value_id", "evidence_ids"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of WebhookClientIdentityVerifiedInner from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + * Fields in `self.additional_properties` are added to the output dict. + """ + excluded_fields: Set[str] = set([ + "additional_properties", + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + + # set to None if subject (nullable) is None + # and model_fields_set contains the field + if self.subject is None and "subject" in self.model_fields_set: + _dict['subject'] = None + + # set to None if value_id (nullable) is None + # and model_fields_set contains the field + if self.value_id is None and "value_id" in self.model_fields_set: + _dict['value_id'] = None + + # set to None if evidence_ids (nullable) is None + # and model_fields_set contains the field + if self.evidence_ids is None and "evidence_ids" in self.model_fields_set: + _dict['evidence_ids'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of WebhookClientIdentityVerifiedInner from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "subject": obj.get("subject"), + "value_id": obj.get("value_id"), + "evidence_ids": obj.get("evidence_ids") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/webhook_ping_event.py b/generated/shieldlabs_generated/models/webhook_ping_event.py index 16e60b9..9defbf7 100644 --- a/generated/shieldlabs_generated/models/webhook_ping_event.py +++ b/generated/shieldlabs_generated/models/webhook_ping_event.py @@ -20,7 +20,7 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from typing import Optional, Set from typing_extensions import Self @@ -31,9 +31,10 @@ class WebhookPingEvent(BaseModel): Body of a `webhook.ping` delivery, sent when you verify an endpoint. It has no `data`. The keys arrive sorted alphabetically and `created_at` has second precision. """ # noqa: E501 event_type: StrictStr = Field(description="Event type.") - schema_version: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Version of the webhook payload contract. Every event sent today carries `2026-06-01`. Accept other values, so that a future version does not break your handler.") + schema_version: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Webhook contract version. Scored release 2026-10-13 (20 public signals); 2026-10-12 keeps 21 independent source signals; multi-account group release 2026-10-07; parsers also accept legacy 2026-06-01.") created_at: datetime = Field(description="When the ping was sent, with second precision.") - __properties: ClassVar[List[str]] = ["event_type", "schema_version", "created_at"] + event_id: Optional[Annotated[str, Field(min_length=1, strict=True)]] = None + __properties: ClassVar[List[str]] = ["event_type", "schema_version", "created_at", "event_id"] @field_validator('event_type') def event_type_validate_enum(cls, value): @@ -105,7 +106,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "event_type": obj.get("event_type"), "schema_version": obj.get("schema_version"), - "created_at": obj.get("created_at") + "created_at": obj.get("created_at"), + "event_id": obj.get("event_id") }) return _obj diff --git a/resources/shieldlabs-api.yaml b/resources/shieldlabs-api.yaml index a5c763e..53c2531 100644 --- a/resources/shieldlabs-api.yaml +++ b/resources/shieldlabs-api.yaml @@ -11,8 +11,7 @@ info: ## How it fits 1. **Browser.** The ShieldLabs agent, loaded from `cdn.shieldlabs.ai`, runs an identification - and hands your page a request ID. The browser never receives a Risk Score, a visitor ID or - a device ID. + and hands your page an Identification ID. 2. **Your backend.** Your page sends the request ID along with the protected action (signup, login, checkout). Your backend reads the verdict for it from the **History API**, or receives it in a signed `identification.scored` **webhook**. @@ -20,10 +19,7 @@ info: and the identifiers, for example by counting how many accounts one `device_id` has used (skipping the `user_hid` values that do not identify a user, listed under Identifiers). - Scoring is asynchronous. The webhook usually arrives about 300 ms after the browser check; when - follow-up network checks run, it is sent when they finish, at most about 10 seconds later. The - History row appears about 1-3 seconds after the browser call and can be refined for up to about - 10 seconds as follow-up checks finish. Start the identification when the user begins the + Scoring is asynchronous. Start the identification when the user begins the protected action (for example when the signup form opens), then either poll the History API by `identification_id` with a short backoff or wait for the webhook. Let one identification authorize one protected action: reject request IDs you have already used and identifications older than your @@ -98,7 +94,7 @@ info: ## Risk Score, risk bands and the 999 marker The Risk Score (`risk_score` on webhooks, `score` in the History API) is an integer from 0 to - 100. Search-engine crawlers always score 0. The three risk bands are computed on your side; no + 100. Recognized good bots, including search bots, AI bots and AI browsers, always score 0. The three risk bands are computed on your side; no band field exists on the wire: | Band | Score | @@ -109,7 +105,7 @@ info: A value above 100 is not a score. **999** is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, - `{"name":"rate_limited","weight":999}`, usually with nil identifiers. Treat every value above + `{"name":"banned_ip","weight":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. @@ -131,19 +127,27 @@ info: ## Webhooks - ShieldLabs sends one signed `POST` for each identification to every enabled endpoint of the - domain. A delivery has a 1-second timeout and is not retried today; a later release adds - retries that resend identical bytes. Answer 2xx within a second, process the event - asynchronously, make the handler idempotent on `data.identification_id`, and use the History API for - guaranteed reads. A History row can be refined after its webhook was sent (its `ver` - increases); the webhook is not sent again. See the `identification.scored` and `webhook.ping` - entries for the signature algorithm. + ShieldLabs creates one logical signed event per identification and enabled endpoint. + Contract `2026-10-13` includes the identification score, named signals, detection flags, + including search bots, AI bots and AI browsers, and account-event evaluations. + ShieldLabs identifies the provider of recognized bots and AI browsers when available. + Multi-account group changes use a separate event. Current user risk categories + are separate from the score of one identification. + Delivery attempts have a 1-second HTTP timeout. Network errors, timeouts, 429 and 5xx + retry with backoff, up to 8 failed sends or a 15-minute retry age; other 4xx are terminal. + Retries preserve the exact signed body and + `event_id`; duplicate deliveries remain possible after an accepted request loses its response. + Verify the signature, durably store the event using its signed `event_id` (legacy fallback: + `data.identification_id`), then answer 2xx within a second and process asynchronously. + Redirects are not followed. A later History refinement does not create a new webhook event. + Use the History API for the latest stored result. See the `identification.scored` and + `webhook.ping` entries for the signature algorithm. ## Compatibility Ignore fields you do not know, keep unknown values of string fields (such as new signal names or channels) instead of failing, and accept webhook `schema_version` values other than - `2026-06-01`. + `2026-10-13` (current) or a supported older version. Start free at https://app.shieldlabs.ai. Guides: https://docs.shieldlabs.ai. contact: @@ -186,8 +190,7 @@ paths: its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row - appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds - as follow-up network checks finish, so start the identification when the user begins the + becomes available asynchronously, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means "not scored yet", never "clean". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as @@ -460,45 +463,69 @@ paths: source: | curl "https://account.shieldlabs.ai/health" webhooks: - identification.scored: + multi-account-changed: post: - operationId: identificationScored + operationId: multiaccountChanged tags: - Webhooks - summary: Identification scored + summary: A multi-account group changed description: |- - Sent to every enabled webhook endpoint of your domain once for each identification, when its - scoring is final: usually about 300 ms after the browser check, and at most about 10 seconds - later when follow-up network checks run. - - **Verify, then parse.** Compute HMAC-SHA256 over the raw request body and compare it with - `X-Shield-Signature` before you parse the JSON: - - key: the endpoint's signing secret as UTF-8 bytes, including the `whsec_` prefix (not hex- - or base64-decoded, not stripped); - - message: the exact bytes received; re-serializing parsed JSON changes them (for example, `&` - arrives escaped as `\u0026`); - - expected header: `sha256=` followed by the lowercase hex digest, compared in constant time. + Opt-in notification when a complete background detector publication detects, updates, or + resolves an exact group of accounts. Enable `multiaccount: true` on the webhook endpoint. + The default is false; existing identification.scored subscriptions are preserved. - There is no timestamp, delivery ID or event-type header. Rotating a secret replaces it at - once, so accept both the old and the new secret until your deployment has switched. + A repeated build with unchanged membership, level, and rules does not produce a new event. + An unambiguous membership expansion or contraction preserves cluster_id and produces updated. + Ambiguous splits or merges resolve replaced groups and detect new identities. Resolved means the group is no longer supported by the current complete source + cut, including evidence expiry; it does not certify that its accounts are safe. - **Respond fast.** Answer any 2xx status within 1 second and process the event asynchronously; - do not redirect. Today each identification is delivered once per endpoint, with no retries. A - later release adds retries that resend identical bytes, so make your handler idempotent on - `data.identification_id`. + Delivery is at least once, with no global ordering guarantee. Save event_id atomically with + your business operation. Within a site, publication epoch, and cluster_id, compare revision + as an integer and ignore older transitions. Verify HMAC-SHA256 over the raw received bytes; + X-Shield-Event-Id is a convenience header, and the signed body is authoritative. - **Latest state.** The event is a snapshot taken when scoring finished. The History row can - still be refined afterwards (its `ver` increases) and no second event is sent. Use the History - API for guaranteed reads and for the latest state. + This event has no identification_id or risk_score. Its source identifies the published group result; + it does not mutate an earlier identification.scored body or an already pinned Console view. + security: [] + parameters: + - $ref: '#/components/parameters/ShieldSignature' + - $ref: '#/components/parameters/ShieldEventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MultiaccountChangedEvent' + examples: + detected: + $ref: '#/components/examples/MultiaccountDetected' + updated: + $ref: '#/components/examples/MultiaccountUpdated' + resolved: + $ref: '#/components/examples/MultiaccountResolved' + responses: + 2XX: + description: Durably accepted by the receiver. The response body is ignored. + 4XX: + description: Terminal delivery failure, except 429 which is retried. + 5XX: + description: Retried within the delivery attempt and age limits. + identification.scored: + post: + operationId: identificationScored + tags: + - Webhooks + summary: Identification scored + description: |- + Sent when an identification is finalized. Risk Score describes this identification. Detection flags include search bots, AI bots and AI browsers. Recognized good bots always have a Risk Score of 0. ShieldLabs identifies the provider when available. - **Test deliveries.** The Test button in the analytics dashboard sends a fixed sample with keys - sorted alphabetically, second-precision timestamps and two-letter country values. Its - `detection_flags` lack `browser_automation` and `search_bot`: parse missing flags as `false`. + Verify HMAC-SHA256 over exact raw bytes with the endpoint secret. Body event_id is signed; X-Shield-Event-Id is its convenience mirror. Persist and deduplicate by event_id before returning 2xx within the 1-second send timeout. Legacy bodies without event_id can use data.identification_id. Retries preserve body and ID. Network/timeouts/429/5xx are retried with backoff up to 8 failed sends or 15 minutes of retry age; exhaustion/permanent 4xx goes ends delivery. Delivery is at-least-once attempts within that window, not guaranteed eternal delivery or exactly-once. Endpoints are independent. Redirects are not followed. - Deliveries are free and do not use your included identifications. + Use History for recovery/latest state; later snapshot corrections do not automatically emit another identification.scored event. security: [] parameters: - $ref: '#/components/parameters/ShieldSignature' + - $ref: '#/components/parameters/ShieldEventId' requestBody: required: true description: The event as compact JSON. Verify the signature over these exact bytes. @@ -513,11 +540,253 @@ webhooks: $ref: '#/components/examples/IdentificationScoredRateLimited' testDelivery: $ref: '#/components/examples/IdentificationScoredTestDelivery' + current: + summary: 20 public detection signals + description: Emitted by Core synthetic fixture export. This example describes serialization, not a live production visit. + value: + event_id: 52102900cd52d25890e83e18bb0bdec95dedea02 + site_id: 7 + event_type: identification.scored + schema_version: '2026-10-13' + created_at: '2026-10-06T00:00:02Z' + data: + hre: + rules_version: fp21-fixture + account_sharing: + cluster_id: 11111111-1111-4111-8111-111111111111 + status: evaluated + level: high + reason: '' + devices: 4 + min_devices: 4 + account_takeover: + cluster_id: 22222222-2222-4222-8222-222222222222 + status: evaluated + level: medium + reason: fixture + impossible_travel: + cluster_id: null + status: evaluated + level: null + reason: below_distance + identification_id: 13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11 + visitor_id: 161dfbad-8e7f-4a6b-9c5d-0e1f2a3b4c5d + device_id: 5eb7fd5c-2a1b-4c3d-9e8f-7a6b5c4d3e2f + session_id: 7a1b2c3d-4e5f-4789-abcd-ef0123456789 + cookie_id: 3f2e1d0c-9b8a-4654-b210-fedcba987654 + user_hid: test-user + domain: example.com + public_ip: + ip: 203.0.113.1 + country: '' + local_ip: + ip: '' + country: '' + connection_type: direct + os: Windows + browser: Chrome + device_type: desktop + traffic_source: + channel: '' + referrer_domain: '' + landing_url: '' + click_id_type: '' + utm_source: '' + utm_medium: '' + utm_campaign: '' + utm_content: '' + utm_term: '' + risk_score: 0 + signals: + - name: incognito + weight: 0 + detection_flags: + device_spoofing: false + latency_test: false + banned_ip: false + vpn: false + private_relay: false + tor: false + proxy: false + os_mismatch: false + os_not_detected: false + timezone_mismatch: false + anti_detect_browser: false + browser_automation: false + ip_mismatch: false + incognito: true + search_bot: false + ai_bot: false + ai_browser: false + javascript_disabled: false + network_check_incomplete: false + check_incomplete: false + observed_at: '2026-10-06T00:00:00Z' + aiBot: + summary: AI crawler flags + description: Emitted by Core synthetic fixture export. This example describes serialization, not a live production visit. + value: + event_id: 52102900cd52d25890e83e18bb0bdec95dedea02 + site_id: 7 + event_type: identification.scored + schema_version: '2026-10-13' + created_at: '2026-10-06T00:00:02Z' + data: + ai_bot_owner: OpenAI + hre: + account_sharing: + cluster_id: null + status: not_applicable + level: null + reason: anonymous_user + account_takeover: + cluster_id: null + status: not_applicable + level: null + reason: anonymous_user + impossible_travel: + cluster_id: null + status: not_applicable + level: null + reason: anonymous_user + identification_id: 13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11 + visitor_id: 161dfbad-8e7f-4a6b-9c5d-0e1f2a3b4c5d + device_id: 5eb7fd5c-2a1b-4c3d-9e8f-7a6b5c4d3e2f + session_id: 7a1b2c3d-4e5f-4789-abcd-ef0123456789 + cookie_id: 3f2e1d0c-9b8a-4654-b210-fedcba987654 + user_hid: anonymous + domain: example.com + public_ip: + ip: 203.0.113.1 + country: '' + local_ip: + ip: '' + country: '' + connection_type: direct + os: Windows + browser: Chrome + device_type: desktop + traffic_source: + channel: '' + referrer_domain: '' + landing_url: '' + click_id_type: '' + utm_source: '' + utm_medium: '' + utm_campaign: '' + utm_content: '' + utm_term: '' + risk_score: 0 + signals: + - name: ai_bot + weight: 0 + detection_flags: + device_spoofing: false + latency_test: false + banned_ip: false + vpn: false + private_relay: false + tor: false + proxy: false + os_mismatch: false + os_not_detected: false + timezone_mismatch: false + anti_detect_browser: false + browser_automation: false + ip_mismatch: false + incognito: false + search_bot: false + ai_bot: true + ai_browser: false + javascript_disabled: false + network_check_incomplete: false + check_incomplete: false + observed_at: '2026-10-06T00:00:00Z' + aiBrowser: + summary: Verified browser infrastructure flags + description: Emitted by Core synthetic fixture export. This example describes serialization, not a live production visit. + value: + event_id: 52102900cd52d25890e83e18bb0bdec95dedea02 + site_id: 7 + event_type: identification.scored + schema_version: '2026-10-13' + created_at: '2026-10-06T00:00:02Z' + data: + ai_browser_owner: Browserbase + hre: + account_sharing: + cluster_id: null + status: not_applicable + level: null + reason: anonymous_user + account_takeover: + cluster_id: null + status: not_applicable + level: null + reason: anonymous_user + impossible_travel: + cluster_id: null + status: not_applicable + level: null + reason: anonymous_user + identification_id: 13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11 + visitor_id: 161dfbad-8e7f-4a6b-9c5d-0e1f2a3b4c5d + device_id: 5eb7fd5c-2a1b-4c3d-9e8f-7a6b5c4d3e2f + session_id: 7a1b2c3d-4e5f-4789-abcd-ef0123456789 + cookie_id: 3f2e1d0c-9b8a-4654-b210-fedcba987654 + user_hid: anonymous + domain: example.com + public_ip: + ip: 203.0.113.1 + country: '' + local_ip: + ip: '' + country: '' + connection_type: direct + os: Windows + browser: Chrome + device_type: desktop + traffic_source: + channel: '' + referrer_domain: '' + landing_url: '' + click_id_type: '' + utm_source: '' + utm_medium: '' + utm_campaign: '' + utm_content: '' + utm_term: '' + risk_score: 0 + signals: + - name: ai_browser + weight: 0 + detection_flags: + device_spoofing: false + latency_test: false + banned_ip: false + vpn: false + private_relay: false + tor: false + proxy: false + os_mismatch: false + os_not_detected: false + timezone_mismatch: false + anti_detect_browser: false + browser_automation: false + ip_mismatch: false + incognito: false + search_bot: false + ai_bot: false + ai_browser: true + javascript_disabled: false + network_check_incomplete: false + check_incomplete: false + observed_at: '2026-10-06T00:00:00Z' responses: 2XX: description: Delivery accepted. The response body is ignored. 4XX: - description: Delivery rejected, for example with `401` when the signature does not verify. Any status other than 2xx, and a timeout after 1 second, counts as a failed delivery; failed deliveries are not retried today. + description: Permanent 4xx are terminal; 429 is retried. 2xx acknowledges durable acceptance. webhook.ping: post: operationId: webhookPing @@ -541,6 +810,7 @@ webhooks: security: [] parameters: - $ref: '#/components/parameters/ShieldSignature' + - $ref: '#/components/parameters/ShieldEventId' requestBody: required: true description: The ping as compact JSON with sorted keys. @@ -573,7 +843,7 @@ components: in: path required: true description: |- - Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): + Identifier to search by: - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; @@ -582,12 +852,14 @@ components: - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. + `request_id` is a legacy alias of `identification_id` and is still accepted. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. schema: type: string enum: - identification_id + - request_id - device_id - user_hid - visitor_id @@ -687,11 +959,12 @@ components: name: type in: path required: true - description: Identifier to search by. Other values get a `404` with a bare JSON string such as `"auto is not supported"`. + description: Identifier to search by. `request_id` is a legacy alias of `identification_id`. Other values get a `404` with a bare JSON string such as `"auto is not supported"`. schema: type: string enum: - identification_id + - request_id - device_id - user_hid - visitor_id @@ -747,11 +1020,160 @@ components: examples: identificationScored: summary: Signature of the identification.scored example - value: sha256=397ff9bd26888e9e86addc2d920a8c5b2037251a3a1181f3b4810ca6c5f78062 + value: sha256=ef87f1f65933ed7536236a3b2354c4ff657c6f7e852cb8d5a7a0ba2604951819 webhookPing: summary: Signature of the webhook.ping example value: sha256=ea2685733d254f7028fb031c4214583b0650de01e6c8c93131236024edd9fdd8 + ShieldEventId: + name: X-Shield-Event-Id + in: header + required: false + description: Convenience mirror of signed body event_id. Trust the body after HMAC verification. Absent on older bodies. Retries reuse the logical ID across attempts; it is not a separate delivery-attempt identity. + schema: + type: string + examples: + event: + summary: Logical event identity + value: 79e23184cd0b5aae43125e3b14910f13b107a54b schemas: + ClientIdentity: + type: object + additionalProperties: true + required: + - schema_version + - classification_revision + - classifier_version + - registry_revision + - availability + - observed_at + - decided_at + - claims + - verified + - assessments + - evidence + properties: + schema_version: + type: string + description: Open version identifier. Version 1 is currently emitted. + classification_revision: + type: integer + minimum: 1 + classifier_version: + type: string + registry_revision: + type: string + availability: + type: string + description: available or input_unavailable. Missing object means no stored identity data; it does not mean human. + observed_at: + type: string + format: date-time + decided_at: + type: string + format: date-time + claims: + type: array + items: + type: object + additionalProperties: true + required: + - profile_id + - provider_id + - provider_name + - agent_name + - client_kind + - purpose + - source + properties: + profile_id: + type: string + provider_id: + type: string + provider_name: + type: string + agent_name: + type: string + client_kind: + type: string + purpose: + type: string + source: + type: string + verified: + type: array + items: + type: object + additionalProperties: true + required: + - subject + - value_id + - evidence_ids + properties: + subject: + type: string + description: 'The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes.' + value_id: + type: string + evidence_ids: + type: array + items: + type: string + assessments: + type: array + items: + type: object + additionalProperties: true + required: + - status + - reason + properties: + candidate_profile_id: + type: string + status: + type: string + reason: + type: string + evidence: + type: array + items: + type: object + additionalProperties: true + required: + - id + - method + - source_id + - source_revision + - checked_at + - evaluated_at + - covered_attributes + properties: + id: + type: string + method: + type: string + source_id: + type: string + source_revision: + type: string + checked_at: + type: string + format: date-time + evaluated_at: + type: string + format: date-time + covered_attributes: + type: array + items: + type: string + covered_components: + type: array + items: + type: string + request_binding: + type: string + replay_policy: + type: string + description: Stored ingest attribution, independent of risk score and browser automation. Claimed names are unverified. A verified provider does not verify the agent name, service or AI mode. Read proof scope from each attribution and its referenced evidence. Unknown string values and fields must be accepted. IdentificationId: type: string format: uuid @@ -860,7 +1282,7 @@ components: A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, - `{"name":"rate_limited","weight":999}`, usually with nil identifiers. Treat every value above + `{"name":"banned_ip","weight":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. @@ -1035,6 +1457,8 @@ components: - is_incognito - is_search_bot properties: + client_identity: + $ref: '#/components/schemas/ClientIdentity' identification_id: $ref: '#/components/schemas/IdentificationId' session_id: @@ -1415,12 +1839,6 @@ components: type: string const: ok description: Always `ok` when the service answers. - SchemaVersion: - type: string - minLength: 1 - description: Version of the webhook payload contract. Every event sent today carries `2026-06-01`. Accept other values, so that a future version does not break your handler. - examples: - - '2026-06-01' Rfc3339Timestamp: type: string format: date-time @@ -1429,6 +1847,688 @@ components: examples: - '2026-09-30T12:34:57.482913041Z' - '2026-09-30T12:34:56Z' + MultiaccountChangedData: + type: object + additionalProperties: false + required: + - kind + - domain + - action + - status + - detected + - level + - cluster_id + - revision + - previous_revision + - user_hids + - added_user_hids + - removed_user_hids + - users_count + - members_complete + - evaluated_at + - rules_version + - source + - evidence_summary + properties: + kind: + const: multi_account + domain: + type: string + minLength: 1 + action: + type: string + enum: + - detected + - updated + - resolved + status: + const: evaluated + description: The result comes from a complete, verified detector source cut. Pending or unavailable work does not produce this event. + detected: + type: boolean + level: + type: + - string + - 'null' + enum: + - medium + - high + - null + cluster_id: + type: string + format: uuid + description: Persistent site-qualified group identity. Unambiguous expansion or contraction preserves this ID; ambiguous split/merge starts new identities. + revision: + type: string + pattern: ^[1-9][0-9]{0,19}$ + description: Publication version of this transition, as a decimal string to preserve UInt64 precision. + previous_revision: + type: + - string + - 'null' + pattern: ^(0|[1-9][0-9]{0,19})$ + description: Previous transition version while the group was active; null for a newly detected or reappearing group. + user_hids: + type: array + minItems: 3 + uniqueItems: true + description: Complete sorted group membership. Users from another site are never included. This list is not truncated. + items: + type: string + minLength: 1 + not: + enum: + - anonymous + - '-1' + - fail + - 00000000-0000-0000-0000-000000000000 + added_user_hids: + type: array + uniqueItems: true + description: Sorted members added since the previous transition; all current members for detected. + items: + type: string + minLength: 1 + removed_user_hids: + type: array + uniqueItems: true + description: Sorted members removed since the previous transition; all previous members for resolved. + items: + type: string + minLength: 1 + users_count: + type: integer + minimum: 3 + description: Equal to the length of user_hids. + members_complete: + const: true + evaluated_at: + $ref: '#/components/schemas/Rfc3339Timestamp' + rules_version: + type: string + minLength: 1 + source: + type: object + additionalProperties: false + required: + - publication_epoch + - publication_version + - coverage_id + properties: + publication_epoch: + const: multiaccount-v1 + publication_version: + type: string + pattern: ^[1-9][0-9]{0,19}$ + description: The exact immutable publication containing this result and event. + coverage_id: + type: string + pattern: ^[a-f0-9]{64}$ + description: Immutable binding to the trusted source completeness proof. + evidence_summary: + type: object + additionalProperties: false + required: + - basis_types + - rules + - detail_truncated + properties: + basis_types: + type: array + uniqueItems: true + items: + type: string + rules: + type: array + uniqueItems: true + items: + type: string + detail_truncated: + type: boolean + description: Whether the detector's underlying detailed evidence was truncated. Group membership remains complete. + allOf: + - if: + properties: + action: + const: detected + required: + - action + then: + additionalProperties: true + properties: + previous_revision: + type: 'null' + else: + additionalProperties: true + properties: + previous_revision: + type: string + - if: + properties: + action: + const: resolved + required: + - action + then: + additionalProperties: true + properties: + detected: + const: false + level: + type: 'null' + else: + additionalProperties: true + properties: + detected: + const: true + level: + type: string + enum: + - medium + - high + MultiaccountChangedEvent: + type: object + description: An immutable opt-in notification of a complete multi-account group transition. + additionalProperties: false + required: + - event_id + - event_type + - schema_version + - site_id + - created_at + - data + properties: + event_id: + type: string + pattern: ^[a-f0-9]{40}$ + description: Stable logical event ID. Delivery retries preserve this ID and the signed body. + event_type: + const: hre.multi_account.changed + schema_version: + const: '2026-10-07' + site_id: + type: integer + minimum: 1 + created_at: + $ref: '#/components/schemas/Rfc3339Timestamp' + data: + $ref: '#/components/schemas/MultiaccountChangedData' + LegacyDetectionFlags: + type: object + description: Detection flags in retained webhook and History payloads before 2026-10-12. Missing extension flags are not evaluated negative results. + required: + - vpn + - privacy_relay + - tor + - proxy + - os_mismatch + - os_not_detected + - timezone_mismatch + - anti_detect_browser + - browser_automation + - ip_mismatch + - incognito + - search_bot + - javascript_disabled + - stun_not_checked + - check_incomplete + properties: + vpn: + type: boolean + description: A VPN or Browser VPN/Proxy was detected. + privacy_relay: + type: boolean + description: A privacy relay such as iCloud Private Relay was detected. + browser_vpn_proxy: + type: boolean + deprecated: true + description: A VPN or proxy built into the browser or one of its extensions. `true` exactly when `connection_type` is `browser_vpn_proxy`. + tor: + type: boolean + description: The request came through the Tor network. + proxy: + type: boolean + description: A proxy was detected. + datacenter_ip: + type: boolean + deprecated: true + description: The public IP belongs to a datacenter or hosting range. + abuser: + type: boolean + deprecated: true + description: The public IP has a record of abuse in IP intelligence. + os_mismatch: + type: boolean + description: Either internal OS consistency check detected a mismatch. The public flag combines browser/network and TCP behaviour checks. + os_not_detected: + type: boolean + description: The operating system could not be determined from the User-Agent or the network. + timezone_mismatch: + type: boolean + description: The browser timezone differs from the timezone of the IP location. + anti_detect_browser: + type: boolean + description: An anti-detect browser was detected. + browser_automation: + type: boolean + description: Browser automation was detected, for example a WebDriver-controlled browser. Catalogue weight is 90 for task 204 scoring releases; historical result versions may retain the earlier weight 60. + ip_mismatch: + type: boolean + description: A qualifying mismatch between the public IP and the browser's network evidence was detected. Read the returned Risk Score and signal contributions. + incognito: + type: boolean + description: The browser runs in a private window. + search_bot: + type: boolean + description: A search-engine crawler. Its Risk Score is always 0. + suspicious_paid_click: + type: boolean + deprecated: true + description: The visit came from a paid ad click (Google Ads, Meta, TikTok, Microsoft Ads, LinkedIn, Pinterest or X) and the Risk Score is 60 or more (the 999 marker included). + javascript_disabled: + type: boolean + description: JavaScript, or the browser APIs the checks need, were unavailable. + stun_not_checked: + type: boolean + description: The browser network (STUN) check did not complete. Cleared again when a late network result arrives. + check_incomplete: + type: boolean + description: Part of the browser checks timed out, so the verdict rests on partial data. Informational. + device_spoofing: + type: boolean + latency_test: + type: boolean + description: Current public name of the latency detector. Payloads from schema 2026-10-13 before the slug rename used ws_tcp_latency. + ws_tcp_latency: + type: boolean + description: Previous public name of the latency detector. Current 2026-10-13 payloads use latency_test. + banned_ip: + type: boolean + description: Current public name of the banned-IP flag. Payloads from schema 2026-10-13 before the slug rename used rate_limited. + rate_limited: + type: boolean + description: Previous public name of the banned-IP flag. Current 2026-10-13 payloads use banned_ip. + ai_bot: + type: boolean + description: An accepted bot is classified as AI training or user-requested fetch. ChatGPT-User, Claude-User and Perplexity-User are AI bots, not AI browsers. Weight 0. The flag is optional on older payloads; its absence is not an evaluated negative result. Provider claims alone do not grant zero risk. + ai_browser: + type: boolean + description: A browser is identified by separate verified browser infrastructure. A generic browser User-Agent or a verified provider alone is insufficient. Weight 0. Optional on older payloads; absence is not an evaluated negative. + os_mismatch2: + type: boolean + deprecated: true + description: Legacy 2026-10-06 only; current payloads combine this check into os_mismatch. + DetectionFlags20261012: + type: object + description: Retained scored webhook contract 2026-10-12. Twenty-one source detection signals, with os_mismatch and os_mismatch2 independent. Current deliveries use DetectionFlags (2026-10-13). + required: + - vpn + - proxy + - privacy_relay + - check_incomplete + - stun_not_checked + - os_not_detected + - timezone_mismatch + - javascript_disabled + - os_mismatch + - os_mismatch2 + - ip_mismatch + - latency_test + - anti_detect_browser + - device_spoofing + - banned_ip + - incognito + - search_bot + - ai_bot + - browser_automation + - ai_browser + - tor + properties: + vpn: + type: boolean + description: A VPN or Browser VPN/Proxy was detected. + proxy: + type: boolean + description: A proxy was detected. + privacy_relay: + type: boolean + description: A privacy relay such as iCloud Private Relay was detected. + check_incomplete: + type: boolean + description: Part of the browser checks timed out, so the verdict rests on partial data. Informational. + stun_not_checked: + type: boolean + description: The browser network (STUN) check did not complete. Cleared again when a late network result arrives. + os_not_detected: + type: boolean + description: The operating system could not be determined from the User-Agent or the network. + timezone_mismatch: + type: boolean + description: The browser timezone differs from the timezone of the IP location. + javascript_disabled: + type: boolean + description: JavaScript, or the browser APIs the checks need, were unavailable. + os_mismatch: + type: boolean + description: The first operating system consistency detector found a mismatch. Independent of os_mismatch2. + os_mismatch2: + type: boolean + description: The second operating system consistency detector (TCP behaviour) found a mismatch. Independent of os_mismatch. + ip_mismatch: + type: boolean + description: A qualifying mismatch between the public IP and the browser's network evidence was detected. Read the returned Risk Score and signal contributions. + latency_test: + type: boolean + anti_detect_browser: + type: boolean + description: An anti-detect browser was detected. + device_spoofing: + type: boolean + banned_ip: + type: boolean + incognito: + type: boolean + description: The browser runs in a private window. + search_bot: + type: boolean + description: A search-engine crawler. Its Risk Score is always 0. + ai_bot: + type: boolean + description: An already accepted crawler has purpose training or user_fetch. ChatGPT-User, Claude-User and Perplexity-User are AI bots, not AI browsers. A User-Agent claim alone does not grant an accepted verdict. + browser_automation: + type: boolean + description: Browser automation was detected, for example a WebDriver-controlled browser. Catalogue weight is 90 for task 204 scoring releases; historical result versions may retain the earlier weight 60. + ai_browser: + type: boolean + description: A browser has separately verified infrastructure or service attribution. A User-Agent claim or provider-only IP proof is insufficient. + tor: + type: boolean + description: The request came through the Tor network. + additionalProperties: false + DetectionFlags: + type: object + description: The 20 public detection signals in scored webhook contract 2026-10-13. os_mismatch is true when either OS detector fired. browser_vpn_proxy contributes to vpn and is not its own flag. Groups and owner attribution are separate from this set. + required: + - vpn + - proxy + - private_relay + - check_incomplete + - network_check_incomplete + - os_not_detected + - timezone_mismatch + - javascript_disabled + - os_mismatch + - ip_mismatch + - latency_test + - anti_detect_browser + - device_spoofing + - banned_ip + - incognito + - search_bot + - ai_bot + - browser_automation + - ai_browser + - tor + properties: + vpn: + type: boolean + description: A VPN was detected, including a VPN or proxy built into the browser. connection_type may still be browser_vpn_proxy. + proxy: + type: boolean + description: A proxy was detected. + private_relay: + type: boolean + description: A privacy relay such as iCloud Private Relay was detected. Older payloads used privacy_relay. + check_incomplete: + type: boolean + description: Part of the browser checks timed out, so the verdict rests on partial data. Informational. Weight 0, and present in signals when true. + network_check_incomplete: + type: boolean + description: The browser network (STUN) check did not complete. Cleared again when a late network result arrives. Older payloads used stun_not_checked. + os_not_detected: + type: boolean + description: The operating system could not be determined from the User-Agent or the network. + timezone_mismatch: + type: boolean + description: The browser timezone differs from the timezone of the IP location. + javascript_disabled: + type: boolean + description: JavaScript, or the browser APIs the checks need, were unavailable. + os_mismatch: + type: boolean + description: Either operating-system consistency detector fired. The two detectors are not separate public flags. + ip_mismatch: + type: boolean + description: A qualifying mismatch between the public IP and the browser's network evidence was detected. Nominal weight 45; read the returned Risk Score. + latency_test: + type: boolean + description: The WebSocket-to-TCP latency test fired. Older payloads used ws_tcp_latency. + anti_detect_browser: + type: boolean + description: An anti-detect browser was detected. + device_spoofing: + type: boolean + banned_ip: + type: boolean + description: The visitor IP is banned for this window. The matching signal weight is 999. Older payloads used rate_limited. + incognito: + type: boolean + description: The browser runs in a private window. Weight 0, and present in signals when true. + search_bot: + type: boolean + description: A search-engine crawler. Its Risk Score is always 0. Weight 0, and present in signals when true. + ai_bot: + type: boolean + description: A recognized AI bot or crawler. Its Risk Score is always 0. ShieldLabs identifies the provider when available. Weight 0, and present in signals when true. + browser_automation: + type: boolean + description: Browser automation was detected, for example a WebDriver-controlled browser. Catalogue weight is 90 for task 204 scoring releases; historical result versions may retain the earlier weight 60. + ai_browser: + type: boolean + description: A recognized AI browser. Its Risk Score is always 0. ShieldLabs identifies the provider when available. Weight 0, and present in signals when true. + tor: + type: boolean + description: The request came through the Tor network. + additionalProperties: false + SchemaVersion: + type: string + minLength: 1 + description: Webhook contract version. Scored release 2026-10-13 (20 public signals); 2026-10-12 keeps 21 independent source signals; multi-account group release 2026-10-07; parsers also accept legacy 2026-06-01. + examples: + - '2026-10-13' + - '2026-10-12' + - '2026-10-09' + - '2026-10-08' + - '2026-10-07' + - '2026-10-06' + - '2026-06-01' + WebhookClientIdentity: + type: + - object + - 'null' + additionalProperties: true + properties: + schema_version: + type: + - string + - 'null' + description: Open version identifier. Version 1 is currently emitted. + classification_revision: + type: + - integer + - 'null' + minimum: 1 + classifier_version: + type: + - string + - 'null' + registry_revision: + type: + - string + - 'null' + availability: + type: + - string + - 'null' + description: available or input_unavailable. Missing object means no stored identity data; it does not mean human. + observed_at: + type: + - string + - 'null' + format: date-time + decided_at: + type: + - string + - 'null' + format: date-time + claims: + type: + - array + - 'null' + items: + type: + - object + - 'null' + additionalProperties: true + properties: + profile_id: + type: + - string + - 'null' + provider_id: + type: + - string + - 'null' + provider_name: + type: + - string + - 'null' + agent_name: + type: + - string + - 'null' + client_kind: + type: + - string + - 'null' + purpose: + type: + - string + - 'null' + source: + type: + - string + - 'null' + verified: + type: + - array + - 'null' + items: + type: + - object + - 'null' + additionalProperties: true + properties: + subject: + type: + - string + - 'null' + description: 'The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes.' + value_id: + type: + - string + - 'null' + evidence_ids: + type: + - array + - 'null' + items: + type: + - string + - 'null' + assessments: + type: + - array + - 'null' + items: + type: + - object + - 'null' + additionalProperties: true + properties: + candidate_profile_id: + type: + - string + - 'null' + status: + type: + - string + - 'null' + reason: + type: + - string + - 'null' + evidence: + type: + - array + - 'null' + items: + type: + - object + - 'null' + additionalProperties: true + properties: + id: + type: + - string + - 'null' + method: + type: + - string + - 'null' + source_id: + type: + - string + - 'null' + source_revision: + type: + - string + - 'null' + checked_at: + type: + - string + - 'null' + format: date-time + evaluated_at: + type: + - string + - 'null' + format: date-time + covered_attributes: + type: + - array + - 'null' + items: + type: + - string + - 'null' + covered_components: + type: + - array + - 'null' + items: + type: + - string + - 'null' + request_binding: + type: + - string + - 'null' + replay_policy: + type: + - string + - 'null' + description: Optional best-effort client attribution for webhooks. The object and every nested field may be omitted or null. Availability and completeness are not guaranteed; integrations must work without this block or any of its fields. Missing or null values do not mean human or a negative verification result. Claimed names are unverified; a verified provider does not prove a service or agent. Unknown fields and string values must be accepted. UserHid: type: - string @@ -1521,7 +2621,7 @@ components: - '' Signal: type: object - description: One weighted risk signal behind the Risk Score. Only signals with a non-zero weight are listed, in scoring order. The same name can appear twice, and weights can be negative. + description: One public risk signal behind the Risk Score. Fired signals are listed in scoring order, including weight 0 (incognito, search_bot, ai_bot, ai_browser, check_incomplete). Debug rows are omitted. The same name can appear twice, and weights can be negative. required: - name - weight @@ -1533,8 +2633,8 @@ components: Signal name. The set is open: new names can appear at any time, so keep unknown names and use them for display and logging only. Known names: - `tor`: the request came through Tor; - - `vpn`: a VPN was detected; - - `privacy_relay`: a privacy relay such as iCloud Private Relay; + - `vpn`: a VPN was detected, including a browser VPN or proxy; + - `private_relay`: a privacy relay such as iCloud Private Relay; - `proxy`: a proxy was detected; - `datacenter_ip`: the IP belongs to a datacenter or hosting range; - `abuser`: the IP has a record of abuse; @@ -1544,16 +2644,16 @@ components: for anti-detect browsers; - `port_scan_routed_via_proxy`: `proxy_routed_antidetect` carried forward from an earlier identification of the same device and IP; - - `browser_automation`: browser automation, for example a WebDriver-controlled browser; + - `browser_automation`: browser automation, for example a WebDriver-controlled browser (90 in task 204 scoring releases); - `javascript_disabled`: JavaScript or the browser APIs the checks need were unavailable; - - `os_mismatch`: the operating system seen on the network differs from the one the browser - reports; + - `os_mismatch`: either operating-system consistency detector fired; + - `latency_test`: the WebSocket-to-TCP latency test fired; - `os_not_detected`: the operating system could not be determined; - `timezone_mismatch`: the browser timezone differs from the IP location timezone; - - `stun_not_checked`: the network (STUN) check did not complete; + - `network_check_incomplete`: the network (STUN) check did not complete; - `stun_late_correction`: a late network result arrived; negative weight that cancels - `stun_not_checked`; - - `rate_limited`: the rate-limit marker, weight 999. + `network_check_incomplete`; + - `banned_ip`: the rate-limit marker, weight 999. A verdict carried forward from an earlier identification of the same device and IP (for example `antidetect_browser`) keeps a name derived from the original signal and can carry a @@ -1563,99 +2663,89 @@ components: - antidetect_browser weight: type: integer - description: Points the signal contributed. Can be negative (`stun_late_correction` is -30) and is 999 for `rate_limited`. Weights can change between releases; never add them up yourself. + description: Points the signal contributed. Can be negative (`stun_late_correction` is -30) and is 999 for `banned_ip`. Weights can change between releases; never add them up yourself. examples: - 10 - -30 - DetectionFlags: + RiskEvent: type: object - description: |- - Stable yes/no verdicts for the identification. Always all 19 keys. Branch on these flags and on - the Risk Score; signal names are for display and logging. - - When `search_bot` is `true`, `incognito`, `check_incomplete`, `ip_mismatch` and - `javascript_disabled` are always `false`. required: - - 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 + - code + - detected + - weight + - contribution + - status properties: - vpn: - type: boolean - description: A VPN was detected (scored `vpn` signal). - privacy_relay: - type: boolean - description: A privacy relay such as iCloud Private Relay was detected. - browser_vpn_proxy: - type: boolean - description: A VPN or proxy built into the browser or one of its extensions. `true` exactly when `connection_type` is `browser_vpn_proxy`. - tor: - type: boolean - description: The request came through the Tor network. - proxy: - type: boolean - description: A proxy was detected. - datacenter_ip: - type: boolean - description: The public IP belongs to a datacenter or hosting range. - abuser: - type: boolean - description: The public IP has a record of abuse in IP intelligence. - os_mismatch: - type: boolean - description: The operating system seen on the network differs from the one the browser reports. - os_not_detected: - type: boolean - description: The operating system could not be determined from the User-Agent or the network. - timezone_mismatch: - type: boolean - description: The browser timezone differs from the timezone of the IP location. - anti_detect_browser: - type: boolean - description: An anti-detect browser was detected. - browser_automation: - type: boolean - description: Browser automation was detected, for example a WebDriver-controlled browser. - ip_mismatch: - type: boolean - description: 'The public IP differs from the local IP found by the browser network check. Informational: it does not add to the score.' - incognito: - type: boolean - description: The browser runs in a private window. - search_bot: - type: boolean - description: A search-engine crawler. Its Risk Score is always 0. - suspicious_paid_click: - type: boolean - description: The visit came from a paid ad click (Google Ads, Meta, TikTok, Microsoft Ads, LinkedIn, Pinterest or X) and the Risk Score is 60 or more (the 999 marker included). - javascript_disabled: - type: boolean - description: JavaScript, or the browser APIs the checks need, were unavailable. - stun_not_checked: - type: boolean - description: The browser network (STUN) check did not complete. Cleared again when a late network result arrives. - check_incomplete: + code: + type: string + description: Open catalogue of independent source signals. ai_bot and ai_browser carry weight 0; search_bot remains separate. browser_automation carries weight 90 in task 204 releases. UI Good bot and Bad bot groups are not risk event codes. Keep unknown codes and use the payload scoring_version/result_version for historical interpretation. + detected: type: boolean - description: Part of the browser checks timed out, so the verdict rests on partial data. Informational. + description: Final scoring flag. false does not assert that every underlying probe completed. + weight: + type: integer + description: Catalogue weight, not an additive score. Banned IP 999 is a marker. + contribution: + type: integer + description: Matching score details, may contain corrections. Never recompute risk_score by summing. + status: + type: string + description: The final scoring flag has been evaluated. Probe incompleteness is reported by dedicated risk events. + examples: + - evaluated + HREResult: + type: object + required: + - status + - level + - reason + properties: + status: + type: string + examples: + - evaluated + - not_evaluated + - not_applicable + level: + type: + - string + - 'null' + examples: + - medium + - high + - null + reason: + type: string + devices: + type: integer + minimum: 0 + min_devices: + type: integer + minimum: 1 + cluster_id: + type: + - string + - 'null' + description: Authoritative cluster ID for this HRE result. Null when no cluster was published for the result, including old stored verdicts; never a device ID. Reused on retries. + HRE: + type: object + required: + - account_sharing + - account_takeover + - impossible_travel + properties: + rules_version: + type: string + account_sharing: + $ref: '#/components/schemas/HREResult' + account_takeover: + $ref: '#/components/schemas/HREResult' + impossible_travel: + $ref: '#/components/schemas/HREResult' + description: Three completed on-demand handler results. Technical errors block emission; no_history/skipped are explicit not_evaluated results. Anonymous checks are not_applicable. Multiaccount is separate. IdentificationScoredData: type: object - description: The scored identification. Every key is always present (no key is ever omitted); only `user_hid` can be `null`. + description: Final identification. risk_score is this scan only. Version 2026-10-13 uses signals for score contributions, including fired signals of weight 0, detection_flags for 20 public signals, and hre for account results. Older bodies remain accepted. required: - identification_id - visitor_id @@ -1676,6 +2766,9 @@ components: - detection_flags - observed_at properties: + client_identity: + $ref: '#/components/schemas/WebhookClientIdentity' + description: Optional best-effort attribution. May be omitted or null; every nested field is also optional and nullable. Integrations must work without these values. identification_id: $ref: '#/components/schemas/IdentificationId' visitor_id: @@ -1713,14 +2806,105 @@ components: $ref: '#/components/schemas/RiskScore' signals: type: array - description: Weighted risk signals behind `risk_score`, in scoring order. Can be empty. The rate-limit marker carries exactly one entry, `{"name":"rate_limited","weight":999}`. + description: Weighted risk signals behind `risk_score`, in scoring order. Can be empty. The rate-limit marker carries exactly one entry, `{"name":"banned_ip","weight":999}`. items: $ref: '#/components/schemas/Signal' detection_flags: - $ref: '#/components/schemas/DetectionFlags' + type: object + description: Contract 2026-10-13 requires DetectionFlags (exactly 20 public signals). 2026-10-12 keeps its 21-flag schema. Older versions keep LegacyDetectionFlags. + additionalProperties: true + properties: + vpn: + type: boolean + proxy: + type: boolean + tor: + type: boolean + private_relay: + type: boolean + privacy_relay: + type: boolean + network_check_incomplete: + type: boolean + stun_not_checked: + type: boolean + latency_test: + type: boolean + ws_tcp_latency: + type: boolean + banned_ip: + type: boolean + rate_limited: + type: boolean + check_incomplete: + type: boolean + os_not_detected: + type: boolean + timezone_mismatch: + type: boolean + javascript_disabled: + type: boolean + os_mismatch: + type: boolean + os_mismatch2: + type: boolean + ip_mismatch: + type: boolean + anti_detect_browser: + type: boolean + device_spoofing: + type: boolean + incognito: + type: boolean + search_bot: + type: boolean + ai_bot: + type: boolean + ai_browser: + type: boolean + browser_automation: + type: boolean + browser_vpn_proxy: + type: boolean + datacenter_ip: + type: boolean + abuser: + type: boolean + suspicious_paid_click: + type: boolean observed_at: $ref: '#/components/schemas/Rfc3339Timestamp' - description: When scoring finished and the event was built (not the page view time); identical to the envelope `created_at`. RFC 3339 in UTC with up to 9 fractional digits. + description: Original snapshot scan clock, distinct from envelope created_at. RFC 3339 in UTC with up to 9 fractional digits. + result_version: + deprecated: true + description: Legacy through 2026-10-08 only; omitted by current emitters. + type: string + minLength: 1 + scoring_version: + deprecated: true + type: string + description: Legacy through 2026-10-08 only; omitted by current emitters. + risk_events: + deprecated: true + description: Legacy 2026-10-06 only; absent from current scored events. + type: array + items: + $ref: '#/components/schemas/RiskEvent' + hre: + $ref: '#/components/schemas/HRE' + search_bot_owner: + type: string + description: Owner label from accepted search-bot detection; omitted when unknown or inactive. + examples: + - Google + ai_bot_owner: + type: string + description: Provider company label from accepted AI-bot detection; omitted when unknown or inactive. + examples: + - OpenAI + ai_browser_owner: + type: string + description: Owner label from accepted AI-browser detection; omitted when unknown or inactive. IdentificationScoredEvent: type: object description: 'Body of an `identification.scored` delivery. The signature is not part of the body: it arrives in the `X-Shield-Signature` header.' @@ -1738,9 +2922,320 @@ components: $ref: '#/components/schemas/SchemaVersion' created_at: $ref: '#/components/schemas/Rfc3339Timestamp' - description: When the event was built. Equal to `data.observed_at`. + description: When the event envelope was created, distinct from data.observed_at (scan time). data: $ref: '#/components/schemas/IdentificationScoredData' + event_id: + type: string + minLength: 1 + description: Logical site/request/final-result-version/type identity. Stable across retries and endpoints. + site_id: + type: integer + minimum: 1 + description: Site scope when available; legacy domain-only accounts omit it. + allOf: + - if: + properties: + schema_version: + const: '2026-10-06' + required: + - schema_version + then: + required: + - event_id + properties: + data: + required: + - result_version + - scoring_version + - risk_events + - hre + type: object + additionalProperties: true + properties: + result_version: {} + scoring_version: {} + risk_events: {} + hre: {} + event_id: + type: string + minLength: 1 + description: Logical site/request/final-result-version/type identity. Stable across retries and endpoints. + type: object + additionalProperties: true + - if: + type: object + required: + - schema_version + properties: + schema_version: + enum: + - '2026-10-07' + - '2026-10-08' + - '2026-10-09' + - '2026-10-12' + - '2026-10-13' + additionalProperties: true + then: + type: object + required: + - event_id + properties: + event_id: + type: string + minLength: 1 + description: Logical site/request/final-result-version/type identity. Stable across retries and endpoints. + data: + type: object + required: + - hre + properties: + result_version: {} + scoring_version: {} + risk_events: {} + fingerprint: {} + hre: + type: object + properties: + account_sharing: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + account_takeover: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + impossible_travel: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + additionalProperties: true + additionalProperties: true + not: + anyOf: + - type: object + required: + - risk_events + properties: + risk_events: {} + additionalProperties: true + - type: object + required: + - fingerprint + properties: + fingerprint: {} + additionalProperties: true + additionalProperties: true + - if: + type: object + properties: + schema_version: + enum: + - '2026-10-08' + - '2026-10-09' + - '2026-10-12' + - '2026-10-13' + required: + - schema_version + additionalProperties: true + then: + type: object + properties: + data: + type: object + properties: + connection_type: + type: string + not: + const: browser_vpn_proxy + signals: + type: array + items: + type: object + properties: + name: + type: string + not: + const: browser_vpn_proxy + additionalProperties: true + detection_flags: + type: object + not: + anyOf: + - type: object + required: + - browser_vpn_proxy + properties: + browser_vpn_proxy: {} + additionalProperties: true + - type: object + required: + - datacenter_ip + properties: + datacenter_ip: {} + additionalProperties: true + - type: object + required: + - abuser + properties: + abuser: {} + additionalProperties: true + - type: object + required: + - suspicious_paid_click + properties: + suspicious_paid_click: {} + additionalProperties: true + additionalProperties: true + additionalProperties: true + additionalProperties: true + - if: + type: object + properties: + schema_version: + enum: + - '2026-10-07' + - '2026-10-08' + required: + - schema_version + then: + type: object + properties: + data: + type: object + required: + - result_version + - scoring_version + properties: + result_version: {} + scoring_version: {} + - if: + type: object + properties: + schema_version: + enum: + - '2026-10-09' + - '2026-10-12' + - '2026-10-13' + required: + - schema_version + additionalProperties: true + then: + type: object + properties: + data: + type: object + not: + anyOf: + - type: object + required: + - result_version + properties: + result_version: {} + additionalProperties: true + - type: object + required: + - scoring_version + properties: + scoring_version: {} + additionalProperties: true + additionalProperties: true + additionalProperties: true + - if: + type: object + required: + - schema_version + properties: + schema_version: + enum: + - '2026-10-07' + - '2026-10-08' + - '2026-10-09' + additionalProperties: true + then: + type: object + properties: + data: + type: object + properties: + detection_flags: + type: object + not: + type: object + required: + - os_mismatch2 + properties: + os_mismatch2: {} + additionalProperties: true + additionalProperties: true + additionalProperties: true + additionalProperties: true + - if: + type: object + required: + - schema_version + properties: + schema_version: + not: + enum: + - '2026-10-12' + - '2026-10-13' + additionalProperties: true + then: + type: object + properties: + data: + type: object + properties: + detection_flags: + $ref: '#/components/schemas/LegacyDetectionFlags' + additionalProperties: true + additionalProperties: true + - if: + type: object + required: + - schema_version + properties: + schema_version: + const: '2026-10-12' + additionalProperties: true + then: + type: object + properties: + data: + type: object + properties: + detection_flags: + $ref: '#/components/schemas/DetectionFlags20261012' + additionalProperties: true + additionalProperties: true + - if: + type: object + required: + - schema_version + properties: + schema_version: + const: '2026-10-13' + additionalProperties: true + then: + type: object + properties: + data: + type: object + properties: + detection_flags: + $ref: '#/components/schemas/DetectionFlags' + additionalProperties: true + additionalProperties: true WebhookPingEvent: type: object description: Body of a `webhook.ping` delivery, sent when you verify an endpoint. It has no `data`. The keys arrive sorted alphabetically and `created_at` has second precision. @@ -1758,6 +3253,9 @@ components: created_at: $ref: '#/components/schemas/Rfc3339Timestamp' description: When the ping was sent, with second precision. + event_id: + type: string + minLength: 1 examples: HistoryPage: summary: Five identifications @@ -2151,6 +3649,127 @@ components: description: The only successful answer. value: status: ok + MultiaccountDetected: + summary: Multi-account group detected + description: Generated by the Core event marshaler for a synthetic three-account group. + value: + event_id: 7795f86574643ece49997885d0a0a3e5768dcb74 + site_id: 319 + event_type: hre.multi_account.changed + schema_version: '2026-10-07' + created_at: '2026-10-07T12:01:04Z' + data: + kind: multi_account + domain: example.com + action: detected + status: evaluated + detected: true + level: medium + cluster_id: abc35eb1-2cd7-5c28-a805-f3b7c1682a2f + revision: '7412' + previous_revision: null + added_user_hids: + - account_a + - account_b + - account_c + removed_user_hids: [] + user_hids: + - account_a + - account_b + - account_c + users_count: 3 + members_complete: true + evaluated_at: '2026-10-07T12:01:00Z' + rules_version: fp21-travel-pair-2026-10-06 + source: + publication_epoch: multiaccount-v1 + publication_version: '7412' + coverage_id: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + evidence_summary: + basis_types: + - lineage + rules: + - lineage_60m + detail_truncated: false + MultiaccountUpdated: + summary: Multi-account group updated + description: Generated by the Core event marshaler for a synthetic three-account group. + value: + event_id: 2feaa43f05ad19dadd78c85c465c8b4d101f9e2f + site_id: 319 + event_type: hre.multi_account.changed + schema_version: '2026-10-07' + created_at: '2026-10-07T12:01:04Z' + data: + kind: multi_account + domain: example.com + action: updated + status: evaluated + detected: true + level: high + cluster_id: abc35eb1-2cd7-5c28-a805-f3b7c1682a2f + revision: '7414' + previous_revision: '7412' + added_user_hids: [] + removed_user_hids: [] + user_hids: + - account_a + - account_b + - account_c + users_count: 3 + members_complete: true + evaluated_at: '2026-10-07T12:01:00Z' + rules_version: fp21-travel-pair-2026-10-06 + source: + publication_epoch: multiaccount-v1 + publication_version: '7414' + coverage_id: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + evidence_summary: + basis_types: + - lineage + rules: + - lineage_60m + detail_truncated: false + MultiaccountResolved: + summary: Multi-account group resolved + description: Generated by the Core event marshaler for a synthetic three-account group. + value: + event_id: 1f12f427edb3e4b74df2c0c958db49186ef0a7b9 + site_id: 319 + event_type: hre.multi_account.changed + schema_version: '2026-10-07' + created_at: '2026-10-07T12:01:04Z' + data: + kind: multi_account + domain: example.com + action: resolved + status: evaluated + detected: false + level: null + cluster_id: abc35eb1-2cd7-5c28-a805-f3b7c1682a2f + revision: '7415' + previous_revision: '7412' + added_user_hids: [] + removed_user_hids: + - account_a + - account_b + - account_c + user_hids: + - account_a + - account_b + - account_c + users_count: 3 + members_complete: true + evaluated_at: '2026-10-07T12:01:00Z' + rules_version: fp21-travel-pair-2026-10-06 + source: + publication_epoch: multiaccount-v1 + publication_version: '7415' + coverage_id: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + evidence_summary: + basis_types: [] + rules: [] + detail_truncated: false IdentificationScored: summary: Dangerous identification from a paid click description: Risk Score 80 from a proxy, a datacenter IP and an anti-detect browser, on a visit from a Google Ads click. diff --git a/src/shieldlabs/_models.py b/src/shieldlabs/_models.py index 9abf521..ba83071 100644 --- a/src/shieldlabs/_models.py +++ b/src/shieldlabs/_models.py @@ -73,17 +73,17 @@ class SignalName: PROXY_ROUTED_ANTIDETECT = "proxy_routed_antidetect" PORT_SCAN_ROUTED_VIA_PROXY = "port_scan_routed_via_proxy" BROWSER_AUTOMATION = "browser_automation" - STUN_NOT_CHECKED = "stun_not_checked" + STUN_NOT_CHECKED = "network_check_incomplete" STUN_LATE_CORRECTION = "stun_late_correction" OS_NOT_DETECTED = "os_not_detected" BROWSER_VPN_PROXY = "browser_vpn_proxy" VPN = "vpn" - PRIVACY_RELAY = "privacy_relay" + PRIVACY_RELAY = "private_relay" PROXY = "proxy" DATACENTER_IP = "datacenter_ip" ABUSER = "abuser" TIMEZONE_MISMATCH = "timezone_mismatch" - RATE_LIMITED = "rate_limited" + RATE_LIMITED = "banned_ip" @dataclass(frozen=True) @@ -163,18 +163,18 @@ class DetectionFlags: vpn: bool = False proxy: bool = False - privacy_relay: bool = False + private_relay: bool = False check_incomplete: bool = False - stun_not_checked: bool = False + network_check_incomplete: bool = False os_not_detected: bool = False timezone_mismatch: bool = False javascript_disabled: bool = False os_mismatch: bool = False ip_mismatch: bool = False - ws_tcp_latency: bool = False + latency_test: bool = False anti_detect_browser: bool = False device_spoofing: bool = False - rate_limited: bool = False + banned_ip: bool = False incognito: bool = False search_bot: bool = False ai_bot: bool = False @@ -186,7 +186,18 @@ class DetectionFlags: def from_dict(cls, data: Mapping[str, Any]) -> DetectionFlags: """Build from a webhook ``detection_flags`` object. A missing key is ``False``.""" data = _mapping(data) - return cls(**{key: bool(data.get(key, False)) for key in FLAG_KEYS}) + legacy = { + "private_relay": "privacy_relay", + "network_check_incomplete": "stun_not_checked", + "latency_test": "ws_tcp_latency", + "banned_ip": "rate_limited", + } + return cls( + **{ + key: bool(data.get(key, False) or data.get(legacy.get(key, ""), False)) + for key in FLAG_KEYS + } + ) def to_dict(self) -> dict[str, bool]: return {key: getattr(self, key) for key in FLAG_KEYS} diff --git a/src/shieldlabs/_normalize.py b/src/shieldlabs/_normalize.py index 1036bc9..0f87ac7 100644 --- a/src/shieldlabs/_normalize.py +++ b/src/shieldlabs/_normalize.py @@ -40,18 +40,18 @@ FLAG_KEYS: tuple[str, ...] = ( "vpn", "proxy", - "privacy_relay", + "private_relay", "check_incomplete", - "stun_not_checked", + "network_check_incomplete", "os_not_detected", "timezone_mismatch", "javascript_disabled", "os_mismatch", "ip_mismatch", - "ws_tcp_latency", + "latency_test", "anti_detect_browser", "device_spoofing", - "rate_limited", + "banned_ip", "incognito", "search_bot", "ai_bot", @@ -63,16 +63,16 @@ HISTORY_FLAG_MAP: dict[str, str] = { "proxy": "is_proxy", - "privacy_relay": "is_privacy_relay", + "private_relay": "is_privacy_relay", "check_incomplete": "check_incomplete", - "stun_not_checked": "is_stun_not_checked", + "network_check_incomplete": "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", + "latency_test": "is_latency_test", "anti_detect_browser": "is_antidetect", "device_spoofing": "is_device_spoof", - "rate_limited": "is_rate_limited", + "banned_ip": "is_rate_limited", "incognito": "is_incognito", "search_bot": "is_search_bot", "ai_bot": "is_ai_bot", @@ -97,11 +97,11 @@ _EXACT_SLUGS: dict[str, str] = { "Is tor": "tor", "Is VPN": "vpn", - "Is privacy relay": "privacy_relay", + "Is privacy relay": "private_relay", "Is proxy": "proxy", "Is datacenter": "datacenter_ip", "Is abuser": "abuser", - "Stun is not checked": "stun_not_checked", + "Stun is not checked": "network_check_incomplete", "Stun passed (late arrival, corrected)": "stun_late_correction", "UA OS is not detected": "os_not_detected", "Network OS is not detected": "os_not_detected", @@ -109,7 +109,7 @@ "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", + "User has been banned 1H, to many requests": "banned_ip", } _PREFIX_SLUGS: tuple[tuple[str, str], ...] = ( @@ -117,7 +117,7 @@ ("Os_mismatch", "os_mismatch"), ("OS mismatch2", "os_mismatch"), ("TCP handshake", "tcp_handshake_v2"), - ("Latency test", "ws_tcp_latency"), + ("Latency test", "latency_test"), ("JavaScript disabled", "javascript_disabled"), ) diff --git a/tests/data/normalization-cases.json b/tests/data/normalization-cases.json index d311d47..e3ff0d1 100644 --- a/tests/data/normalization-cases.json +++ b/tests/data/normalization-cases.json @@ -116,18 +116,18 @@ "detection_flags": { "vpn": false, "proxy": true, - "privacy_relay": false, + "private_relay": false, "check_incomplete": false, - "stun_not_checked": false, + "network_check_incomplete": false, "os_not_detected": false, "timezone_mismatch": false, "javascript_disabled": false, "os_mismatch": false, "ip_mismatch": true, - "ws_tcp_latency": false, + "latency_test": false, "anti_detect_browser": true, "device_spoofing": false, - "rate_limited": false, + "banned_ip": false, "incognito": false, "search_bot": false, "ai_bot": false, @@ -157,7 +157,7 @@ "country": "United States", "connection_type": "direct", "score": 10, - "score_details": "[{\"Value\":10,\"Description\":\"Browser timezone ≠ IP-timezone\"}]", + "score_details": "[{\"Value\":10,\"Description\":\"Browser timezone \u2260 IP-timezone\"}]", "created_at": "2026-09-30 12:40:01.007", "ver": 1790772001007, "web_rtc_ip": "192.0.2.44", @@ -229,24 +229,24 @@ { "name": "timezone_mismatch", "weight": 10, - "description": "Browser timezone ≠ IP-timezone" + "description": "Browser timezone \u2260 IP-timezone" } ], "detection_flags": { "vpn": false, "proxy": false, - "privacy_relay": false, + "private_relay": false, "check_incomplete": false, - "stun_not_checked": false, + "network_check_incomplete": false, "os_not_detected": false, "timezone_mismatch": true, "javascript_disabled": false, "os_mismatch": false, "ip_mismatch": false, - "ws_tcp_latency": false, + "latency_test": false, "anti_detect_browser": false, "device_spoofing": false, - "rate_limited": false, + "banned_ip": false, "incognito": true, "search_bot": false, "ai_bot": false, @@ -276,7 +276,7 @@ "country": "France", "connection_type": "vpn", "score": 45, - "score_details": "[{\"Value\":15,\"Description\":\"Is VPN\"},{\"Value\":30,\"Description\":\"Stun is not checked\"},{\"Value\":-30,\"Description\":\"Stun passed (late arrival, corrected)\"},{\"Value\":30,\"Description\":\"Sticky verdict: Stun is not checked (request 11111111-2222-4333-8444-555555555555)\"},{\"Value\":0,\"Description\":\"IP ≠ leakIP (198.51.100.7 ≠ 203.0.113.9, source=scanner)\"}]", + "score_details": "[{\"Value\":15,\"Description\":\"Is VPN\"},{\"Value\":30,\"Description\":\"Stun is not checked\"},{\"Value\":-30,\"Description\":\"Stun passed (late arrival, corrected)\"},{\"Value\":30,\"Description\":\"Sticky verdict: Stun is not checked (request 11111111-2222-4333-8444-555555555555)\"},{\"Value\":0,\"Description\":\"IP \u2260 leakIP (198.51.100.7 \u2260 203.0.113.9, source=scanner)\"}]", "created_at": "2026-09-30 13:05:12", "ver": 1790773512000, "web_rtc_ip": "0.0.0.0", @@ -356,7 +356,7 @@ "description": "Is VPN" }, { - "name": "stun_not_checked", + "name": "network_check_incomplete", "weight": 30, "description": "Stun is not checked" }, @@ -374,18 +374,18 @@ "detection_flags": { "vpn": true, "proxy": false, - "privacy_relay": false, + "private_relay": false, "check_incomplete": true, - "stun_not_checked": true, + "network_check_incomplete": true, "os_not_detected": false, "timezone_mismatch": false, "javascript_disabled": false, "os_mismatch": false, "ip_mismatch": true, - "ws_tcp_latency": false, + "latency_test": false, "anti_detect_browser": false, "device_spoofing": false, - "rate_limited": false, + "banned_ip": false, "incognito": false, "search_bot": false, "ai_bot": false, @@ -485,7 +485,7 @@ "risk_score": 999, "signals": [ { - "name": "rate_limited", + "name": "banned_ip", "weight": 999, "description": "User has been banned 1H, to many requests" } @@ -493,18 +493,18 @@ "detection_flags": { "vpn": false, "proxy": false, - "privacy_relay": false, + "private_relay": false, "check_incomplete": false, - "stun_not_checked": false, + "network_check_incomplete": false, "os_not_detected": false, "timezone_mismatch": false, "javascript_disabled": false, "os_mismatch": false, "ip_mismatch": false, - "ws_tcp_latency": false, + "latency_test": false, "anti_detect_browser": false, "device_spoofing": false, - "rate_limited": false, + "banned_ip": false, "incognito": false, "search_bot": false, "ai_bot": false, @@ -610,18 +610,18 @@ "detection_flags": { "vpn": false, "proxy": false, - "privacy_relay": false, + "private_relay": false, "check_incomplete": false, - "stun_not_checked": false, + "network_check_incomplete": false, "os_not_detected": false, "timezone_mismatch": false, "javascript_disabled": false, "os_mismatch": false, "ip_mismatch": false, - "ws_tcp_latency": false, + "latency_test": false, "anti_detect_browser": false, "device_spoofing": false, - "rate_limited": false, + "banned_ip": false, "incognito": false, "search_bot": true, "ai_bot": false, @@ -758,18 +758,18 @@ "detection_flags": { "vpn": false, "proxy": true, - "privacy_relay": false, + "private_relay": false, "check_incomplete": false, - "stun_not_checked": false, + "network_check_incomplete": false, "os_not_detected": false, "timezone_mismatch": false, "javascript_disabled": false, "os_mismatch": false, "ip_mismatch": true, - "ws_tcp_latency": false, + "latency_test": false, "anti_detect_browser": true, "device_spoofing": false, - "rate_limited": false, + "banned_ip": false, "incognito": false, "search_bot": false, "ai_bot": false, @@ -888,18 +888,18 @@ "detection_flags": { "vpn": false, "proxy": false, - "privacy_relay": false, + "private_relay": false, "check_incomplete": false, - "stun_not_checked": false, + "network_check_incomplete": false, "os_not_detected": false, "timezone_mismatch": false, "javascript_disabled": false, "os_mismatch": false, "ip_mismatch": false, - "ws_tcp_latency": false, + "latency_test": false, "anti_detect_browser": false, "device_spoofing": false, - "rate_limited": false, + "banned_ip": false, "incognito": false, "search_bot": false, "ai_bot": false, @@ -1036,18 +1036,18 @@ "detection_flags": { "vpn": false, "proxy": true, - "privacy_relay": false, + "private_relay": false, "check_incomplete": false, - "stun_not_checked": false, + "network_check_incomplete": false, "os_not_detected": false, "timezone_mismatch": false, "javascript_disabled": false, "os_mismatch": false, "ip_mismatch": false, - "ws_tcp_latency": false, + "latency_test": false, "anti_detect_browser": false, "device_spoofing": false, - "rate_limited": false, + "banned_ip": false, "incognito": false, "search_bot": false, "ai_bot": false, diff --git a/tests/data/signal-slug-cases.json b/tests/data/signal-slug-cases.json index bb3cc8a..66772c5 100644 --- a/tests/data/signal-slug-cases.json +++ b/tests/data/signal-slug-cases.json @@ -11,7 +11,7 @@ }, { "description": "Is privacy relay", - "slug": "privacy_relay" + "slug": "private_relay" }, { "description": "Is proxy", @@ -27,7 +27,7 @@ }, { "description": "Stun is not checked", - "slug": "stun_not_checked" + "slug": "network_check_incomplete" }, { "description": "Stun passed (late arrival, corrected)", @@ -42,7 +42,7 @@ "slug": "os_not_detected" }, { - "description": "Browser timezone ≠ IP-timezone", + "description": "Browser timezone \u2260 IP-timezone", "slug": "timezone_mismatch" }, { @@ -59,7 +59,7 @@ }, { "description": "User has been banned 1H, to many requests", - "slug": "rate_limited" + "slug": "banned_ip" }, { "description": "Antidetect browser (turn_block)", @@ -83,7 +83,7 @@ }, { "description": "Latency test (WS vs TCP: 40ms)", - "slug": "ws_tcp_latency" + "slug": "latency_test" }, { "description": "JavaScript disabled (WebRTC, WebGL)", @@ -98,7 +98,7 @@ "slug": "port_scan_routed_via_proxy" }, { - "description": "IP ≠ leakIP (198.51.100.7 ≠ 203.0.113.9, source=scanner)", + "description": "IP \u2260 leakIP (198.51.100.7 \u2260 203.0.113.9, source=scanner)", "slug": "ip__neq__leakip" }, { @@ -110,8 +110,8 @@ "slug": "weird_name_here" }, { - "description": "État Spécial (x)", - "slug": "état_spécial" + "description": "\u00c9tat Sp\u00e9cial (x)", + "slug": "\u00e9tat_sp\u00e9cial" }, { "description": "(only parens)", diff --git a/tests/test_normalization.py b/tests/test_normalization.py index 4946f5f..c396371 100644 --- a/tests/test_normalization.py +++ b/tests/test_normalization.py @@ -162,9 +162,9 @@ def test_history_row_keeps_negative_and_repeated_signals_and_skips_non_integers( ] identification = Identification.from_history_row({"score_details": json.dumps(details)}) assert [(s.name, s.weight) for s in identification.signals] == [ - ("stun_not_checked", 30), + ("network_check_incomplete", 30), ("stun_late_correction", -30), - ("stun_not_checked", 30), + ("network_check_incomplete", 30), ] @@ -272,7 +272,7 @@ def test_history_page_rows_match_normalization_cases() -> None: def test_known_signal_names_are_plain_strings() -> None: assert SignalName.ANTIDETECT_BROWSER == "antidetect_browser" assert SignalName.STUN_LATE_CORRECTION == "stun_late_correction" - assert SignalName.RATE_LIMITED == "rate_limited" + assert SignalName.RATE_LIMITED == "banned_ip" def test_models_are_frozen() -> None: