diff --git a/generated/.openapi-generator/FILES b/generated/.openapi-generator/FILES index 34d3b18..a910d2f 100644 --- a/generated/.openapi-generator/FILES +++ b/generated/.openapi-generator/FILES @@ -8,10 +8,14 @@ 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/domain_profile.py shieldlabs_generated/models/error_body.py -shieldlabs_generated/models/fingerprint.py shieldlabs_generated/models/health_status.py shieldlabs_generated/models/history_page.py shieldlabs_generated/models/history_row.py @@ -21,6 +25,10 @@ shieldlabs_generated/models/identification_scored_data.py shieldlabs_generated/models/identification_scored_event.py shieldlabs_generated/models/ip_info.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 diff --git a/generated/shieldlabs_generated/__init__.py b/generated/shieldlabs_generated/__init__.py index 456aa65..644a075 100644 --- a/generated/shieldlabs_generated/__init__.py +++ b/generated/shieldlabs_generated/__init__.py @@ -31,10 +31,14 @@ "ApiKeyError", "ApiAttributeError", "ApiException", + "ClientIdentity", + "ClientIdentityAssessmentsInner", + "ClientIdentityClaimsInner", + "ClientIdentityEvidenceInner", + "ClientIdentityVerifiedInner", "DetectionFlags", "DomainProfile", "ErrorBody", - "Fingerprint", "HRE", "HREResult", "HealthStatus", @@ -44,6 +48,10 @@ "IdentificationScoredEvent", "IpInfo", "LegacySnapshot", + "MultiaccountChangedData", + "MultiaccountChangedDataEvidenceSummary", + "MultiaccountChangedDataSource", + "MultiaccountChangedEvent", "RiskEvent", "ScoreDetail", "Signal", @@ -68,10 +76,14 @@ 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.domain_profile import DomainProfile as DomainProfile from shieldlabs_generated.models.error_body import ErrorBody as ErrorBody -from shieldlabs_generated.models.fingerprint import Fingerprint as Fingerprint 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 @@ -81,6 +93,10 @@ 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_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 diff --git a/generated/shieldlabs_generated/models/__init__.py b/generated/shieldlabs_generated/models/__init__.py index 2441e82..4253897 100644 --- a/generated/shieldlabs_generated/models/__init__.py +++ b/generated/shieldlabs_generated/models/__init__.py @@ -14,10 +14,14 @@ """ # 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.domain_profile import DomainProfile from shieldlabs_generated.models.error_body import ErrorBody -from shieldlabs_generated.models.fingerprint import Fingerprint from shieldlabs_generated.models.hre import HRE from shieldlabs_generated.models.hre_result import HREResult from shieldlabs_generated.models.health_status import HealthStatus @@ -27,6 +31,10 @@ from shieldlabs_generated.models.identification_scored_event import IdentificationScoredEvent from shieldlabs_generated.models.ip_info import IpInfo 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 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/fingerprint.py b/generated/shieldlabs_generated/models/client_identity_assessments_inner.py similarity index 63% rename from generated/shieldlabs_generated/models/fingerprint.py rename to generated/shieldlabs_generated/models/client_identity_assessments_inner.py index b765df6..030f5e8 100644 --- a/generated/shieldlabs_generated/models/fingerprint.py +++ b/generated/shieldlabs_generated/models/client_identity_assessments_inner.py @@ -18,24 +18,21 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr +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 Fingerprint(BaseModel): +class ClientIdentityAssessmentsInner(BaseModel): """ - FP21 for tracked users. Legacy sharing/takeover/travel mirrors are retained; prefer data.hre. Absent on anonymous checks. + ClientIdentityAssessmentsInner """ # noqa: E501 - outcome: StrictStr - record_id: Optional[StrictStr] = None - hardware_id: Optional[StrictStr] = Field(default=None, description="FP21 hardware identity, distinct from device_id.") - rules_version: StrictStr - sharing: Optional[Dict[str, Any]] = None - takeover: Optional[Dict[str, Any]] = None - travel: Optional[Dict[str, Any]] = None - __properties: ClassVar[List[str]] = ["outcome", "record_id", "hardware_id", "rules_version", "sharing", "takeover", "travel"] + 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, @@ -55,7 +52,7 @@ def to_json(self) -> str: @classmethod def from_json(cls, json_str: str) -> Optional[Self]: - """Create an instance of Fingerprint from a JSON string""" + """Create an instance of ClientIdentityAssessmentsInner from a JSON string""" return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: @@ -67,8 +64,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( @@ -76,11 +75,16 @@ 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 def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: - """Create an instance of Fingerprint from a dict""" + """Create an instance of ClientIdentityAssessmentsInner from a dict""" if obj is None: return None @@ -88,14 +92,15 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "outcome": obj.get("outcome"), - "record_id": obj.get("record_id"), - "hardware_id": obj.get("hardware_id"), - "rules_version": obj.get("rules_version"), - "sharing": obj.get("sharing"), - "takeover": obj.get("takeover"), - "travel": obj.get("travel") + "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 7b1a1d4..6a3b228 100644 --- a/generated/shieldlabs_generated/models/detection_flags.py +++ b/generated/shieldlabs_generated/models/detection_flags.py @@ -26,7 +26,7 @@ class DetectionFlags(BaseModel): """ - Stable yes/no verdicts for the identification. Legacy 19 keys are always present; the four extension flags are present in schema 2026-10-06. 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`. + Stable yes/no verdicts for the identification. Legacy 19 keys are always present; the extension flags depend on the scoring release. Task 204 adds optional ai_bot and ai_browser flags; older payloads omit them. 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`. """ # 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.") @@ -35,11 +35,11 @@ class DetectionFlags(BaseModel): 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.") + 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.") + 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="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.") @@ -47,11 +47,13 @@ class DetectionFlags(BaseModel): 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.") - os_mismatch2: Optional[StrictBool] = None device_spoofing: Optional[StrictBool] = None latency_test: Optional[StrictBool] = None banned_ip: Optional[StrictBool] = None - __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_mismatch2", "device_spoofing", "latency_test", "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", "banned_ip", "ai_bot", "ai_browser", "os_mismatch2"] model_config = ConfigDict( validate_by_name=True, @@ -123,10 +125,12 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "javascript_disabled": obj.get("javascript_disabled"), "stun_not_checked": obj.get("stun_not_checked"), "check_incomplete": obj.get("check_incomplete"), - "os_mismatch2": obj.get("os_mismatch2"), "device_spoofing": obj.get("device_spoofing"), "latency_test": obj.get("latency_test"), - "banned_ip": obj.get("banned_ip") + "banned_ip": obj.get("banned_ip"), + "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/history_row.py b/generated/shieldlabs_generated/models/history_row.py index 7312dd4..9ef8348 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 request_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.") @@ -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]] = ["request_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", "request_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, "request_id": obj.get("request_id"), "session_id": obj.get("session_id"), "cookie_id": obj.get("cookie_id"), diff --git a/generated/shieldlabs_generated/models/hre_result.py b/generated/shieldlabs_generated/models/hre_result.py index 3f37f92..622b991 100644 --- a/generated/shieldlabs_generated/models/hre_result.py +++ b/generated/shieldlabs_generated/models/hre_result.py @@ -34,7 +34,8 @@ class HREResult(BaseModel): reason: StrictStr devices: Optional[Annotated[int, Field(strict=True, ge=0)]] = None min_devices: Optional[Annotated[int, Field(strict=True, ge=1)]] = None - __properties: ClassVar[List[str]] = ["status", "level", "reason", "devices", "min_devices"] + 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, @@ -80,6 +81,11 @@ def to_dict(self) -> Dict[str, Any]: 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 @@ -96,7 +102,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "level": obj.get("level"), "reason": obj.get("reason"), "devices": obj.get("devices"), - "min_devices": obj.get("min_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 d20b64a..2b691c5 100644 --- a/generated/shieldlabs_generated/models/identification_scored_data.py +++ b/generated/shieldlabs_generated/models/identification_scored_data.py @@ -23,8 +23,8 @@ 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 shieldlabs_generated.models.detection_flags import DetectionFlags -from shieldlabs_generated.models.fingerprint import Fingerprint from shieldlabs_generated.models.hre import HRE from shieldlabs_generated.models.ip_info import IpInfo from shieldlabs_generated.models.risk_event import RiskEvent @@ -36,8 +36,9 @@ class IdentificationScoredData(BaseModel): """ - Final identification. risk_score is this scan only; no all-time entity risk. New extension fields are required by version 2026-10-06; legacy bodies remain accepted. + Final identification. risk_score is this scan only. Version 2026-10-07 uses signals for score contributions, detection_flags for final decisions, and hre for account results. Older bodies remain accepted. """ # noqa: E501 + client_identity: Optional[ClientIdentity] = None request_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.") @@ -58,10 +59,12 @@ class IdentificationScoredData(BaseModel): 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)]] = None scoring_version: Optional[StrictStr] = Field(default=None, description="Core build source revision; core:unversioned on local builds.") - risk_events: Optional[List[RiskEvent]] = None + risk_events: Optional[List[RiskEvent]] = Field(default=None, description="Legacy 2026-10-06 only; absent from current scored events.") hre: Optional[HRE] = None - fingerprint: Optional[Fingerprint] = None - __properties: ClassVar[List[str]] = ["request_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", "fingerprint"] + 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", "request_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): @@ -112,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() @@ -141,9 +147,6 @@ def to_dict(self) -> Dict[str, Any]: # override the default output from pydantic by calling `to_dict()` of hre if self.hre: _dict['hre'] = self.hre.to_dict() - # override the default output from pydantic by calling `to_dict()` of fingerprint - if self.fingerprint: - _dict['fingerprint'] = self.fingerprint.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: @@ -161,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": ClientIdentity.from_dict(obj["client_identity"]) if obj.get("client_identity") is not None else None, "request_id": obj.get("request_id"), "visitor_id": obj.get("visitor_id"), "device_id": obj.get("device_id"), @@ -183,7 +187,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "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, - "fingerprint": Fingerprint.from_dict(obj["fingerprint"]) if obj.get("fingerprint") 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_event.py b/generated/shieldlabs_generated/models/identification_scored_event.py index be42015..d60b91e 100644 --- a/generated/shieldlabs_generated/models/identification_scored_event.py +++ b/generated/shieldlabs_generated/models/identification_scored_event.py @@ -32,7 +32,7 @@ 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="Webhook contract version. Current release 2026-10-06; parsers also accept legacy 2026-06-01.") + schema_version: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Webhook contract version. Scored release 2026-10-06; 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 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.") 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..668f756 --- /dev/null +++ b/generated/shieldlabs_generated/models/multiaccount_changed_data.py @@ -0,0 +1,210 @@ +# 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="ID of the exact sorted user set within this site. A membership change resolves the old group and detects a new group.") + 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.") + 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", "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"), + "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 index cd432f9..401d461 100644 --- a/generated/shieldlabs_generated/models/risk_event.py +++ b/generated/shieldlabs_generated/models/risk_event.py @@ -28,7 +28,7 @@ class RiskEvent(BaseModel): """ RiskEvent """ # noqa: E501 - code: StrictStr + 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.") diff --git a/generated/shieldlabs_generated/models/signal.py b/generated/shieldlabs_generated/models/signal.py index 38e490e..551a398 100644 --- a/generated/shieldlabs_generated/models/signal.py +++ b/generated/shieldlabs_generated/models/signal.py @@ -29,7 +29,7 @@ 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. """ # 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.") + 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 (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_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.") __properties: ClassVar[List[str]] = ["name", "weight"] diff --git a/generated/shieldlabs_generated/models/webhook_ping_event.py b/generated/shieldlabs_generated/models/webhook_ping_event.py index 45f432e..65569f2 100644 --- a/generated/shieldlabs_generated/models/webhook_ping_event.py +++ b/generated/shieldlabs_generated/models/webhook_ping_event.py @@ -31,7 +31,7 @@ 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="Webhook contract version. Current release 2026-10-06; parsers also accept legacy 2026-06-01.") + schema_version: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Webhook contract version. Scored release 2026-10-06; 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.") event_id: Optional[Annotated[str, Field(min_length=1, strict=True)]] = None __properties: ClassVar[List[str]] = ["event_type", "schema_version", "created_at", "event_id"] diff --git a/resources/shieldlabs-api.yaml b/resources/shieldlabs-api.yaml index 03edf61..9cbe011 100644 --- a/resources/shieldlabs-api.yaml +++ b/resources/shieldlabs-api.yaml @@ -466,6 +466,53 @@ paths: source: | curl "https://account.shieldlabs.ai/health" webhooks: + multi-account-changed: + post: + operationId: multiaccountChanged + tags: + - Webhooks + summary: A multi-account group changed + description: |- + 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. + + A repeated build with unchanged membership, level, and rules does not produce a new event. + A membership change produces resolved for the previous exact group and detected for the new + exact group. 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. + + 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. + + This event has no request_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 @@ -673,21 +720,6 @@ webhooks: stun_not_checked: false check_incomplete: false observed_at: '2026-10-06T00:00:00Z' - fingerprint: - outcome: new_record - record_id: sample-record - hardware_id: sample-hardware - rules_version: fp21-fixture - sharing: - level: high - devices: 4 - min_devices: 4 - takeover: - level: medium - reason: fixture - travel: - level: '' - reason: below_distance responses: 2XX: description: Delivery accepted. The response body is ignored. @@ -939,6 +971,144 @@ components: 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. RequestId: type: string format: uuid @@ -1222,6 +1392,8 @@ components: - is_incognito - is_search_bot properties: + client_identity: + $ref: '#/components/schemas/ClientIdentity' request_id: $ref: '#/components/schemas/RequestId' session_id: @@ -1602,13 +1774,6 @@ components: type: string const: ok description: Always `ok` when the service answers. - SchemaVersion: - type: string - minLength: 1 - description: Webhook contract version. Current release 2026-10-06; parsers also accept legacy 2026-06-01. - examples: - - '2026-10-06' - - '2026-06-01' Rfc3339Timestamp: type: string format: date-time @@ -1617,6 +1782,187 @@ 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 + - 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: ID of the exact sorted user set within this site. A membership change resolves the old group and detects a new group. + 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 + 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: 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' + SchemaVersion: + type: string + minLength: 1 + description: Webhook contract version. Scored release 2026-10-06; multi-account group release 2026-10-07; parsers also accept legacy 2026-06-01. + examples: + - '2026-10-06' + - '2026-06-01' UserHid: type: - string @@ -1732,7 +2078,7 @@ 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; @@ -1758,7 +2104,7 @@ components: DetectionFlags: type: object description: |- - Stable yes/no verdicts for the identification. Legacy 19 keys are always present; the four extension flags are present in schema 2026-10-06. Branch on these flags and on + Stable yes/no verdicts for the identification. Legacy 19 keys are always present; the extension flags depend on the scoring release. Task 204 adds optional ai_bot and ai_browser flags; older payloads omit them. 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 @@ -1807,7 +2153,7 @@ components: 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. + 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. @@ -1819,7 +2165,7 @@ components: description: An anti-detect browser was detected. browser_automation: type: boolean - description: Browser automation was detected, for example a WebDriver-controlled browser. + 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: 'The public IP differs from the local IP found by the browser network check. Informational: it does not add to the score.' @@ -1841,14 +2187,22 @@ components: check_incomplete: type: boolean description: Part of the browser checks timed out, so the verdict rests on partial data. Informational. - os_mismatch2: - type: boolean device_spoofing: type: boolean latency_test: type: boolean banned_ip: type: boolean + 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. RiskEvent: type: object required: @@ -1860,6 +2214,7 @@ components: properties: 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: Final scoring flag. false does not assert that every underlying probe completed. @@ -1903,6 +2258,11 @@ components: 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: @@ -1919,31 +2279,9 @@ components: 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. - Fingerprint: - type: object - required: - - outcome - - rules_version - properties: - outcome: - type: string - record_id: - type: string - hardware_id: - type: string - description: FP21 hardware identity, distinct from device_id. - rules_version: - type: string - sharing: - type: object - takeover: - type: object - travel: - type: object - description: FP21 for tracked users. Legacy sharing/takeover/travel mirrors are retained; prefer data.hre. Absent on anonymous checks. IdentificationScoredData: type: object - description: Final identification. risk_score is this scan only; no all-time entity risk. New extension fields are required by version 2026-10-06; legacy bodies remain accepted. + description: Final identification. risk_score is this scan only. Version 2026-10-07 uses signals for score contributions, detection_flags for final decisions, and hre for account results. Older bodies remain accepted. required: - request_id - visitor_id @@ -1964,6 +2302,8 @@ components: - detection_flags - observed_at properties: + client_identity: + $ref: '#/components/schemas/ClientIdentity' request_id: $ref: '#/components/schemas/RequestId' visitor_id: @@ -2016,13 +2356,26 @@ components: type: string description: Core build source revision; core:unversioned on local builds. 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' - fingerprint: - $ref: '#/components/schemas/Fingerprint' + 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.' @@ -2081,6 +2434,83 @@ components: 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: + const: '2026-10-07' + 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: + - result_version + - scoring_version + - hre + properties: + result_version: {} + scoring_version: {} + risk_events: {} + fingerprint: {} + detection_flags: + type: object + not: + type: object + required: + - os_mismatch2 + properties: + os_mismatch2: {} + additionalProperties: true + 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 + not: + anyOf: + - type: object + required: + - risk_events + properties: + risk_events: {} + additionalProperties: true + - type: object + required: + - fingerprint + properties: + fingerprint: {} + 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. @@ -2494,6 +2924,115 @@ 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: ad5efdbb113f50d1207c3a3b1d03e54ee60dfb9a + 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: 4293f02e-7cf0-5cda-9150-b5b8c4ee685d + revision: '7412' + previous_revision: null + 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: 27225e88895f45c6f96562578308d3afea9d4ea2 + 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: 4293f02e-7cf0-5cda-9150-b5b8c4ee685d + revision: '7414' + previous_revision: '7412' + 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: 0e703af70ff450065df0457d71789d32df4de079 + 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: 4293f02e-7cf0-5cda-9150-b5b8c4ee685d + revision: '7415' + previous_revision: '7412' + 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 81b7de7..f52c90c 100644 --- a/src/shieldlabs/_models.py +++ b/src/shieldlabs/_models.py @@ -175,6 +175,8 @@ class DetectionFlags: javascript_disabled: bool = False stun_not_checked: bool = False check_incomplete: bool = False + ai_bot: Optional[bool] = None + ai_browser: Optional[bool] = None os_mismatch2: Optional[bool] = None device_spoofing: Optional[bool] = None latency_test: Optional[bool] = None @@ -188,7 +190,14 @@ def from_dict(cls, data: Mapping[str, Any]) -> DetectionFlags: **{key: bool(data.get(key, False)) for key in FLAG_KEYS}, **{ key: data[key] - for key in ("os_mismatch2", "device_spoofing", "latency_test", "banned_ip") + for key in ( + "os_mismatch2", + "device_spoofing", + "latency_test", + "banned_ip", + "ai_bot", + "ai_browser", + ) if isinstance(data.get(key), bool) }, ) @@ -198,7 +207,14 @@ def to_dict(self) -> dict[str, bool]: **{key: getattr(self, key) for key in FLAG_KEYS}, **{ key: getattr(self, key) - for key in ("os_mismatch2", "device_spoofing", "latency_test", "banned_ip") + for key in ( + "os_mismatch2", + "device_spoofing", + "latency_test", + "banned_ip", + "ai_bot", + "ai_browser", + ) if getattr(self, key) is not None }, } @@ -269,6 +285,9 @@ class Identification: source: IdentificationSource result_version: Optional[str] = None scoring_version: Optional[str] = None + search_bot_owner: Optional[str] = None + ai_bot_owner: Optional[str] = None + ai_browser_owner: Optional[str] = None risk_events: Optional[tuple[RiskEvent, ...]] = None hre: Optional[Mapping[str, Any]] = None fingerprint: Optional[Mapping[str, Any]] = None @@ -321,6 +340,13 @@ def from_webhook_data(cls, data: Mapping[str, Any]) -> Identification: detection_flags=DetectionFlags.from_dict(flags), observed_at=parse_rfc3339(data.get("observed_at")), source="webhook", + search_bot_owner=as_str(data["search_bot_owner"]) + if "search_bot_owner" in data + else None, + ai_bot_owner=as_str(data["ai_bot_owner"]) if "ai_bot_owner" in data else None, + ai_browser_owner=as_str(data["ai_browser_owner"]) + if "ai_browser_owner" in data + else None, result_version=as_str(data["result_version"]) if "result_version" in data else None, scoring_version=as_str(data["scoring_version"]) if "scoring_version" in data else None, risk_events=tuple( @@ -453,6 +479,13 @@ def from_dict(cls, data: Mapping[str, Any]) -> Identification: if isinstance(observed_at, datetime) else parse_rfc3339(observed_at), source="history" if source == "history" else "webhook", + search_bot_owner=as_str(data["search_bot_owner"]) + if "search_bot_owner" in data + else None, + ai_bot_owner=as_str(data["ai_bot_owner"]) if "ai_bot_owner" in data else None, + ai_browser_owner=as_str(data["ai_browser_owner"]) + if "ai_browser_owner" in data + else None, result_version=as_str(data["result_version"]) if "result_version" in data else None, scoring_version=as_str(data["scoring_version"]) if "scoring_version" in data else None, risk_events=tuple( @@ -500,6 +533,17 @@ def to_dict(self) -> dict[str, Any]: if self.risk_events is not None else {} ), + **( + {"search_bot_owner": self.search_bot_owner} + if self.search_bot_owner is not None + else {} + ), + **({"ai_bot_owner": self.ai_bot_owner} if self.ai_bot_owner is not None else {}), + **( + {"ai_browser_owner": self.ai_browser_owner} + if self.ai_browser_owner is not None + else {} + ), **({"hre": dict(self.hre)} if self.hre is not None else {}), **({"fingerprint": dict(self.fingerprint)} if self.fingerprint is not None else {}), } diff --git a/src/shieldlabs/webhooks.py b/src/shieldlabs/webhooks.py index c46d846..cfa1cce 100644 --- a/src/shieldlabs/webhooks.py +++ b/src/shieldlabs/webhooks.py @@ -5,7 +5,7 @@ string exactly as shown in the analytics dashboard, ``whsec_`` prefix included, and the message is the raw request body. Always verify the raw bytes you received, before parsing them. -Version 2026-10-06 carries a signed event_id in the body and X-Shield-Event-Id header. +Version 2026-10-07 carries a signed event_id in the body and X-Shield-Event-Id header. Failed deliveries are retried within a bounded window. Deduplicate by event_id, store the event durably before returning 2xx, then process asynchronously. For old bodies without event_id, fall back to data.request_id. History provides recovery/latest state. @@ -49,7 +49,7 @@ SIGNATURE_HEADER = "X-Shield-Signature" """Name of the header that carries the signature.""" -SCHEMA_VERSION = "2026-10-06" +SCHEMA_VERSION = "2026-10-07" """Webhook ``schema_version`` this SDK was built for. Other values are parsed with a warning.""" _SIGNATURE_PREFIX = "sha256=" diff --git a/tests/contracts/ai-bot.json b/tests/contracts/ai-bot.json new file mode 100644 index 0000000..30b6bc0 --- /dev/null +++ b/tests/contracts/ai-bot.json @@ -0,0 +1,91 @@ +{ + "event_id": "ce256c0f66536957adc5f719250f5b10ce331bd1", + "site_id": 7, + "event_type": "identification.scored", + "schema_version": "2026-10-07", + "created_at": "2026-10-06T00:00:02Z", + "data": { + "result_version": "2026-10-06T00:00:01Z", + "scoring_version": "core:fixture", + "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" + } + }, + "request_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": [], + "detection_flags": { + "device_spoofing": false, + "latency_test": false, + "banned_ip": false, + "vpn": false, + "privacy_relay": false, + "browser_vpn_proxy": false, + "tor": false, + "proxy": false, + "datacenter_ip": false, + "abuser": 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, + "suspicious_paid_click": false, + "javascript_disabled": false, + "stun_not_checked": false, + "check_incomplete": false + }, + "observed_at": "2026-10-06T00:00:00Z" + } +} diff --git a/tests/contracts/current.json b/tests/contracts/current.json index 4c6dcd6..f2f77c1 100644 --- a/tests/contracts/current.json +++ b/tests/contracts/current.json @@ -2,149 +2,15 @@ "event_id": "ce256c0f66536957adc5f719250f5b10ce331bd1", "site_id": 7, "event_type": "identification.scored", - "schema_version": "2026-10-06", + "schema_version": "2026-10-07", "created_at": "2026-10-06T00:00:02Z", "data": { "result_version": "2026-10-06T00:00:01Z", "scoring_version": "core:fixture", - "risk_events": [ - { - "code": "vpn", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "proxy", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "privacy_relay", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "check_incomplete", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "network_check_incomplete", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_not_detected", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "timezone_mismatch", - "detected": false, - "weight": 10, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "javascript_disabled", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch2", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "ip_mismatch", - "detected": false, - "weight": 45, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "latency_test", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "anti_detect_browser", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "device_spoofing", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "banned_ip", - "detected": false, - "weight": 999, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "incognito", - "detected": true, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "search_bot", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "browser_automation", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "tor", - "detected": false, - "weight": 99, - "contribution": 0, - "status": "evaluated" - } - ], "hre": { "rules_version": "fp21-fixture", "account_sharing": { + "cluster_id": null, "status": "evaluated", "level": null, "reason": "", @@ -152,11 +18,13 @@ "min_devices": 4 }, "account_takeover": { + "cluster_id": null, "status": "not_evaluated", "level": null, "reason": "no_history" }, "impossible_travel": { + "cluster_id": null, "status": "not_evaluated", "level": null, "reason": "skipped" @@ -195,7 +63,6 @@ "risk_score": 0, "signals": [], "detection_flags": { - "os_mismatch2": false, "device_spoofing": false, "latency_test": false, "banned_ip": false, @@ -214,30 +81,13 @@ "ip_mismatch": false, "incognito": true, "search_bot": false, + "ai_bot": false, + "ai_browser": false, "suspicious_paid_click": false, "javascript_disabled": false, "stun_not_checked": false, "check_incomplete": false }, - "observed_at": "2026-10-06T00:00:00Z", - "fingerprint": { - "outcome": "new_record", - "record_id": "sample-record", - "hardware_id": "sample-hardware", - "rules_version": "fp21-fixture", - "sharing": { - "level": "", - "devices": 1, - "min_devices": 4 - }, - "takeover": { - "level": "", - "reason": "no_history" - }, - "travel": { - "level": "", - "reason": "skipped" - } - } + "observed_at": "2026-10-06T00:00:00Z" } } diff --git a/tests/test_contract_current.py b/tests/test_contract_current.py index a62efe8..418a616 100644 --- a/tests/test_contract_current.py +++ b/tests/test_contract_current.py @@ -12,14 +12,23 @@ def test_current_core_fixture_preserves_identity_and_results(): assert event.event_id assert event.site_id == 7 assert isinstance(event, webhooks.IdentificationScoredEvent) - assert event.data.risk_events is not None - assert len(event.data.risk_events) == 19 - assert event.data.fingerprint is not None - assert event.data.fingerprint["hardware_id"] == "sample-hardware" - assert event.data.device_id != event.data.fingerprint["hardware_id"] + assert event.data.risk_events is None + assert event.data.fingerprint is None + assert event.data.detection_flags.os_mismatch2 is None assert event.data.hre is not None assert event.data.hre["account_takeover"]["reason"] == "no_history" encoded = event.data.to_dict() - assert encoded["risk_events"][15]["weight"] == 0 + assert "risk_events" not in encoded + assert encoded["hre"]["account_sharing"]["cluster_id"] is None assert event.data.from_dict(encoded).to_dict() == encoded assert not webhooks.verify_signature(raw + b" ", signature, "secret") + + +def test_accepted_ai_bot_owner_survives_normalization(): + raw = (Path(__file__).parent / "contracts/ai-bot.json").read_bytes() + signature = "sha256=" + hmac.new(b"secret", raw, hashlib.sha256).hexdigest() + event = webhooks.construct_event(raw, signature, "secret") + assert isinstance(event, webhooks.IdentificationScoredEvent) + assert event.data.ai_bot_owner == "OpenAI" + assert event.data.detection_flags.ai_bot is True + assert event.data.to_dict()["ai_bot_owner"] == "OpenAI" diff --git a/tests/test_package.py b/tests/test_package.py index 961f6b3..4e558f3 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -87,5 +87,5 @@ def test_version_and_typing_marker() -> None: def test_webhooks_module_surface() -> None: assert shieldlabs.webhooks.SIGNATURE_HEADER == "X-Shield-Signature" - assert shieldlabs.webhooks.SCHEMA_VERSION == "2026-10-06" + assert shieldlabs.webhooks.SCHEMA_VERSION == "2026-10-07" assert shieldlabs.webhooks.IdentificationScoredEvent is shieldlabs.IdentificationScoredEvent