From e2a0eba8d4e5dc8d6afd929431c0b5fc79391878 Mon Sep 17 00:00:00 2001 From: EgorMajj <91486022+EgorMajj@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:00:44 +0300 Subject: [PATCH] Add an OpenAPI generator beside the supported client sync.sh downloads the current API description and generate.sh rebuilds generated/ from it. The library people install is unchanged. --- .gitattributes | 4 + .github/workflows/ci.yml | 12 + CHANGELOG.md | 4 + README.md | 8 + config.json | 9 + generate.sh | 69 + generated/.openapi-generator-ignore | 8 + generated/.openapi-generator/FILES | 26 + generated/.openapi-generator/VERSION | 1 + generated/shieldlabs_generated/__init__.py | 81 + .../shieldlabs_generated/api/__init__.py | 7 + .../shieldlabs_generated/api/health_api.py | 296 ++ .../api/history_api_api.py | 372 +++ .../api/management_api_api.py | 661 +++++ generated/shieldlabs_generated/api_client.py | 805 ++++++ .../shieldlabs_generated/api_response.py | 21 + .../shieldlabs_generated/configuration.py | 619 +++++ generated/shieldlabs_generated/exceptions.py | 219 ++ .../shieldlabs_generated/models/__init__.py | 31 + .../models/detection_flags.py | 125 + .../models/domain_profile.py | 121 + .../shieldlabs_generated/models/error_body.py | 89 + .../models/health_status.py | 96 + .../models/history_page.py | 100 + .../models/history_row.py | 218 ++ .../models/identification_scored_data.py | 164 ++ .../models/identification_scored_event.py | 118 + .../shieldlabs_generated/models/ip_info.py | 102 + .../models/legacy_snapshot.py | 147 + .../models/score_detail.py | 91 + .../shieldlabs_generated/models/signal.py | 92 + .../models/traffic_source.py | 105 + .../models/webhook_ping_event.py | 112 + generated/shieldlabs_generated/rest.py | 264 ++ pyproject.toml | 1 + resources/shieldlabs-api.yaml | 2462 +++++++++++++++++ sync.sh | 15 + 37 files changed, 7675 insertions(+) create mode 100644 config.json create mode 100755 generate.sh create mode 100644 generated/.openapi-generator-ignore create mode 100644 generated/.openapi-generator/FILES create mode 100644 generated/.openapi-generator/VERSION create mode 100644 generated/shieldlabs_generated/__init__.py create mode 100644 generated/shieldlabs_generated/api/__init__.py create mode 100644 generated/shieldlabs_generated/api/health_api.py create mode 100644 generated/shieldlabs_generated/api/history_api_api.py create mode 100644 generated/shieldlabs_generated/api/management_api_api.py create mode 100644 generated/shieldlabs_generated/api_client.py create mode 100644 generated/shieldlabs_generated/api_response.py create mode 100644 generated/shieldlabs_generated/configuration.py create mode 100644 generated/shieldlabs_generated/exceptions.py create mode 100644 generated/shieldlabs_generated/models/__init__.py create mode 100644 generated/shieldlabs_generated/models/detection_flags.py create mode 100644 generated/shieldlabs_generated/models/domain_profile.py create mode 100644 generated/shieldlabs_generated/models/error_body.py create mode 100644 generated/shieldlabs_generated/models/health_status.py create mode 100644 generated/shieldlabs_generated/models/history_page.py create mode 100644 generated/shieldlabs_generated/models/history_row.py create mode 100644 generated/shieldlabs_generated/models/identification_scored_data.py create mode 100644 generated/shieldlabs_generated/models/identification_scored_event.py create mode 100644 generated/shieldlabs_generated/models/ip_info.py create mode 100644 generated/shieldlabs_generated/models/legacy_snapshot.py create mode 100644 generated/shieldlabs_generated/models/score_detail.py create mode 100644 generated/shieldlabs_generated/models/signal.py create mode 100644 generated/shieldlabs_generated/models/traffic_source.py create mode 100644 generated/shieldlabs_generated/models/webhook_ping_event.py create mode 100644 generated/shieldlabs_generated/rest.py create mode 100644 resources/shieldlabs-api.yaml create mode 100755 sync.sh diff --git a/.gitattributes b/.gitattributes index f858f1b..8f6e5d3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,5 @@ tests/data/** -text +generated/** linguist-generated +resources/shieldlabs-api.yaml linguist-generated +sync.sh text eol=lf +generate.sh text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9976f0f..e1755be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,3 +70,15 @@ jobs: python -m pip install build==1.6.1 twine==7.0.0 python -m build twine check --strict dist/* + + generated: + name: Generated API types + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Rebuild generated files + run: ./generate.sh + - name: Fail if generated files drifted + run: git diff --exit-code diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6e331..d6427d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project are documented in this file. The format foll ## [Unreleased] +### Added + +- `sync.sh` downloads the OpenAPI description and `generate.sh` rebuilds `generated/` from it. The supported client is unchanged. + ## [1.0.0] - 2026-09-30 First stable release. It replaces the 0.1.0 preview package completely. diff --git a/README.md b/README.md index 470b8c8..0be23b7 100644 --- a/README.md +++ b/README.md @@ -446,6 +446,14 @@ Keys and request bodies are never logged. Each request carries ## Development +Refresh the generated client when the API description changes. This does not replace the supported library in this repository. + +```bash +./sync.sh # download the current OpenAPI description into resources/ +./generate.sh # rebuild generated/ from that file +``` + + ```bash python3 -m venv .venv source .venv/bin/activate diff --git a/config.json b/config.json new file mode 100644 index 0000000..85b3671 --- /dev/null +++ b/config.json @@ -0,0 +1,9 @@ +{ + "packageName": "shieldlabs_generated", + "projectName": "shieldlabs-generated", + "packageVersion": "1.0.0", + "packageUrl": "https://github.com/ShieldLabs-ai/shieldlabs-python", + "infoEmail": "contact@shieldlabs.ai", + "generateSourceCodeOnly": true, + "hideGenerationTimestamp": true +} diff --git a/generate.sh b/generate.sh new file mode 100755 index 0000000..4293a5e --- /dev/null +++ b/generate.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + +if ! docker info >/dev/null 2>&1; then + echo "Docker is not running. Start Docker and run this script again." >&2 + exit 1 +fi + +generator="python" +image="openapitools/openapi-generator-cli:v7.23.0" +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT + +python3 - "$PWD/resources/shieldlabs-api.yaml" "$workdir/spec.yaml" << 'PY' +import sys +from pathlib import Path + +source, dest = sys.argv[1:] +lines = Path(source).read_text().splitlines(keepends=True) +out = [] +i = 0 +while i < len(lines): + if lines[i].startswith(" description:"): + out.append(" description: Identification results and risk scoring for your backend.\n") + i += 1 + while i < len(lines) and not (lines[i].startswith(" ") and not lines[i].startswith(" ")): + i += 1 + continue + out.append(lines[i]) + i += 1 +Path(dest).write_text("".join(out)) +PY + +rm -rf generated +mkdir -p generated +cat > generated/.openapi-generator-ignore << 'IGN' +README.md +git_push.sh +.travis.yml +.gitignore +docs/ +test/ +api/openapi.yaml +.github/ +IGN + +docker run --rm -u "$(id -u):$(id -g)" \ + -v "$PWD":/local -v "$workdir":/work -w /local \ + "$image" generate \ + -i /work/spec.yaml \ + -g "$generator" \ + -o /local/generated \ + -c /local/config.json \ + --global-property apis,models,supportingFiles,modelTests=false,apiTests=false,modelDocs=false,apiDocs=false + +find generated \( -name README.md -o -name '*README.md' -o -name git_push.sh -o -name .travis.yml -o -name appveyor.yml -o -name .gitignore -o -name build.sbt -o -name '*.sln' \) -delete +rm -rf generated/docs generated/test generated/.github +find generated -type d -empty -delete + +if [ "$generator" = "go" ]; then + docker run --rm -u "$(id -u):$(id -g)" -v "$PWD/generated":/src -w /src golang:1.24-bookworm gofmt -w . + cat > generated/go.mod << 'MOD' +module github.com/ShieldLabs-ai/shieldlabs-go/generated + +go 1.23 +MOD +fi diff --git a/generated/.openapi-generator-ignore b/generated/.openapi-generator-ignore new file mode 100644 index 0000000..432ff52 --- /dev/null +++ b/generated/.openapi-generator-ignore @@ -0,0 +1,8 @@ +README.md +git_push.sh +.travis.yml +.gitignore +docs/ +test/ +api/openapi.yaml +.github/ diff --git a/generated/.openapi-generator/FILES b/generated/.openapi-generator/FILES new file mode 100644 index 0000000..4a175b3 --- /dev/null +++ b/generated/.openapi-generator/FILES @@ -0,0 +1,26 @@ +shieldlabs_generated/__init__.py +shieldlabs_generated/api/__init__.py +shieldlabs_generated/api/health_api.py +shieldlabs_generated/api/history_api_api.py +shieldlabs_generated/api/management_api_api.py +shieldlabs_generated/api_client.py +shieldlabs_generated/api_response.py +shieldlabs_generated/configuration.py +shieldlabs_generated/exceptions.py +shieldlabs_generated/models/__init__.py +shieldlabs_generated/models/detection_flags.py +shieldlabs_generated/models/domain_profile.py +shieldlabs_generated/models/error_body.py +shieldlabs_generated/models/health_status.py +shieldlabs_generated/models/history_page.py +shieldlabs_generated/models/history_row.py +shieldlabs_generated/models/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/score_detail.py +shieldlabs_generated/models/signal.py +shieldlabs_generated/models/traffic_source.py +shieldlabs_generated/models/webhook_ping_event.py +shieldlabs_generated/rest.py +shieldlabs_generated_README.md diff --git a/generated/.openapi-generator/VERSION b/generated/.openapi-generator/VERSION new file mode 100644 index 0000000..14d6b5d --- /dev/null +++ b/generated/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.23.0 diff --git a/generated/shieldlabs_generated/__init__.py b/generated/shieldlabs_generated/__init__.py new file mode 100644 index 0000000..37cc875 --- /dev/null +++ b/generated/shieldlabs_generated/__init__.py @@ -0,0 +1,81 @@ +# coding: utf-8 + +# flake8: noqa + +""" + 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 + + +__version__ = "1.0.0" + +# Define package exports +__all__ = [ + "HealthApi", + "HistoryAPIApi", + "ManagementAPIApi", + "ApiResponse", + "ApiClient", + "Configuration", + "OpenApiException", + "ApiTypeError", + "ApiValueError", + "ApiKeyError", + "ApiAttributeError", + "ApiException", + "DetectionFlags", + "DomainProfile", + "ErrorBody", + "HealthStatus", + "HistoryPage", + "HistoryRow", + "IdentificationScoredData", + "IdentificationScoredEvent", + "IpInfo", + "LegacySnapshot", + "ScoreDetail", + "Signal", + "TrafficSource", + "WebhookPingEvent", +] + +# import apis into sdk package +from shieldlabs_generated.api.health_api import HealthApi as HealthApi +from shieldlabs_generated.api.history_api_api import HistoryAPIApi as HistoryAPIApi +from shieldlabs_generated.api.management_api_api import ManagementAPIApi as ManagementAPIApi + +# import ApiClient +from shieldlabs_generated.api_response import ApiResponse as ApiResponse +from shieldlabs_generated.api_client import ApiClient as ApiClient +from shieldlabs_generated.configuration import Configuration as Configuration +from shieldlabs_generated.exceptions import OpenApiException as OpenApiException +from shieldlabs_generated.exceptions import ApiTypeError as ApiTypeError +from shieldlabs_generated.exceptions import ApiValueError as ApiValueError +from shieldlabs_generated.exceptions import ApiKeyError as ApiKeyError +from shieldlabs_generated.exceptions import ApiAttributeError as ApiAttributeError +from shieldlabs_generated.exceptions import ApiException as ApiException + +# import models into sdk package +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.health_status import HealthStatus as HealthStatus +from shieldlabs_generated.models.history_page import HistoryPage as HistoryPage +from shieldlabs_generated.models.history_row import HistoryRow as HistoryRow +from shieldlabs_generated.models.identification_scored_data import IdentificationScoredData as IdentificationScoredData +from shieldlabs_generated.models.identification_scored_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.score_detail import ScoreDetail as ScoreDetail +from shieldlabs_generated.models.signal import Signal as Signal +from shieldlabs_generated.models.traffic_source import TrafficSource as TrafficSource +from shieldlabs_generated.models.webhook_ping_event import WebhookPingEvent as WebhookPingEvent + diff --git a/generated/shieldlabs_generated/api/__init__.py b/generated/shieldlabs_generated/api/__init__.py new file mode 100644 index 0000000..566892c --- /dev/null +++ b/generated/shieldlabs_generated/api/__init__.py @@ -0,0 +1,7 @@ +# flake8: noqa + +# import apis into api package +from shieldlabs_generated.api.health_api import HealthApi +from shieldlabs_generated.api.history_api_api import HistoryAPIApi +from shieldlabs_generated.api.management_api_api import ManagementAPIApi + diff --git a/generated/shieldlabs_generated/api/health_api.py b/generated/shieldlabs_generated/api/health_api.py new file mode 100644 index 0000000..59fdc9a --- /dev/null +++ b/generated/shieldlabs_generated/api/health_api.py @@ -0,0 +1,296 @@ +""" + 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 + + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from shieldlabs_generated.models.health_status import HealthStatus + +from shieldlabs_generated.api_client import ApiClient, RequestSerialized +from shieldlabs_generated.api_response import ApiResponse +from shieldlabs_generated.rest import RESTResponseType + + +class HealthApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def get_health( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=2)] = 0, + ) -> HealthStatus: + """Check service health + + Liveness check. Returns `{\"status\":\"ok\"}` while the service answers. Available on both API hosts: `https://account.shieldlabs.ai/health` for the History API and `https://api.shieldlabs.ai/health` for the Management API. No authentication, not rate limited, not billed. + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_health_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "HealthStatus", + '404': "str", + '502': "str", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_health_with_http_info( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=2)] = 0, + ) -> ApiResponse[HealthStatus]: + """Check service health + + Liveness check. Returns `{\"status\":\"ok\"}` while the service answers. Available on both API hosts: `https://account.shieldlabs.ai/health` for the History API and `https://api.shieldlabs.ai/health` for the Management API. No authentication, not rate limited, not billed. + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_health_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "HealthStatus", + '404': "str", + '502': "str", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_health_without_preload_content( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=2)] = 0, + ) -> RESTResponseType: + """Check service health + + Liveness check. Returns `{\"status\":\"ok\"}` while the service answers. Available on both API hosts: `https://account.shieldlabs.ai/health` for the History API and `https://api.shieldlabs.ai/health` for the Management API. No authentication, not rate limited, not billed. + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_health_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "HealthStatus", + '404': "str", + '502': "str", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_health_serialize( + self, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _hosts = [ + 'https://account.shieldlabs.ai', + 'https://api.shieldlabs.ai' + ] + _host = _hosts[_host_index] + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json', + 'text/plain', + 'text/html' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/health', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/generated/shieldlabs_generated/api/history_api_api.py b/generated/shieldlabs_generated/api/history_api_api.py new file mode 100644 index 0000000..dbb26a9 --- /dev/null +++ b/generated/shieldlabs_generated/api/history_api_api.py @@ -0,0 +1,372 @@ +""" + 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 + + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import Field, StrictStr, field_validator +from typing import Optional +from typing_extensions import Annotated +from shieldlabs_generated.models.history_page import HistoryPage + +from shieldlabs_generated.api_client import ApiClient, RequestSerialized +from shieldlabs_generated.api_response import ApiResponse +from shieldlabs_generated.rest import RESTResponseType + + +class HistoryAPIApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def search_history( + self, + search_type: Annotated[StrictStr, Field(description="Identifier to search by. Only these seven values are supported: - `request_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], + value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier, validated on your side before sending: - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`.")], + limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side.")] = None, + offset: Annotated[Optional[Annotated[int, Field(strict=True, ge=0)]], Field(description="Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> HistoryPage: + """Search identifications + + Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `request_id` with `limit=1`. The row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up network checks finish, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. + + :param search_type: Identifier to search by. Only these seven values are supported: - `request_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) + :type search_type: str + :param value: Value of the identifier, validated on your side before sending: - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`. (required) + :type value: str + :param limit: Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side. + :type limit: int + :param offset: Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`. + :type offset: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._search_history_serialize( + search_type=search_type, + value=value, + limit=limit, + offset=offset, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "HistoryPage", + '401': "str", + '404': "str", + '429': "ErrorBody", + '500': "ErrorBody", + '502': "str", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def search_history_with_http_info( + self, + search_type: Annotated[StrictStr, Field(description="Identifier to search by. Only these seven values are supported: - `request_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], + value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier, validated on your side before sending: - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`.")], + limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side.")] = None, + offset: Annotated[Optional[Annotated[int, Field(strict=True, ge=0)]], Field(description="Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> ApiResponse[HistoryPage]: + """Search identifications + + Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `request_id` with `limit=1`. The row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up network checks finish, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. + + :param search_type: Identifier to search by. Only these seven values are supported: - `request_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) + :type search_type: str + :param value: Value of the identifier, validated on your side before sending: - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`. (required) + :type value: str + :param limit: Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side. + :type limit: int + :param offset: Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`. + :type offset: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._search_history_serialize( + search_type=search_type, + value=value, + limit=limit, + offset=offset, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "HistoryPage", + '401': "str", + '404': "str", + '429': "ErrorBody", + '500': "ErrorBody", + '502': "str", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def search_history_without_preload_content( + self, + search_type: Annotated[StrictStr, Field(description="Identifier to search by. Only these seven values are supported: - `request_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side.")], + value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier, validated on your side before sending: - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`.")], + limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side.")] = None, + offset: Annotated[Optional[Annotated[int, Field(strict=True, ge=0)]], Field(description="Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> RESTResponseType: + """Search identifications + + Returns the identifications of your domain that match one identifier, newest first, together with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). **Read one verdict.** After a protected action, search by `request_id` with `limit=1`. The row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up network checks finish, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty `data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as \"wait longer\". The official server SDKs do this for you. **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how many accounts share a device, how many devices one account uses, or what else came from one IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. **Validate before sending.** The server does not validate the path: an unknown `search_type` returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 value returns `500`, and a `limit` outside 1-100 silently becomes 20. **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest version, which makes it the guaranteed read path. Reads are free: they do not use your included identifications. + + :param search_type: Identifier to search by. Only these seven values are supported: - `request_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; - `ip`: every identification from one public IPv4 address; - `session_id`: every identification of one visit; - `cookie_id`: every identification with one browser cookie. The server does not reject other values: it ignores them and returns the latest identifications of the whole domain, so restrict the value on your side. (required) + :type search_type: str + :param value: Value of the identifier, validated on your side before sending: - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it lowercase. - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build this path yourself when yours does. A User HID that contains `/` cannot be searched, and most HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern rejects these values. Hex-encoded hashes need no escaping at all. The server does not validate the value: a malformed UUID or IPv4 address gets a `500`. (required) + :type value: str + :param limit: Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side. + :type limit: int + :param offset: Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`. + :type offset: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._search_history_serialize( + search_type=search_type, + value=value, + limit=limit, + offset=offset, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "HistoryPage", + '401': "str", + '404': "str", + '429': "ErrorBody", + '500': "ErrorBody", + '502': "str", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _search_history_serialize( + self, + search_type, + value, + limit, + offset, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _hosts = [ + 'https://account.shieldlabs.ai' + ] + _host = _hosts[_host_index] + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if search_type is not None: + _path_params['search_type'] = search_type + if value is not None: + _path_params['value'] = value + # process the query parameters + if limit is not None: + + _query_params.append(('limit', limit)) + + if offset is not None: + + _query_params.append(('offset', offset)) + + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json', + 'text/plain', + 'text/html' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'historyApiKey' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/api/v1/history/{search_type}/{value}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/generated/shieldlabs_generated/api/management_api_api.py b/generated/shieldlabs_generated/api/management_api_api.py new file mode 100644 index 0000000..6461fba --- /dev/null +++ b/generated/shieldlabs_generated/api/management_api_api.py @@ -0,0 +1,661 @@ +""" + 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 + + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import Field, StrictStr, field_validator +from typing import List, Optional +from typing_extensions import Annotated +from shieldlabs_generated.models.domain_profile import DomainProfile +from shieldlabs_generated.models.legacy_snapshot import LegacySnapshot + +from shieldlabs_generated.api_client import ApiClient, RequestSerialized +from shieldlabs_generated.api_response import ApiResponse +from shieldlabs_generated.rest import RESTResponseType + + +class ManagementAPIApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def get_domain_profile( + self, + x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> DomainProfile: + """Get the domain profile + + Returns the registered domain, the remaining included identifications of the account and the masked keys. **Credentials.** Send the Secret Key as a Bearer token and the registered domain in `X-Shield-Domain`. The domain is matched exactly: send it lowercase, without scheme, path, trailing slash or a leading `www.`. **Rate limit.** 15 requests per minute per client IP. The request that goes over the limit starts a 10-minute block during which every request to the Management API gets `429`. Call this endpoint sparingly, cache the profile, and never retry a `429`. `Weight` can be negative when the account is over its included volume. The call is free. + + :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) + :type x_shield_domain: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_domain_profile_serialize( + x_shield_domain=x_shield_domain, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "DomainProfile", + '401': None, + '404': "str", + '429': "ErrorBody", + '502': "str", + '503': "ErrorBody", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def get_domain_profile_with_http_info( + self, + x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> ApiResponse[DomainProfile]: + """Get the domain profile + + Returns the registered domain, the remaining included identifications of the account and the masked keys. **Credentials.** Send the Secret Key as a Bearer token and the registered domain in `X-Shield-Domain`. The domain is matched exactly: send it lowercase, without scheme, path, trailing slash or a leading `www.`. **Rate limit.** 15 requests per minute per client IP. The request that goes over the limit starts a 10-minute block during which every request to the Management API gets `429`. Call this endpoint sparingly, cache the profile, and never retry a `429`. `Weight` can be negative when the account is over its included volume. The call is free. + + :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) + :type x_shield_domain: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_domain_profile_serialize( + x_shield_domain=x_shield_domain, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "DomainProfile", + '401': None, + '404': "str", + '429': "ErrorBody", + '502': "str", + '503': "ErrorBody", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def get_domain_profile_without_preload_content( + self, + x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> RESTResponseType: + """Get the domain profile + + Returns the registered domain, the remaining included identifications of the account and the masked keys. **Credentials.** Send the Secret Key as a Bearer token and the registered domain in `X-Shield-Domain`. The domain is matched exactly: send it lowercase, without scheme, path, trailing slash or a leading `www.`. **Rate limit.** 15 requests per minute per client IP. The request that goes over the limit starts a 10-minute block during which every request to the Management API gets `429`. Call this endpoint sparingly, cache the profile, and never retry a `429`. `Weight` can be negative when the account is over its included volume. The call is free. + + :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) + :type x_shield_domain: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_domain_profile_serialize( + x_shield_domain=x_shield_domain, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "DomainProfile", + '401': None, + '404': "str", + '429': "ErrorBody", + '502': "str", + '503': "ErrorBody", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_domain_profile_serialize( + self, + x_shield_domain, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _hosts = [ + 'https://api.shieldlabs.ai' + ] + _host = _hosts[_host_index] + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + if x_shield_domain is not None: + _header_params['X-Shield-Domain'] = x_shield_domain + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json', + 'text/plain', + 'text/html' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'managementSecretKey' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/profile', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def search_history_deprecated( + self, + x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], + type: Annotated[StrictStr, Field(description="Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], + value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text.")], + limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> List[LegacySnapshot]: + """(Deprecated) Search history by identifier (deprecated) + + **Deprecated.** This endpoint stops working after Sat, 01 Jan 2027 00:00:00 GMT. Use `searchHistory` on the History API instead: `https://account.shieldlabs.ai/api/v1/history`. Every answer of this route except `429` and `503` carries `Deprecation: true`, a `Sunset` header and a `Link` header with `rel=\"successor-version\"` pointing there. The plain-text `404` for a path that matches no route and the edge proxy errors do not carry them. Differences from the History API: the answer is a bare array of PascalCase objects; only rows whose request host equals `X-Shield-Domain` are returned (no subdomain traffic); `limit` defaults to 100 and there is no `offset`. It uses the Management API credentials and rate limit (15 requests per minute per client IP, then a 10-minute block). The call is free. + + :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) + :type x_shield_domain: str + :param type: Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) + :type type: str + :param value: Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. (required) + :type value: str + :param limit: Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`. + :type limit: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + warnings.warn("GET /v1/history/{type}/{value} is deprecated.", DeprecationWarning) + + _param = self._search_history_deprecated_serialize( + x_shield_domain=x_shield_domain, + type=type, + value=value, + limit=limit, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[LegacySnapshot]", + '400': "str", + '401': None, + '404': "str", + '429': "ErrorBody", + '502': "str", + '503': "ErrorBody", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def search_history_deprecated_with_http_info( + self, + x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], + type: Annotated[StrictStr, Field(description="Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], + value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text.")], + limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> ApiResponse[List[LegacySnapshot]]: + """(Deprecated) Search history by identifier (deprecated) + + **Deprecated.** This endpoint stops working after Sat, 01 Jan 2027 00:00:00 GMT. Use `searchHistory` on the History API instead: `https://account.shieldlabs.ai/api/v1/history`. Every answer of this route except `429` and `503` carries `Deprecation: true`, a `Sunset` header and a `Link` header with `rel=\"successor-version\"` pointing there. The plain-text `404` for a path that matches no route and the edge proxy errors do not carry them. Differences from the History API: the answer is a bare array of PascalCase objects; only rows whose request host equals `X-Shield-Domain` are returned (no subdomain traffic); `limit` defaults to 100 and there is no `offset`. It uses the Management API credentials and rate limit (15 requests per minute per client IP, then a 10-minute block). The call is free. + + :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) + :type x_shield_domain: str + :param type: Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) + :type type: str + :param value: Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. (required) + :type value: str + :param limit: Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`. + :type limit: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + warnings.warn("GET /v1/history/{type}/{value} is deprecated.", DeprecationWarning) + + _param = self._search_history_deprecated_serialize( + x_shield_domain=x_shield_domain, + type=type, + value=value, + limit=limit, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[LegacySnapshot]", + '400': "str", + '401': None, + '404': "str", + '429': "ErrorBody", + '502': "str", + '503': "ErrorBody", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def search_history_deprecated_without_preload_content( + self, + x_shield_domain: Annotated[str, Field(min_length=1, strict=True, max_length=253, description="Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.")], + type: Annotated[StrictStr, Field(description="Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.")], + value: Annotated[str, Field(min_length=1, strict=True, description="Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text.")], + limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`.")] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0, + ) -> RESTResponseType: + """(Deprecated) Search history by identifier (deprecated) + + **Deprecated.** This endpoint stops working after Sat, 01 Jan 2027 00:00:00 GMT. Use `searchHistory` on the History API instead: `https://account.shieldlabs.ai/api/v1/history`. Every answer of this route except `429` and `503` carries `Deprecation: true`, a `Sunset` header and a `Link` header with `rel=\"successor-version\"` pointing there. The plain-text `404` for a path that matches no route and the edge proxy errors do not carry them. Differences from the History API: the answer is a bare array of PascalCase objects; only rows whose request host equals `X-Shield-Domain` are returned (no subdomain traffic); `limit` defaults to 100 and there is no `offset`. It uses the Management API credentials and rate limit (15 requests per minute per client IP, then a 10-minute block). The call is free. + + :param x_shield_domain: Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. (required) + :type x_shield_domain: str + :param type: Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`. (required) + :type type: str + :param value: Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. (required) + :type value: str + :param limit: Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`. + :type limit: int + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + warnings.warn("GET /v1/history/{type}/{value} is deprecated.", DeprecationWarning) + + _param = self._search_history_deprecated_serialize( + x_shield_domain=x_shield_domain, + type=type, + value=value, + limit=limit, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "List[LegacySnapshot]", + '400': "str", + '401': None, + '404': "str", + '429': "ErrorBody", + '502': "str", + '503': "ErrorBody", + '504': "str", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _search_history_deprecated_serialize( + self, + x_shield_domain, + type, + value, + limit, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _hosts = [ + 'https://api.shieldlabs.ai' + ] + _host = _hosts[_host_index] + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if type is not None: + _path_params['type'] = type + if value is not None: + _path_params['value'] = value + # process the query parameters + if limit is not None: + + _query_params.append(('limit', limit)) + + # process the header parameters + if x_shield_domain is not None: + _header_params['X-Shield-Domain'] = x_shield_domain + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json', + 'text/plain', + 'text/html' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'managementSecretKey' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/history/{type}/{value}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/generated/shieldlabs_generated/api_client.py b/generated/shieldlabs_generated/api_client.py new file mode 100644 index 0000000..a133d54 --- /dev/null +++ b/generated/shieldlabs_generated/api_client.py @@ -0,0 +1,805 @@ +""" + 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 + + + +import datetime +from dateutil.parser import parse +from enum import Enum +import decimal +import json +import mimetypes +import os +import re +import tempfile +import uuid + +from urllib.parse import quote +from typing import Tuple, Optional, List, Dict, Union +from pydantic import SecretStr + +from shieldlabs_generated.configuration import Configuration +from shieldlabs_generated.api_response import ApiResponse, T as ApiResponseT +import shieldlabs_generated.models +from shieldlabs_generated import rest +from shieldlabs_generated.exceptions import ( + ApiValueError, + ApiException, + BadRequestException, + UnauthorizedException, + ForbiddenException, + NotFoundException, + ServiceException +) + +RequestSerialized = Tuple[str, str, Dict[str, str], Optional[str], List[str]] + +class ApiClient: + """Generic API client for OpenAPI client library builds. + + OpenAPI generic API client. This client handles the client- + server communication, and is invariant across implementations. Specifics of + the methods and models for each application are generated from the OpenAPI + templates. + + :param configuration: .Configuration object for this client + :param header_name: a header to pass when making calls to the API. + :param header_value: a header value to pass when making calls to + the API. + :param cookie: a cookie to include in the header when making calls + to the API + """ + + PRIMITIVE_TYPES = (float, bool, bytes, str, int) + NATIVE_TYPES_MAPPING = { + 'int': int, + 'long': int, # TODO remove as only py3 is supported? + 'float': float, + 'str': str, + 'bool': bool, + 'date': datetime.date, + 'datetime': datetime.datetime, + 'decimal': decimal.Decimal, + 'UUID': uuid.UUID, + 'object': object, + } + _pool = None + + def __init__( + self, + configuration=None, + header_name=None, + header_value=None, + cookie=None + ) -> None: + # use default configuration if none is provided + if configuration is None: + configuration = Configuration.get_default() + self.configuration = configuration + + self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} + if header_name is not None: + self.default_headers[header_name] = header_value + self.cookie = cookie + # Set default User-Agent. + self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.client_side_validation = configuration.client_side_validation + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + @property + def user_agent(self): + """User agent for this API client""" + return self.default_headers['User-Agent'] + + @user_agent.setter + def user_agent(self, value): + self.default_headers['User-Agent'] = value + + def set_default_header(self, header_name, header_value): + self.default_headers[header_name] = header_value + + + _default = None + + @classmethod + def get_default(cls): + """Return new instance of ApiClient. + + This method returns newly created, based on default constructor, + object of ApiClient class or returns a copy of default + ApiClient. + + :return: The ApiClient object. + """ + if cls._default is None: + cls._default = ApiClient() + return cls._default + + @classmethod + def set_default(cls, default): + """Set default instance of ApiClient. + + It stores default ApiClient. + + :param default: object of ApiClient. + """ + cls._default = default + + def param_serialize( + self, + method, + resource_path, + path_params=None, + query_params=None, + header_params=None, + body=None, + post_params=None, + files=None, auth_settings=None, + collection_formats=None, + _host=None, + _request_auth=None + ) -> RequestSerialized: + + """Builds the HTTP request params needed by the request. + :param method: Method to call. + :param resource_path: Path to method endpoint. + :param path_params: Path parameters in the url. + :param query_params: Query parameters in the url. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param auth_settings list: Auth Settings names for the request. + :param files dict: key -> filename, value -> filepath, + for `multipart/form-data`. + :param collection_formats: dict of collection formats for path, query, + header, and post parameters. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :return: tuple of form (path, http_method, query_params, header_params, + body, post_params, files) + """ + + config = self.configuration + + # header parameters + header_params = header_params or {} + header_params.update(self.default_headers) + if self.cookie: + header_params['Cookie'] = self.cookie + if header_params: + header_params = self.sanitize_for_serialization(header_params) + header_params = dict( + self.parameters_to_tuples(header_params,collection_formats) + ) + + # path parameters + if path_params: + path_params = self.sanitize_for_serialization(path_params) + path_params = self.parameters_to_tuples( + path_params, + collection_formats + ) + for k, v in path_params: + # specified safe chars, encode everything + resource_path = resource_path.replace( + '{%s}' % k, + quote(str(v), safe=config.safe_chars_for_path_param) + ) + + # post parameters + if post_params or files: + post_params = post_params if post_params else [] + post_params = self.sanitize_for_serialization(post_params) + post_params = self.parameters_to_tuples( + post_params, + collection_formats + ) + if files: + post_params.extend(self.files_parameters(files)) + + # auth setting + self.update_params_for_auth( + header_params, + query_params, + auth_settings, + resource_path, + method, + body, + request_auth=_request_auth + ) + + # body + if body: + body = self.sanitize_for_serialization(body) + + # request url + if _host is None or self.configuration.ignore_operation_servers: + url = self.configuration.host + resource_path + else: + # use server/host defined in path or operation instead + url = _host + resource_path + + # query parameters + if query_params: + query_params = self.sanitize_for_serialization(query_params) + url_query = self.parameters_to_url_query( + query_params, + collection_formats + ) + url += "?" + url_query + + return method, url, header_params, body, post_params + + + def call_api( + self, + method, + url, + header_params=None, + body=None, + post_params=None, + _request_timeout=None + ) -> rest.RESTResponse: + """Makes the HTTP request (synchronous) + :param method: Method to call. + :param url: Path to method endpoint. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param _request_timeout: timeout setting for this request. + :return: RESTResponse + """ + + try: + # perform request and return response + response_data = self.rest_client.request( + method, url, + headers=header_params, + body=body, post_params=post_params, + _request_timeout=_request_timeout + ) + + except ApiException as e: + raise e + + return response_data + + def response_deserialize( + self, + response_data: rest.RESTResponse, + response_types_map: Optional[Dict[str, ApiResponseT]]=None + ) -> ApiResponse[ApiResponseT]: + """Deserializes response into an object. + :param response_data: RESTResponse object to be deserialized. + :param response_types_map: dict of response types. + :return: ApiResponse + """ + + msg = "RESTResponse.read() must be called before passing it to response_deserialize()" + assert response_data.data is not None, msg + + response_type = response_types_map.get(str(response_data.status), None) + if not response_type and isinstance(response_data.status, int) and 100 <= response_data.status <= 599: + # if not found, look for '1XX', '2XX', etc. + response_type = response_types_map.get(str(response_data.status)[0] + "XX", None) + + # deserialize response data + response_text = None + return_data = None + try: + if response_type in ("bytearray", "bytes"): + return_data = response_data.data + elif response_type == "file": + return_data = self.__deserialize_file(response_data) + elif response_type is not None: + match = None + content_type = response_data.headers.get('content-type') + if content_type is not None: + match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) + encoding = match.group(1) if match else "utf-8" + response_text = response_data.data.decode(encoding) + return_data = self.deserialize(response_text, response_type, content_type) + finally: + if not 200 <= response_data.status <= 299: + raise ApiException.from_response( + http_resp=response_data, + body=response_text, + data=return_data, + ) + + return ApiResponse( + status_code = response_data.status, + data = return_data, + headers = response_data.headers, + raw_data = response_data.data + ) + + def sanitize_for_serialization(self, obj): + """Builds a JSON POST object. + + If obj is None, return None. + If obj is SecretStr, return obj.get_secret_value() + If obj is str, int, long, float, bool, return directly. + If obj is datetime.datetime, datetime.date + convert to string in iso8601 format. + If obj is decimal.Decimal return string representation. + If obj is list, sanitize each element in the list. + If obj is dict, return the dict. + If obj is OpenAPI model, return the properties dict. + + :param obj: The data to serialize. + :return: The serialized form of data. + """ + if obj is None: + return None + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, SecretStr): + return obj.get_secret_value() + elif isinstance(obj, self.PRIMITIVE_TYPES): + return obj + elif isinstance(obj, uuid.UUID): + return str(obj) + elif isinstance(obj, list): + return [ + self.sanitize_for_serialization(sub_obj) for sub_obj in obj + ] + elif isinstance(obj, tuple): + return tuple( + self.sanitize_for_serialization(sub_obj) for sub_obj in obj + ) + elif isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + elif isinstance(obj, decimal.Decimal): + return str(obj) + elif isinstance(obj, dict): + return { + key: self.sanitize_for_serialization(val) + for key, val in obj.items() + } + + # Convert model obj to dict except + # attributes `openapi_types`, `attribute_map` + # and attributes which value is not None. + # Convert attribute name to json key in + # model definition for request. + if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')): + obj_dict = obj.to_dict() + else: + obj_dict = obj.__dict__ + + return self.sanitize_for_serialization(obj_dict) + + + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): + """Deserializes response into an object. + + :param response: RESTResponse object to be deserialized. + :param response_type: class literal for + deserialized object, or string of class name. + :param content_type: content type of response. + + :return: deserialized object. + """ + + # fetch data from response object + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif re.match(r'^application/(json|[\w!#$&.+\-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif re.match(r'^text\/[a-z.+-]+\s*(;|$)', content_type, re.IGNORECASE): + data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) + + return self.__deserialize(data, response_type) + + def __deserialize(self, data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + + if isinstance(klass, str): + if klass.startswith('List['): + m = re.match(r'List\[(.*)]', klass) + assert m is not None, "Malformed List type definition" + sub_kls = m.group(1) + return [self.__deserialize(sub_data, sub_kls) + for sub_data in data] + + if klass.startswith('Dict['): + m = re.match(r'Dict\[([^,]*), (.*)]', klass) + assert m is not None, "Malformed Dict type definition" + sub_kls = m.group(2) + return {k: self.__deserialize(v, sub_kls) + for k, v in data.items()} + + # convert str to class + if klass in self.NATIVE_TYPES_MAPPING: + klass = self.NATIVE_TYPES_MAPPING[klass] + else: + klass = getattr(shieldlabs_generated.models, klass) + + if klass in self.PRIMITIVE_TYPES: + return self.__deserialize_primitive(data, klass) + elif klass is object: + return self.__deserialize_object(data) + elif klass is datetime.date: + return self.__deserialize_date(data) + elif klass is datetime.datetime: + return self.__deserialize_datetime(data) + elif klass is decimal.Decimal: + return decimal.Decimal(data) + elif klass is uuid.UUID: + return uuid.UUID(data) + elif issubclass(klass, Enum): + return self.__deserialize_enum(data, klass) + else: + return self.__deserialize_model(data, klass) + + def parameters_to_tuples(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: Parameters as list of tuples, collections formatted + """ + new_params: List[Tuple[str, str]] = [] + if collection_formats is None: + collection_formats = {} + for k, v in params.items() if isinstance(params, dict) else params: + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, value) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(str(value) for value in v))) + else: + new_params.append((k, v)) + return new_params + + def parameters_to_url_query(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: URL query string (e.g. a=Hello%20World&b=123) + """ + new_params: List[Tuple[str, str]] = [] + if collection_formats is None: + collection_formats = {} + for k, v in params.items() if isinstance(params, dict) else params: + if isinstance(v, bool): + v = str(v).lower() + if isinstance(v, (int, float)): + v = str(v) + if isinstance(v, dict): + v = json.dumps(v) + + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, quote(str(value))) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(quote(str(value)) for value in v)) + ) + else: + new_params.append((k, quote(str(v)))) + + return "&".join(["=".join(map(str, item)) for item in new_params]) + + def files_parameters( + self, + files: Dict[str, Union[str, bytes, List[str], List[bytes], Tuple[str, bytes]]], + ): + """Builds form parameters. + + :param files: File parameters. + :return: Form parameters with files. + """ + params = [] + for k, v in files.items(): + if isinstance(v, str): + with open(v, 'rb') as f: + filename = os.path.basename(f.name) + filedata = f.read() + elif isinstance(v, bytes): + filename = k + filedata = v + elif isinstance(v, tuple): + filename, filedata = v + elif isinstance(v, list): + for file_param in v: + params.extend(self.files_parameters({k: file_param})) + continue + else: + raise ValueError("Unsupported file value") + mimetype = ( + mimetypes.guess_type(filename)[0] + or 'application/octet-stream' + ) + params.append( + tuple([k, tuple([filename, filedata, mimetype])]) + ) + return params + + def select_header_accept(self, accepts: List[str]) -> Optional[str]: + """Returns `Accept` based on an array of accepts provided. + + :param accepts: List of headers. + :return: Accept (e.g. application/json). + """ + if not accepts: + return None + + for accept in accepts: + if re.search('json', accept, re.IGNORECASE): + return accept + + return accepts[0] + + def select_header_content_type(self, content_types): + """Returns `Content-Type` based on an array of content_types provided. + + :param content_types: List of content-types. + :return: Content-Type (e.g. application/json). + """ + if not content_types: + return None + + for content_type in content_types: + if re.search('json', content_type, re.IGNORECASE): + return content_type + + return content_types[0] + + def update_params_for_auth( + self, + headers, + queries, + auth_settings, + resource_path, + method, + body, + request_auth=None + ) -> None: + """Updates header and query params based on authentication setting. + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :param auth_settings: Authentication setting identifiers list. + :resource_path: A string representation of the HTTP request resource path. + :method: A string representation of the HTTP request method. + :body: A object representing the body of the HTTP request. + The object type is the return value of sanitize_for_serialization(). + :param request_auth: if set, the provided settings will + override the token in the configuration. + """ + if not auth_settings: + return + + if request_auth: + self._apply_auth_params( + headers, + queries, + resource_path, + method, + body, + request_auth + ) + else: + for auth in auth_settings: + auth_setting = self.configuration.auth_settings().get(auth) + if auth_setting: + self._apply_auth_params( + headers, + queries, + resource_path, + method, + body, + auth_setting + ) + + def _apply_auth_params( + self, + headers, + queries, + resource_path, + method, + body, + auth_setting + ) -> None: + """Updates the request parameters based on a single auth_setting + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :resource_path: A string representation of the HTTP request resource path. + :method: A string representation of the HTTP request method. + :body: A object representing the body of the HTTP request. + The object type is the return value of sanitize_for_serialization(). + :param auth_setting: auth settings for the endpoint + """ + if auth_setting['in'] == 'cookie': + headers['Cookie'] = auth_setting['value'] + elif auth_setting['in'] == 'header': + if auth_setting['type'] != 'http-signature': + headers[auth_setting['key']] = auth_setting['value'] + elif auth_setting['in'] == 'query': + queries.append((auth_setting['key'], auth_setting['value'])) + else: + raise ApiValueError( + 'Authentication token must be in `query` or `header`' + ) + + def __deserialize_file(self, response): + """Deserializes body to file + + Saves response body into a file in a temporary folder, + using the filename from the `Content-Disposition` header if provided. + + handle file downloading + save response body into a tmp file and return the instance + + :param response: RESTResponse. + :return: file path. + """ + fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path) + os.close(fd) + os.remove(path) + + content_disposition = response.headers.get("Content-Disposition") + if content_disposition: + m = re.search( + r'filename=[\'"]?([^\'"\s]+)[\'"]?', + content_disposition + ) + assert m is not None, "Unexpected 'content-disposition' header value" + filename = os.path.basename(m.group(1)) # Strip any directory traversal + if filename in ("", ".", ".."): # fall back to tmp filename + filename = os.path.basename(path) + path = os.path.join(os.path.dirname(path), filename) + + with open(path, "wb") as f: + f.write(response.data) + + return path + + def __deserialize_primitive(self, data, klass): + """Deserializes string to primitive type. + + :param data: str. + :param klass: class literal. + + :return: int, long, float, str, bool. + """ + try: + return klass(data) + except UnicodeEncodeError: + return str(data) + except TypeError: + return data + + def __deserialize_object(self, value): + """Return an original value. + + :return: object. + """ + return value + + def __deserialize_date(self, string): + """Deserializes string to date. + + :param string: str. + :return: date. + """ + try: + return parse(string).date() + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason="Failed to parse `{0}` as date object".format(string) + ) + + def __deserialize_datetime(self, string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :return: datetime. + """ + try: + return parse(string) + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as datetime object" + .format(string) + ) + ) + + def __deserialize_enum(self, data, klass): + """Deserializes primitive type to enum. + + :param data: primitive type. + :param klass: class literal. + :return: enum value. + """ + try: + return klass(data) + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as `{1}`" + .format(data, klass) + ) + ) + + def __deserialize_model(self, data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :param klass: class literal. + :return: model object. + """ + + return klass.from_dict(data) diff --git a/generated/shieldlabs_generated/api_response.py b/generated/shieldlabs_generated/api_response.py new file mode 100644 index 0000000..9bc7c11 --- /dev/null +++ b/generated/shieldlabs_generated/api_response.py @@ -0,0 +1,21 @@ +"""API response object.""" + +from __future__ import annotations +from typing import Optional, Generic, Mapping, TypeVar +from pydantic import Field, StrictInt, StrictBytes, BaseModel + +T = TypeVar("T") + +class ApiResponse(BaseModel, Generic[T]): + """ + API response object + """ + + status_code: StrictInt = Field(description="HTTP status code") + headers: Optional[Mapping[str, str]] = Field(None, description="HTTP headers") + data: T = Field(description="Deserialized data given the data type") + raw_data: StrictBytes = Field(description="Raw data (HTTP response body)") + + model_config = { + "arbitrary_types_allowed": True + } diff --git a/generated/shieldlabs_generated/configuration.py b/generated/shieldlabs_generated/configuration.py new file mode 100644 index 0000000..77ac63d --- /dev/null +++ b/generated/shieldlabs_generated/configuration.py @@ -0,0 +1,619 @@ +""" + 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 + + +import copy +import http.client as httplib +import logging +from logging import FileHandler +import multiprocessing +import sys +from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union +from typing_extensions import NotRequired, Self + +import urllib3 + + +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + +ServerVariablesT = Dict[str, str] + +GenericAuthSetting = TypedDict( + "GenericAuthSetting", + { + "type": str, + "in": str, + "key": str, + "value": str, + }, +) + + +OAuth2AuthSetting = TypedDict( + "OAuth2AuthSetting", + { + "type": Literal["oauth2"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +APIKeyAuthSetting = TypedDict( + "APIKeyAuthSetting", + { + "type": Literal["api_key"], + "in": str, + "key": str, + "value": Optional[str], + }, +) + + +BasicAuthSetting = TypedDict( + "BasicAuthSetting", + { + "type": Literal["basic"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": Optional[str], + }, +) + + +BearerFormatAuthSetting = TypedDict( + "BearerFormatAuthSetting", + { + "type": Literal["bearer"], + "in": Literal["header"], + "format": Literal["JWT"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +BearerAuthSetting = TypedDict( + "BearerAuthSetting", + { + "type": Literal["bearer"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +HTTPSignatureAuthSetting = TypedDict( + "HTTPSignatureAuthSetting", + { + "type": Literal["http-signature"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": None, + }, +) + + +AuthSettings = TypedDict( + "AuthSettings", + { + "historyApiKey": BearerFormatAuthSetting, + "managementSecretKey": BearerAuthSetting, + }, + total=False, +) + + +class HostSettingVariable(TypedDict): + description: str + default_value: str + enum_values: List[str] + + +class HostSetting(TypedDict): + url: str + description: str + variables: NotRequired[Dict[str, HostSettingVariable]] + + +class Configuration: + """This class contains various settings of the API client. + + :param host: Base url. + :param ignore_operation_servers + Boolean to ignore operation servers for the API client. + Config will use `host` as the base url regardless of the operation servers. + :param api_key: Dict to store API key(s). + Each entry in the dict specifies an API key. + The dict key is the name of the security scheme in the OAS specification. + The dict value is the API key secret. + :param api_key_prefix: Dict to store API prefix (e.g. Bearer). + The dict key is the name of the security scheme in the OAS specification. + The dict value is an API key prefix when generating the auth data. + :param username: Username for HTTP basic authentication. + :param password: Password for HTTP basic authentication. + :param access_token: Access token. + :param server_index: Index to servers configuration. + :param server_variables: Mapping with string values to replace variables in + templated server configuration. The validation of enums is performed for + variables with defined enum values before. + :param server_operation_index: Mapping from operation ID to an index to server + configuration. + :param server_operation_variables: Mapping from operation ID to a mapping with + string values to replace variables in templated server configuration. + The validation of enums is performed for variables with defined enum + values before. + :param verify_ssl: bool - Set this to false to skip verifying SSL certificate + when calling API from https server. + :param ssl_ca_cert: str - the path to a file of concatenated CA certificates + in PEM format. + :param retries: int | urllib3.util.retry.Retry - Retry configuration. + :param ca_cert_data: verify the peer using concatenated CA certificate data + in PEM (str) or DER (bytes) format. + :param cert_file: the path to a client certificate file, for mTLS. + :param key_file: the path to a client key file, for mTLS. + :param assert_hostname: Set this to True/False to enable/disable SSL hostname verification. + :param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server. + :param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync. + :param proxy: Proxy URL. + :param proxy_headers: Proxy headers. + :param safe_chars_for_path_param: Safe characters for path parameter encoding. + :param client_side_validation: Enable client-side validation. Default True. + :param socket_options: Options to pass down to the underlying urllib3 socket. + :param datetime_format: Datetime format string for serialization. + :param date_format: Date format string for serialization. + + :Example: + """ + + _default: ClassVar[Optional[Self]] = None + + def __init__( + self, + host: Optional[str]=None, + api_key: Optional[Dict[str, str]]=None, + api_key_prefix: Optional[Dict[str, str]]=None, + username: Optional[str]=None, + password: Optional[str]=None, + access_token: Optional[str]=None, + server_index: Optional[int]=None, + server_variables: Optional[ServerVariablesT]=None, + server_operation_index: Optional[Dict[int, int]]=None, + server_operation_variables: Optional[Dict[int, ServerVariablesT]]=None, + ignore_operation_servers: bool=False, + ssl_ca_cert: Optional[str]=None, + retries: Optional[Union[int, urllib3.util.retry.Retry]] = None, + ca_cert_data: Optional[Union[str, bytes]] = None, + cert_file: Optional[str]=None, + key_file: Optional[str]=None, + verify_ssl: bool=True, + assert_hostname: Optional[bool]=None, + tls_server_name: Optional[str]=None, + connection_pool_maxsize: Optional[int]=None, + proxy: Optional[str]=None, + proxy_headers: Optional[Any]=None, + safe_chars_for_path_param: str='', + client_side_validation: bool=True, + socket_options: Optional[Any]=None, + datetime_format: str="%Y-%m-%dT%H:%M:%S.%f%z", + date_format: str="%Y-%m-%d", + *, + debug: Optional[bool] = None, + ) -> None: + """Constructor + """ + self._base_path = "https://account.shieldlabs.ai" if host is None else host + """Default Base url + """ + self.server_index = 0 if server_index is None and host is None else server_index + self.server_operation_index = server_operation_index or {} + """Default server index + """ + self.server_variables = server_variables or {} + self.server_operation_variables = server_operation_variables or {} + """Default server variables + """ + self.ignore_operation_servers = ignore_operation_servers + """Ignore operation servers + """ + self.temp_folder_path = None + """Temp file folder for downloading files + """ + # Authentication Settings + self.api_key = {} + if api_key: + self.api_key = api_key + """dict to store API key(s) + """ + self.api_key_prefix = {} + if api_key_prefix: + self.api_key_prefix = api_key_prefix + """dict to store API prefix (e.g. Bearer) + """ + self.refresh_api_key_hook = None + """function hook to refresh API key if expired + """ + self.username = username + """Username for HTTP basic authentication + """ + self.password = password + """Password for HTTP basic authentication + """ + self.access_token = access_token + """Access token + """ + self.logger = {} + """Logging Settings + """ + self.logger["package_logger"] = logging.getLogger("shieldlabs_generated") + self.logger["urllib3_logger"] = logging.getLogger("urllib3") + self.logger_format = '%(asctime)s %(levelname)s %(message)s' + """Log format + """ + self.logger_stream_handler = None + """Log stream handler + """ + self.logger_file_handler: Optional[FileHandler] = None + """Log file handler + """ + self.logger_file = None + """Debug file location + """ + if debug is not None: + self.debug = debug + else: + self.__debug = False + """Debug switch + """ + + self.verify_ssl = verify_ssl + """SSL/TLS verification + Set this to false to skip verifying SSL certificate when calling API + from https server. + """ + self.ssl_ca_cert = ssl_ca_cert + """Set this to customize the certificate file to verify the peer. + """ + self.ca_cert_data = ca_cert_data + """Set this to verify the peer using PEM (str) or DER (bytes) + certificate data. + """ + self.cert_file = cert_file + """client certificate file + """ + self.key_file = key_file + """client key file + """ + self.assert_hostname = assert_hostname + """Set this to True/False to enable/disable SSL hostname verification. + """ + self.tls_server_name = tls_server_name + """SSL/TLS Server Name Indication (SNI) + Set this to the SNI value expected by the server. + """ + + self.connection_pool_maxsize = connection_pool_maxsize if connection_pool_maxsize is not None else multiprocessing.cpu_count() * 5 + """urllib3 connection pool's maximum number of connections saved + per pool. None in the constructor is coerced to cpu_count * 5. + """ + + self.proxy = proxy + """Proxy URL + """ + self.proxy_headers = proxy_headers + """Proxy headers + """ + self.safe_chars_for_path_param = safe_chars_for_path_param + """Safe chars for path_param + """ + self.retries = retries + """Retry configuration + """ + # Enable client side validation + self.client_side_validation = client_side_validation + + self.socket_options = socket_options + """Options to pass down to the underlying urllib3 socket + """ + + self.datetime_format = datetime_format + """datetime format + """ + + self.date_format = date_format + """date format + """ + + def __deepcopy__(self, memo: Dict[int, Any]) -> Self: + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k not in ('logger', 'logger_file_handler'): + setattr(result, k, copy.deepcopy(v, memo)) + # shallow copy of loggers + result.logger = copy.copy(self.logger) + # use setters to configure loggers + result.logger_file = self.logger_file + result.debug = self.debug + return result + + def __setattr__(self, name: str, value: Any) -> None: + object.__setattr__(self, name, value) + + @classmethod + def set_default(cls, default: Optional[Self]) -> None: + """Set default instance of configuration. + + It stores default configuration, which can be + returned by get_default_copy method. + + :param default: object of Configuration + """ + cls._default = default + + @classmethod + def get_default_copy(cls) -> Self: + """Deprecated. Please use `get_default` instead. + + Deprecated. Please use `get_default` instead. + + :return: The configuration object. + """ + return cls.get_default() + + @classmethod + def get_default(cls) -> Self: + """Return the default configuration. + + This method returns newly created, based on default constructor, + object of Configuration class or returns a copy of default + configuration. + + :return: The configuration object. + """ + if cls._default is None: + cls._default = cls() + return cls._default + + @property + def logger_file(self) -> Optional[str]: + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + return self.__logger_file + + @logger_file.setter + def logger_file(self, value: Optional[str]) -> None: + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + self.__logger_file = value + if self.__logger_file: + # If set logging file, + # then add file handler and remove stream handler. + self.logger_file_handler = logging.FileHandler(self.__logger_file) + self.logger_file_handler.setFormatter(self.logger_formatter) + for _, logger in self.logger.items(): + logger.addHandler(self.logger_file_handler) + + @property + def debug(self) -> bool: + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + return self.__debug + + @debug.setter + def debug(self, value: bool) -> None: + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + self.__debug = value + if self.__debug: + # if debug status is True, turn on debug logging + for _, logger in self.logger.items(): + logger.setLevel(logging.DEBUG) + # turn on httplib debug + httplib.HTTPConnection.debuglevel = 1 + else: + # if debug status is False, turn off debug logging, + # setting log level to default `logging.WARNING` + for _, logger in self.logger.items(): + logger.setLevel(logging.WARNING) + # turn off httplib debug + httplib.HTTPConnection.debuglevel = 0 + + @property + def logger_format(self) -> str: + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + return self.__logger_format + + @logger_format.setter + def logger_format(self, value: str) -> None: + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + self.__logger_format = value + self.logger_formatter = logging.Formatter(self.__logger_format) + + def get_api_key_with_prefix(self, identifier: str, alias: Optional[str]=None) -> Optional[str]: + """Gets API key (with prefix if set). + + :param identifier: The identifier of apiKey. + :param alias: The alternative identifier of apiKey. + :return: The token for api key authentication. + """ + if self.refresh_api_key_hook is not None: + self.refresh_api_key_hook(self) + key = self.api_key.get(identifier, self.api_key.get(alias) if alias is not None else None) + if key: + prefix = self.api_key_prefix.get(identifier) + if prefix: + return "%s %s" % (prefix, key) + else: + return key + + return None + + def get_basic_auth_token(self) -> Optional[str]: + """Gets HTTP basic authentication header (string). + + :return: The token for basic HTTP authentication. + """ + username = "" + if self.username is not None: + username = self.username + password = "" + if self.password is not None: + password = self.password + + return urllib3.util.make_headers( + basic_auth=username + ':' + password + ).get('authorization') + + def auth_settings(self)-> AuthSettings: + """Gets Auth Settings dict for api client. + + :return: The Auth Settings information dict. + """ + auth: AuthSettings = {} + if self.access_token is not None: + auth['historyApiKey'] = { + 'type': 'bearer', + 'in': 'header', + 'format': 'sec_xxxxxxxx-xxxxxxxx-xxxxxxxx', + 'key': 'Authorization', + 'value': 'Bearer ' + self.access_token + } + if self.access_token is not None: + auth['managementSecretKey'] = { + 'type': 'bearer', + 'in': 'header', + 'key': 'Authorization', + 'value': 'Bearer ' + self.access_token + } + return auth + + def to_debug_report(self) -> str: + """Gets the essential information for debugging. + + :return: The report for debugging. + """ + return "Python SDK Debug Report:\n"\ + "OS: {env}\n"\ + "Python Version: {pyversion}\n"\ + "Version of the API: 1.0.1\n"\ + "SDK Package Version: 1.0.0".\ + format(env=sys.platform, pyversion=sys.version) + + def get_host_settings(self) -> List[HostSetting]: + """Gets an array of host settings + + :return: An array of host settings + """ + return [ + { + 'url': "https://account.shieldlabs.ai", + 'description': "History API (every operation also declares its own server)", + }, + { + 'url': "https://api.shieldlabs.ai", + 'description': "Management API (every operation also declares its own server)", + } + ] + + def get_host_from_settings( + self, + index: Optional[int], + variables: Optional[ServerVariablesT]=None, + servers: Optional[List[HostSetting]]=None, + ) -> str: + """Gets host URL based on the index and variables + :param index: array index of the host settings + :param variables: hash of variable and the corresponding value + :param servers: an array of host settings or None + :return: URL based on host settings + """ + if index is None: + return self._base_path + + variables = {} if variables is None else variables + servers = self.get_host_settings() if servers is None else servers + + try: + server = servers[index] + except IndexError: + raise ValueError( + "Invalid index {0} when selecting the host settings. " + "Must be less than {1}".format(index, len(servers))) + + url = server['url'] + + # go through variables and replace placeholders + for variable_name, variable in server.get('variables', {}).items(): + used_value = variables.get( + variable_name, variable['default_value']) + + if 'enum_values' in variable \ + and variable['enum_values'] \ + and used_value not in variable['enum_values']: + raise ValueError( + "The variable `{0}` in the host URL has invalid value " + "{1}. Must be {2}.".format( + variable_name, variables[variable_name], + variable['enum_values'])) + + url = url.replace("{" + variable_name + "}", used_value) + + return url + + @property + def host(self) -> str: + """Return generated host.""" + return self.get_host_from_settings(self.server_index, variables=self.server_variables) + + @host.setter + def host(self, value: str) -> None: + """Fix base path.""" + self._base_path = value + self.server_index = None diff --git a/generated/shieldlabs_generated/exceptions.py b/generated/shieldlabs_generated/exceptions.py new file mode 100644 index 0000000..593d758 --- /dev/null +++ b/generated/shieldlabs_generated/exceptions.py @@ -0,0 +1,219 @@ +""" + 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 typing import Any, Optional +from typing_extensions import Self + +class OpenApiException(Exception): + """The base exception class for all OpenAPIExceptions""" + + +class ApiTypeError(OpenApiException, TypeError): + def __init__(self, msg, path_to_item=None, valid_classes=None, + key_type=None) -> None: + """ Raises an exception for TypeErrors + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list): a list of keys an indices to get to the + current_item + None if unset + valid_classes (tuple): the primitive classes that current item + should be an instance of + None if unset + key_type (bool): False if our value is a value in a dict + True if it is a key in a dict + False if our item is an item in a list + None if unset + """ + self.path_to_item = path_to_item + self.valid_classes = valid_classes + self.key_type = key_type + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiTypeError, self).__init__(full_msg) + + +class ApiValueError(OpenApiException, ValueError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list) the path to the exception in the + received_data dict. None if unset + """ + + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiValueError, self).__init__(full_msg) + + +class ApiAttributeError(OpenApiException, AttributeError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Raised when an attribute reference or assignment fails. + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiAttributeError, self).__init__(full_msg) + + +class ApiKeyError(OpenApiException, KeyError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiKeyError, self).__init__(full_msg) + + +class ApiException(OpenApiException): + + def __init__( + self, + status=None, + reason=None, + http_resp=None, + *, + body: Optional[str] = None, + data: Optional[Any] = None, + ) -> None: + self.status = status + self.reason = reason + self.body = body + self.data = data + self.headers = None + + if http_resp: + if self.status is None: + self.status = http_resp.status + if self.reason is None: + self.reason = http_resp.reason + if self.body is None: + try: + self.body = http_resp.data.decode('utf-8') + except Exception: + pass + self.headers = http_resp.headers + + @classmethod + def from_response( + cls, + *, + http_resp, + body: Optional[str], + data: Optional[Any], + ) -> Self: + if http_resp.status == 400: + raise BadRequestException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 401: + raise UnauthorizedException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 403: + raise ForbiddenException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 404: + raise NotFoundException(http_resp=http_resp, body=body, data=data) + + # Added new conditions for 409 and 422 + if http_resp.status == 409: + raise ConflictException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 422: + raise UnprocessableEntityException(http_resp=http_resp, body=body, data=data) + + if 500 <= http_resp.status <= 599: + raise ServiceException(http_resp=http_resp, body=body, data=data) + raise ApiException(http_resp=http_resp, body=body, data=data) + + def __str__(self): + """Custom error messages for exception""" + error_message = "({0})\n"\ + "Reason: {1}\n".format(self.status, self.reason) + if self.headers: + error_message += "HTTP response headers: {0}\n".format( + self.headers) + + if self.body: + error_message += "HTTP response body: {0}\n".format(self.body) + + if self.data: + error_message += "HTTP response data: {0}\n".format(self.data) + + return error_message + + +class BadRequestException(ApiException): + pass + + +class NotFoundException(ApiException): + pass + + +class UnauthorizedException(ApiException): + pass + + +class ForbiddenException(ApiException): + pass + + +class ServiceException(ApiException): + pass + + +class ConflictException(ApiException): + """Exception for HTTP 409 Conflict.""" + pass + + +class UnprocessableEntityException(ApiException): + """Exception for HTTP 422 Unprocessable Entity.""" + pass + + +def render_path(path_to_item): + """Returns a string representation of a path""" + result = "" + for pth in path_to_item: + if isinstance(pth, int): + result += "[{0}]".format(pth) + else: + result += "['{0}']".format(pth) + return result diff --git a/generated/shieldlabs_generated/models/__init__.py b/generated/shieldlabs_generated/models/__init__.py new file mode 100644 index 0000000..9560d31 --- /dev/null +++ b/generated/shieldlabs_generated/models/__init__.py @@ -0,0 +1,31 @@ +# coding: utf-8 + +# flake8: noqa +""" + 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 + +# import models into model package +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.health_status import HealthStatus +from shieldlabs_generated.models.history_page import HistoryPage +from shieldlabs_generated.models.history_row import HistoryRow +from shieldlabs_generated.models.identification_scored_data import IdentificationScoredData +from shieldlabs_generated.models.identification_scored_event import IdentificationScoredEvent +from shieldlabs_generated.models.ip_info import IpInfo +from shieldlabs_generated.models.legacy_snapshot import LegacySnapshot +from shieldlabs_generated.models.score_detail import ScoreDetail +from shieldlabs_generated.models.signal import Signal +from shieldlabs_generated.models.traffic_source import TrafficSource +from shieldlabs_generated.models.webhook_ping_event import WebhookPingEvent + diff --git a/generated/shieldlabs_generated/models/detection_flags.py b/generated/shieldlabs_generated/models/detection_flags.py new file mode 100644 index 0000000..775aeb7 --- /dev/null +++ b/generated/shieldlabs_generated/models/detection_flags.py @@ -0,0 +1,125 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class DetectionFlags(BaseModel): + """ + Stable yes/no verdicts for the identification. Always all 19 keys. Branch on these flags and on the Risk Score; signal names are for display and logging. When `search_bot` is `true`, `incognito`, `check_incomplete`, `ip_mismatch` and `javascript_disabled` are always `false`. + """ # noqa: E501 + vpn: StrictBool = Field(description="A VPN was detected (scored `vpn` signal).") + privacy_relay: StrictBool = Field(description="A privacy relay such as iCloud Private Relay was detected.") + browser_vpn_proxy: StrictBool = Field(description="A VPN or proxy built into the browser or one of its extensions. `true` exactly when `connection_type` is `browser_vpn_proxy`.") + tor: StrictBool = Field(description="The request came through the Tor network.") + 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_not_detected: StrictBool = Field(description="The operating system could not be determined from the User-Agent or the network.") + timezone_mismatch: StrictBool = Field(description="The browser timezone differs from the timezone of the IP location.") + anti_detect_browser: StrictBool = Field(description="An anti-detect browser was detected.") + browser_automation: StrictBool = Field(description="Browser automation was detected, for example a WebDriver-controlled browser.") + ip_mismatch: StrictBool = Field(description="The public IP differs from the local IP found by the browser network check. Informational: it does not add to the score.") + incognito: StrictBool = Field(description="The browser runs in a private window.") + search_bot: StrictBool = Field(description="A search-engine crawler. Its Risk Score is always 0.") + suspicious_paid_click: StrictBool = Field(description="The visit came from a paid ad click (Google Ads, Meta, TikTok, Microsoft Ads, LinkedIn, Pinterest or X) and the Risk Score is 60 or more (the 999 marker included).") + javascript_disabled: StrictBool = Field(description="JavaScript, or the browser APIs the checks need, were unavailable.") + stun_not_checked: StrictBool = Field(description="The browser network (STUN) check did not complete. Cleared again when a late network result arrives.") + check_incomplete: StrictBool = Field(description="Part of the browser checks timed out, so the verdict rests on partial data. Informational.") + __properties: ClassVar[List[str]] = ["vpn", "privacy_relay", "browser_vpn_proxy", "tor", "proxy", "datacenter_ip", "abuser", "os_mismatch", "os_not_detected", "timezone_mismatch", "anti_detect_browser", "browser_automation", "ip_mismatch", "incognito", "search_bot", "suspicious_paid_click", "javascript_disabled", "stun_not_checked", "check_incomplete"] + + 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 DetectionFlags 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 DetectionFlags from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "vpn": obj.get("vpn"), + "privacy_relay": obj.get("privacy_relay"), + "browser_vpn_proxy": obj.get("browser_vpn_proxy"), + "tor": obj.get("tor"), + "proxy": obj.get("proxy"), + "datacenter_ip": obj.get("datacenter_ip"), + "abuser": obj.get("abuser"), + "os_mismatch": obj.get("os_mismatch"), + "os_not_detected": obj.get("os_not_detected"), + "timezone_mismatch": obj.get("timezone_mismatch"), + "anti_detect_browser": obj.get("anti_detect_browser"), + "browser_automation": obj.get("browser_automation"), + "ip_mismatch": obj.get("ip_mismatch"), + "incognito": obj.get("incognito"), + "search_bot": obj.get("search_bot"), + "suspicious_paid_click": obj.get("suspicious_paid_click"), + "javascript_disabled": obj.get("javascript_disabled"), + "stun_not_checked": obj.get("stun_not_checked"), + "check_incomplete": obj.get("check_incomplete") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/domain_profile.py b/generated/shieldlabs_generated/models/domain_profile.py new file mode 100644 index 0000000..eff29b2 --- /dev/null +++ b/generated/shieldlabs_generated/models/domain_profile.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, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class DomainProfile(BaseModel): + """ + Profile of the registered domain. The keys are PascalCase on the wire. Ignore keys you do not know. + """ # noqa: E501 + domain: StrictStr = Field(description="The registered domain, as sent in `X-Shield-Domain`.", alias="Domain") + weight: StrictInt = Field(description="Remaining included identifications of the account (shared by its domains). Can be negative when the account is over its included volume.", alias="Weight") + callback: StrictStr = Field(description="Legacy field kept for compatibility, normally an empty string. Webhook deliveries do not use it: configure webhook endpoints in the analytics dashboard.", alias="Callback") + public_key: Annotated[str, Field(strict=True)] = Field(description="The domain's Public Key, masked.", alias="PublicKey") + secret: Annotated[str, Field(strict=True)] = Field(description="The domain's Secret Key, masked.", alias="Secret") + created_at: datetime = Field(description="When the domain was registered, RFC 3339 in UTC with second precision. `0001-01-01T00:00:00Z` when unknown.", alias="CreatedAt") + __properties: ClassVar[List[str]] = ["Domain", "Weight", "Callback", "PublicKey", "Secret", "CreatedAt"] + + @field_validator('public_key') + def public_key_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^(\*+.{4}|.{0,4})$", value): + raise ValueError(r"must validate the regular expression /^(\*+.{4}|.{0,4})$/") + return value + + @field_validator('secret') + def secret_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^(\*+.{4}|.{0,4})$", value): + raise ValueError(r"must validate the regular expression /^(\*+.{4}|.{0,4})$/") + 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 DomainProfile 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 DomainProfile from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "Domain": obj.get("Domain"), + "Weight": obj.get("Weight"), + "Callback": obj.get("Callback"), + "PublicKey": obj.get("PublicKey"), + "Secret": obj.get("Secret"), + "CreatedAt": obj.get("CreatedAt") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/error_body.py b/generated/shieldlabs_generated/models/error_body.py new file mode 100644 index 0000000..1287525 --- /dev/null +++ b/generated/shieldlabs_generated/models/error_body.py @@ -0,0 +1,89 @@ +# 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 ErrorBody(BaseModel): + """ + Error object sent by the History API and by the Management API rate and load limits. + """ # noqa: E501 + error: StrictStr = Field(description="Human-readable error message. Branch on the HTTP status, not on this text.") + __properties: ClassVar[List[str]] = ["error"] + + 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 ErrorBody 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 ErrorBody from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "error": obj.get("error") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/health_status.py b/generated/shieldlabs_generated/models/health_status.py new file mode 100644 index 0000000..e18476a --- /dev/null +++ b/generated/shieldlabs_generated/models/health_status.py @@ -0,0 +1,96 @@ +# 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, field_validator +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 HealthStatus(BaseModel): + """ + Liveness status. + """ # noqa: E501 + status: StrictStr = Field(description="Always `ok` when the service answers.") + __properties: ClassVar[List[str]] = ["status"] + + @field_validator('status') + def status_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['ok']): + raise ValueError("must be one of enum values ('ok')") + 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 HealthStatus 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 HealthStatus from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "status": obj.get("status") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/history_page.py b/generated/shieldlabs_generated/models/history_page.py new file mode 100644 index 0000000..937f1d9 --- /dev/null +++ b/generated/shieldlabs_generated/models/history_page.py @@ -0,0 +1,100 @@ +# 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 +from typing import Any, ClassVar, Dict, List +from typing_extensions import Annotated +from shieldlabs_generated.models.history_row import HistoryRow +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class HistoryPage(BaseModel): + """ + One page of identifications, newest first. + """ # noqa: E501 + data: List[HistoryRow] = Field(description="Identifications on this page, ordered by `created_at` descending. Empty when nothing matched.") + total: Annotated[int, Field(strict=True, ge=0)] = Field(description="Number of identifications that match the search in total, across all pages. Page with `offset` while it is below `total`.") + __properties: ClassVar[List[str]] = ["data", "total"] + + 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 HistoryPage from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in data (list) + _items = [] + if self.data: + for _item_data in self.data: + if _item_data: + _items.append(_item_data.to_dict()) + _dict['data'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of HistoryPage from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "data": [HistoryRow.from_dict(_item) for _item in obj["data"]] if obj.get("data") is not None else None, + "total": obj.get("total") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/history_row.py b/generated/shieldlabs_generated/models/history_row.py new file mode 100644 index 0000000..7312dd4 --- /dev/null +++ b/generated/shieldlabs_generated/models/history_row.py @@ -0,0 +1,218 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from uuid import UUID +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +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 + 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.") + domain: StrictStr = Field(description="Host the identification came from. Can be a subdomain of your registered domain.") + site_domain: Optional[StrictStr] = Field(default=None, description="Your registered domain, present when the identification came from a subdomain. Omitted when empty.") + user_hid: StrictStr = Field(description="User HID exactly as it was passed to the agent (hashed or pseudonymous account identifier). `anonymous` for anonymous checks; `fail`, `-1` and `unknown` also mean \"no user\". Empty string when no value was stored. Leave the empty string and these values out when you count accounts.") + 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.") + 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.") + ip: StrictStr = Field(description="Public IPv4 address of the HTTP request; `0.0.0.0` when none (for example IPv6 visitors).") + os: StrictStr = Field(description="Operating system name, for example `Windows`, `Mac OS X`, `Linux`, `Android`, `IOS (iPhone)`, `IOS (iPad)`, `ChromeOS` or `Unknown`. Open set: display it, do not branch on it.") + browser: StrictStr = Field(description="Browser name, for example `Chrome`, `Safari`, `Firefox`, `Microsoft Edge`, `Opera`, `Samsung Internet`, `Brave`, `Chrome (iOS)`, `Safari (iOS)` or `Unknown`. Open set: display it, do not branch on it.") + device_type: StrictStr = Field(description="Device class from the browser. Known values: `desktop`, `mobile`, `tablet` and `unknown` (the class could not be determined). The set is open: keep values added in later versions and treat them as `unknown`.") + country: StrictStr = Field(description="Country of `ip` as an English country name, or an empty string.") + connection_type: StrictStr = Field(description="How the visitor connected. Known values: - `direct`: a regular connection; - `mobile`: a mobile carrier network; - `vpn`: a VPN; - `proxy`: a proxy, datacenter or hosting network (search-engine crawlers are reported here too); - `tor`: the Tor network; - `privacy_relay`: a privacy relay such as iCloud Private Relay; - `browser_vpn_proxy`: a VPN or proxy built into the browser or one of its extensions; - `unknown`: not enough data. The value can say `vpn` while `detection_flags.vpn` is `false` (IP intelligence classified the network, but the scored VPN check did not fire). Branch on `detection_flags` for decisions. The set is open: keep values added in later versions and treat them as `unknown`.") + score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"rate_limited\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.") + score_details: StrictStr = Field(description="The entries behind `score` as a JSON-encoded **string** holding an array of `{\"Value\": , \"Description\": }`. Parse it before use. Scored entries come first, followed by informational entries with `Value` 0, which can be long. Empty string when no details were stored. The webhook `signals` are the entries with a non-zero `Value`, in the same order, with each description turned into a signal name (for example `Is proxy` becomes `proxy`). Descriptions are free text for display: never branch on them.") + created_at: Annotated[str, Field(strict=True)] = Field(description="Time of the identification as `YYYY-MM-DD HH:MM:SS.mmm` in UTC, without a zone designator (not RFC 3339). Older rows can lack the milliseconds.") + ver: StrictInt = Field(description="Version of the row in Unix milliseconds. It increases every time the row is refined, for example when late network data re-scores it after the webhook was sent.") + web_rtc_ip: StrictStr = Field(description="Local IP address observed by the ShieldLabs network check; `0.0.0.0` when none.") + web_rtc_country: StrictStr = Field(description="Country of `web_rtc_ip`, or an empty string.") + web_rtc_connection_type: StrictStr = Field(description="Connection class of `web_rtc_ip`, or an empty string.") + webrtc_leak_ip: StrictStr = Field(description="Local network address leaked by the browser; `0.0.0.0` when none.") + webrtc_leak_country: StrictStr = Field(description="Country of `webrtc_leak_ip`, or an empty string.") + webrtc_leak_connection_type: StrictStr = Field(description="Connection class of `webrtc_leak_ip`, or an empty string.") + webrtc_leak_source: StrictStr = Field(description="Which check found the local network leak. Known values: `scanner`, `shield`, `none` and the empty string. `none` or an empty string when there is no leak; the webhook `local_ip` then uses `web_rtc_ip`. The set is open: keep values added in later versions.") + is_vpn: StrictBool = Field(description="Same meaning as `detection_flags.vpn`.") + is_tor: StrictBool = Field(description="Same meaning as `detection_flags.tor`.") + is_proxy: StrictBool = Field(description="Same meaning as `detection_flags.proxy`.") + is_datacenter: StrictBool = Field(description="Same meaning as `detection_flags.datacenter_ip`.") + is_abuser: StrictBool = Field(description="Same meaning as `detection_flags.abuser`.") + is_privacy_relay: StrictBool = Field(description="Same meaning as `detection_flags.privacy_relay`.") + is_stun_not_checked: StrictBool = Field(description="Same meaning as `detection_flags.stun_not_checked`.") + check_incomplete: StrictBool = Field(description="Same meaning as `detection_flags.check_incomplete`. Always `false` for search-engine crawlers.") + is_antidetect: StrictBool = Field(description="Same meaning as `detection_flags.anti_detect_browser`.") + is_os_mismatch: StrictBool = Field(description="Same meaning as `detection_flags.os_mismatch`.") + is_os_not_detected: StrictBool = Field(description="Same meaning as `detection_flags.os_not_detected`.") + is_timezone_mismatch: StrictBool = Field(description="Same meaning as `detection_flags.timezone_mismatch`.") + is_js_disabled: StrictBool = Field(description="Same meaning as `detection_flags.javascript_disabled`. Always `false` for search-engine crawlers.") + is_browser_automation: StrictBool = Field(description="Same meaning as `detection_flags.browser_automation`.") + is_incognito: StrictBool = Field(description="Same meaning as `detection_flags.incognito`. Always `false` for search-engine crawlers.") + is_search_bot: StrictBool = Field(description="Same meaning as `detection_flags.search_bot`.") + is_suspicious_paid_click: Optional[StrictBool] = Field(default=None, description="Same meaning as `detection_flags.suspicious_paid_click`. Omitted when `false`.") + entry_url: Optional[StrictStr] = Field(default=None, description="Landing page URL without the `#fragment` (webhook `traffic_source.landing_url`). Omitted when empty. It keeps the query string, which can contain personal data.") + utm_source: Optional[StrictStr] = Field(default=None, description="`utm_source`, lowercased. Omitted when empty.") + utm_medium: Optional[StrictStr] = Field(default=None, description="`utm_medium`, lowercased. Omitted when empty.") + utm_campaign: Optional[StrictStr] = Field(default=None, description="`utm_campaign` as sent. Omitted when empty.") + utm_content: Optional[StrictStr] = Field(default=None, description="`utm_content` as sent. Omitted when empty.") + utm_term: Optional[StrictStr] = Field(default=None, description="`utm_term` as sent. Omitted when empty.") + traffic_channel: Optional[StrictStr] = Field(default=None, description="Marketing channel (webhook `traffic_source.channel`). Omitted when empty.") + traffic_channel_group: Optional[StrictStr] = Field(default=None, description="Group of the marketing channel. Omitted when empty.") + traffic_reason: Optional[StrictStr] = Field(default=None, description="Why the channel was chosen. Omitted when empty.") + 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"] + + @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} [0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?$", value): + raise ValueError(r"must validate the regular expression /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?$/") + 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 HistoryRow 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 HistoryRow from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "request_id": obj.get("request_id"), + "session_id": obj.get("session_id"), + "cookie_id": obj.get("cookie_id"), + "domain": obj.get("domain"), + "site_domain": obj.get("site_domain"), + "user_hid": obj.get("user_hid"), + "device_id": obj.get("device_id"), + "visitor_id": obj.get("visitor_id"), + "ip": obj.get("ip"), + "os": obj.get("os"), + "browser": obj.get("browser"), + "device_type": obj.get("device_type"), + "country": obj.get("country"), + "connection_type": obj.get("connection_type"), + "score": obj.get("score"), + "score_details": obj.get("score_details"), + "created_at": obj.get("created_at"), + "ver": obj.get("ver"), + "web_rtc_ip": obj.get("web_rtc_ip"), + "web_rtc_country": obj.get("web_rtc_country"), + "web_rtc_connection_type": obj.get("web_rtc_connection_type"), + "webrtc_leak_ip": obj.get("webrtc_leak_ip"), + "webrtc_leak_country": obj.get("webrtc_leak_country"), + "webrtc_leak_connection_type": obj.get("webrtc_leak_connection_type"), + "webrtc_leak_source": obj.get("webrtc_leak_source"), + "is_vpn": obj.get("is_vpn"), + "is_tor": obj.get("is_tor"), + "is_proxy": obj.get("is_proxy"), + "is_datacenter": obj.get("is_datacenter"), + "is_abuser": obj.get("is_abuser"), + "is_privacy_relay": obj.get("is_privacy_relay"), + "is_stun_not_checked": obj.get("is_stun_not_checked"), + "check_incomplete": obj.get("check_incomplete"), + "is_antidetect": obj.get("is_antidetect"), + "is_os_mismatch": obj.get("is_os_mismatch"), + "is_os_not_detected": obj.get("is_os_not_detected"), + "is_timezone_mismatch": obj.get("is_timezone_mismatch"), + "is_js_disabled": obj.get("is_js_disabled"), + "is_browser_automation": obj.get("is_browser_automation"), + "is_incognito": obj.get("is_incognito"), + "is_search_bot": obj.get("is_search_bot"), + "is_suspicious_paid_click": obj.get("is_suspicious_paid_click"), + "entry_url": obj.get("entry_url"), + "utm_source": obj.get("utm_source"), + "utm_medium": obj.get("utm_medium"), + "utm_campaign": obj.get("utm_campaign"), + "utm_content": obj.get("utm_content"), + "utm_term": obj.get("utm_term"), + "traffic_channel": obj.get("traffic_channel"), + "traffic_channel_group": obj.get("traffic_channel_group"), + "traffic_reason": obj.get("traffic_reason"), + "referrer_domain": obj.get("referrer_domain"), + "click_id_type": obj.get("click_id_type") + }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + + return _obj + + diff --git a/generated/shieldlabs_generated/models/identification_scored_data.py b/generated/shieldlabs_generated/models/identification_scored_data.py new file mode 100644 index 0000000..bb78dd9 --- /dev/null +++ b/generated/shieldlabs_generated/models/identification_scored_data.py @@ -0,0 +1,164 @@ +# 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, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from uuid import UUID +from shieldlabs_generated.models.detection_flags import DetectionFlags +from shieldlabs_generated.models.ip_info import IpInfo +from shieldlabs_generated.models.signal import Signal +from shieldlabs_generated.models.traffic_source import TrafficSource +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class IdentificationScoredData(BaseModel): + """ + The scored identification. Every key is always present (no key is ever omitted); only `user_hid` can be `null`. + """ # noqa: E501 + 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.") + 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.") + user_hid: Optional[StrictStr] = Field(description="User HID: your hashed or pseudonymous account identifier, exactly as it was passed to the ShieldLabs agent. Pass a hashed value, never a raw email address or database ID. Values that do not identify a user: - `anonymous`: an anonymous check; - `fail`: the agent sent no value; - `-1` and `unknown`: rows created by ShieldLabs itself, such as rate-limit marker rows. `null` only when the stored value is an empty string. Leave `null` and the values above out when you count the accounts of one device, visitor or IP address.") + domain: StrictStr = Field(description="Registered domain of your site (the request host when no registered domain matched).") + public_ip: IpInfo = Field(description="Public IPv4 address of the HTTP request and its country. `ip` is empty when the request did not arrive over IPv4.") + local_ip: IpInfo = Field(description="Local IP address found by the browser network check (WebRTC): the leaked address when a local network leak was found, otherwise the address ShieldLabs observed. Both keys are empty when the check found nothing.") + connection_type: StrictStr = Field(description="How the visitor connected. Known values: - `direct`: a regular connection; - `mobile`: a mobile carrier network; - `vpn`: a VPN; - `proxy`: a proxy, datacenter or hosting network (search-engine crawlers are reported here too); - `tor`: the Tor network; - `privacy_relay`: a privacy relay such as iCloud Private Relay; - `browser_vpn_proxy`: a VPN or proxy built into the browser or one of its extensions; - `unknown`: not enough data. The value can say `vpn` while `detection_flags.vpn` is `false` (IP intelligence classified the network, but the scored VPN check did not fire). Branch on `detection_flags` for decisions. The set is open: keep values added in later versions and treat them as `unknown`.") + os: StrictStr = Field(description="Operating system name, for example `Windows`, `Mac OS X`, `Linux`, `Android`, `IOS (iPhone)`, `IOS (iPad)`, `ChromeOS` or `Unknown`. Open set: display it, do not branch on it.") + browser: StrictStr = Field(description="Browser name, for example `Chrome`, `Safari`, `Firefox`, `Microsoft Edge`, `Opera`, `Samsung Internet`, `Brave`, `Chrome (iOS)`, `Safari (iOS)` or `Unknown`. Open set: display it, do not branch on it.") + device_type: StrictStr = Field(description="Device class from the browser. Known values: `desktop`, `mobile`, `tablet` and `unknown` (the class could not be determined). The set is open: keep values added in later versions and treat them as `unknown`.") + traffic_source: TrafficSource + risk_score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"rate_limited\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.") + signals: List[Signal] = Field(description="Weighted risk signals behind `risk_score`, in scoring order. Can be empty. The rate-limit marker carries exactly one entry, `{\"name\":\"rate_limited\",\"weight\":999}`.") + detection_flags: DetectionFlags + observed_at: datetime = Field(description="When scoring finished and the event was built (not the page view time); identical to the envelope `created_at`. RFC 3339 in UTC with up to 9 fractional digits.") + __properties: ClassVar[List[str]] = ["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"] + + @field_validator('observed_at') + def observed_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 IdentificationScoredData from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of public_ip + if self.public_ip: + _dict['public_ip'] = self.public_ip.to_dict() + # override the default output from pydantic by calling `to_dict()` of local_ip + if self.local_ip: + _dict['local_ip'] = self.local_ip.to_dict() + # override the default output from pydantic by calling `to_dict()` of traffic_source + if self.traffic_source: + _dict['traffic_source'] = self.traffic_source.to_dict() + # override the default output from pydantic by calling `to_dict()` of each item in signals (list) + _items = [] + if self.signals: + for _item_signals in self.signals: + if _item_signals: + _items.append(_item_signals.to_dict()) + _dict['signals'] = _items + # override the default output from pydantic by calling `to_dict()` of detection_flags + if self.detection_flags: + _dict['detection_flags'] = self.detection_flags.to_dict() + # 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: + _dict['user_hid'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of IdentificationScoredData from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "request_id": obj.get("request_id"), + "visitor_id": obj.get("visitor_id"), + "device_id": obj.get("device_id"), + "session_id": obj.get("session_id"), + "cookie_id": obj.get("cookie_id"), + "user_hid": obj.get("user_hid"), + "domain": obj.get("domain"), + "public_ip": IpInfo.from_dict(obj["public_ip"]) if obj.get("public_ip") is not None else None, + "local_ip": IpInfo.from_dict(obj["local_ip"]) if obj.get("local_ip") is not None else None, + "connection_type": obj.get("connection_type"), + "os": obj.get("os"), + "browser": obj.get("browser"), + "device_type": obj.get("device_type"), + "traffic_source": TrafficSource.from_dict(obj["traffic_source"]) if obj.get("traffic_source") is not None else None, + "risk_score": obj.get("risk_score"), + "signals": [Signal.from_dict(_item) for _item in obj["signals"]] if obj.get("signals") is not None else None, + "detection_flags": DetectionFlags.from_dict(obj["detection_flags"]) if obj.get("detection_flags") is not None else None, + "observed_at": obj.get("observed_at") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/identification_scored_event.py b/generated/shieldlabs_generated/models/identification_scored_event.py new file mode 100644 index 0000000..e6177a7 --- /dev/null +++ b/generated/shieldlabs_generated/models/identification_scored_event.py @@ -0,0 +1,118 @@ +# 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, field_validator +from typing import Any, ClassVar, Dict, List +from typing_extensions import Annotated +from shieldlabs_generated.models.identification_scored_data import IdentificationScoredData +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class IdentificationScoredEvent(BaseModel): + """ + Body of an `identification.scored` delivery. The signature is not part of the body: it arrives in the `X-Shield-Signature` header. + """ # noqa: E501 + event_type: StrictStr = Field(description="Event type. Ignore events whose type you do not know instead of failing.") + schema_version: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Version of the webhook payload contract. Every event sent today carries `2026-06-01`. Accept other values, so that a future version does not break your handler.") + created_at: datetime = Field(description="When the event was built. Equal to `data.observed_at`.") + data: IdentificationScoredData + __properties: ClassVar[List[str]] = ["event_type", "schema_version", "created_at", "data"] + + @field_validator('event_type') + def event_type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['identification.scored']): + raise ValueError("must be one of enum values ('identification.scored')") + return value + + @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 IdentificationScoredEvent from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of IdentificationScoredEvent from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "event_type": obj.get("event_type"), + "schema_version": obj.get("schema_version"), + "created_at": obj.get("created_at"), + "data": IdentificationScoredData.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/ip_info.py b/generated/shieldlabs_generated/models/ip_info.py new file mode 100644 index 0000000..d5080cd --- /dev/null +++ b/generated/shieldlabs_generated/models/ip_info.py @@ -0,0 +1,102 @@ +# 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, field_validator +from typing import Any, ClassVar, Dict, List +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class IpInfo(BaseModel): + """ + An IPv4 address and its country. Both keys are always present and can be empty strings. + """ # noqa: E501 + ip: Annotated[str, Field(strict=True)] = Field(description="Dotted IPv4 address, or an empty string when no IPv4 address is known (for example for visitors on IPv6).") + country: StrictStr = Field(description="English country name from IP intelligence, for example `Germany` or `United States` (not an ISO code). Empty string when the country is unknown.") + __properties: ClassVar[List[str]] = ["ip", "country"] + + @field_validator('ip') + def ip_validate_regular_expression(cls, value): + """Validates the regular expression""" + if not isinstance(value, str): + value = str(value) + + if not re.match(r"^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})?$", value): + raise ValueError(r"must validate the regular expression /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})?$/") + 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 IpInfo 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 IpInfo from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "ip": obj.get("ip"), + "country": obj.get("country") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/legacy_snapshot.py b/generated/shieldlabs_generated/models/legacy_snapshot.py new file mode 100644 index 0000000..f694a2c --- /dev/null +++ b/generated/shieldlabs_generated/models/legacy_snapshot.py @@ -0,0 +1,147 @@ +# 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 uuid import UUID +from shieldlabs_generated.models.score_detail import ScoreDetail +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class LegacySnapshot(BaseModel): + """ + One identification as returned by the deprecated Management API history endpoint (PascalCase keys). Also carries diagnostic network fields that are not part of the stable contract. Use the History API row instead. + """ # noqa: E501 + 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.", alias="RequestID") + 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.", alias="SessionID") + 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.", alias="CookieID") + 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.", alias="DeviceID") + 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.", alias="VisitorID") + ip: StrictStr = Field(description="Public IPv4 address of the HTTP request.", alias="IP") + connection_type: StrictStr = Field(description="How the visitor connected. Known values: - `direct`: a regular connection; - `mobile`: a mobile carrier network; - `vpn`: a VPN; - `proxy`: a proxy, datacenter or hosting network (search-engine crawlers are reported here too); - `tor`: the Tor network; - `privacy_relay`: a privacy relay such as iCloud Private Relay; - `browser_vpn_proxy`: a VPN or proxy built into the browser or one of its extensions; - `unknown`: not enough data. The value can say `vpn` while `detection_flags.vpn` is `false` (IP intelligence classified the network, but the scored VPN check did not fire). Branch on `detection_flags` for decisions. The set is open: keep values added in later versions and treat them as `unknown`.", alias="ConnectionType") + web_rtc_hip: StrictStr = Field(description="Local IP address observed by the ShieldLabs network check (not hashed); `0.0.0.0` when none.", alias="WebRtcHIP") + web_rtc_country: StrictStr = Field(description="Country of `WebRtcHIP`, or an empty string.", alias="WebRtcCountry") + web_rtc_connection_type: StrictStr = Field(description="Connection class of `WebRtcHIP`, or an empty string.", alias="WebRtcConnectionType") + os: StrictStr = Field(description="Operating system name, for example `Windows`, `Mac OS X`, `Linux`, `Android`, `IOS (iPhone)`, `IOS (iPad)`, `ChromeOS` or `Unknown`. Open set: display it, do not branch on it.", alias="OS") + browser: StrictStr = Field(description="Browser name, for example `Chrome`, `Safari`, `Firefox`, `Microsoft Edge`, `Opera`, `Samsung Internet`, `Brave`, `Chrome (iOS)`, `Safari (iOS)` or `Unknown`. Open set: display it, do not branch on it.", alias="Browser") + device_type: StrictStr = Field(description="Device class from the browser. Known values: `desktop`, `mobile`, `tablet` and `unknown` (the class could not be determined). The set is open: keep values added in later versions and treat them as `unknown`.", alias="DeviceType") + country: StrictStr = Field(description="Country of `IP` as an English country name, or an empty string.", alias="Country") + user_hid: StrictStr = Field(description="User HID as passed to the agent; `anonymous` for anonymous checks.", alias="UserHID") + score: Annotated[int, Field(strict=True, ge=0)] = Field(description="Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. Risk bands are computed on your side from the score; no band field exists on the wire: - trusted: 0-29 - suspicious: 30-59 - dangerous: 60-100 A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the ingest rate limit, and the identification carries exactly one signal, `{\"name\":\"rate_limited\",\"weight\":999}`, usually with nil identifiers. Treat every value above 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued while it stays blocked get no row and no webhook, so they stay unverified. The score usually equals the sum of the signal weights capped at 100, but carried-forward verdicts and corrections make that unreliable: never recompute or validate it yourself.", alias="Score") + details: List[ScoreDetail] = Field(description="Every entry behind `Score`, informational entries with `Value` 0 included (unlike the History API, this is a parsed array, not a string).", alias="Details") + last_request_time: datetime = Field(description="Time of the identification, RFC 3339 with fractional seconds.", alias="LastRequestTime") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["RequestID", "SessionID", "CookieID", "DeviceID", "VisitorID", "IP", "ConnectionType", "WebRtcHIP", "WebRtcCountry", "WebRtcConnectionType", "OS", "Browser", "DeviceType", "Country", "UserHID", "Score", "Details", "LastRequestTime"] + + 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 LegacySnapshot 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 details (list) + _items = [] + if self.details: + for _item_details in self.details: + if _item_details: + _items.append(_item_details.to_dict()) + _dict['Details'] = _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 LegacySnapshot from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "RequestID": obj.get("RequestID"), + "SessionID": obj.get("SessionID"), + "CookieID": obj.get("CookieID"), + "DeviceID": obj.get("DeviceID"), + "VisitorID": obj.get("VisitorID"), + "IP": obj.get("IP"), + "ConnectionType": obj.get("ConnectionType"), + "WebRtcHIP": obj.get("WebRtcHIP"), + "WebRtcCountry": obj.get("WebRtcCountry"), + "WebRtcConnectionType": obj.get("WebRtcConnectionType"), + "OS": obj.get("OS"), + "Browser": obj.get("Browser"), + "DeviceType": obj.get("DeviceType"), + "Country": obj.get("Country"), + "UserHID": obj.get("UserHID"), + "Score": obj.get("Score"), + "Details": [ScoreDetail.from_dict(_item) for _item in obj["Details"]] if obj.get("Details") is not None else None, + "LastRequestTime": obj.get("LastRequestTime") + }) + # 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/score_detail.py b/generated/shieldlabs_generated/models/score_detail.py new file mode 100644 index 0000000..a7b8ca7 --- /dev/null +++ b/generated/shieldlabs_generated/models/score_detail.py @@ -0,0 +1,91 @@ +# 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, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class ScoreDetail(BaseModel): + """ + One entry behind the score, in the PascalCase shape the server stores. `Value` is the weight (0 for informational entries); `Description` is free text for display, never branch on it. + """ # noqa: E501 + value: StrictInt = Field(description="Weight of the entry. Can be negative; 0 for informational entries.", alias="Value") + description: StrictStr = Field(description="Human-readable description, for example `Is proxy` or `Antidetect browser (turn_block)`.", alias="Description") + __properties: ClassVar[List[str]] = ["Value", "Description"] + + 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 ScoreDetail 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 ScoreDetail from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "Value": obj.get("Value"), + "Description": obj.get("Description") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/signal.py b/generated/shieldlabs_generated/models/signal.py new file mode 100644 index 0000000..38e490e --- /dev/null +++ b/generated/shieldlabs_generated/models/signal.py @@ -0,0 +1,92 @@ +# 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, StrictInt +from typing import Any, ClassVar, Dict, List +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +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.") + 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"] + + 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 Signal 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 Signal from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "name": obj.get("name"), + "weight": obj.get("weight") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/traffic_source.py b/generated/shieldlabs_generated/models/traffic_source.py new file mode 100644 index 0000000..cc0e51d --- /dev/null +++ b/generated/shieldlabs_generated/models/traffic_source.py @@ -0,0 +1,105 @@ +# coding: utf-8 + +""" + ShieldLabs API + + Identification results and risk scoring for your backend. + + The version of the OpenAPI document: 1.0.1 + Contact: contact@shieldlabs.ai + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, 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 TrafficSource(BaseModel): + """ + Where the visit came from. All nine keys are always present; values can be empty strings. + """ # noqa: E501 + channel: StrictStr = Field(description="Marketing channel of the visit. Known values: `Google Ads`, `Meta`, `TikTok`, `LinkedIn`, `X`, `Pinterest`, `Microsoft Ads`, `Organic Search`, `Search bot`, `Referral`, `Direct`, `Other` and the empty string. Resolved in this order: a click ID, then UTM parameters, then the referrer (search engines give `Organic Search`, social networks give the platform name, other sites give `Referral`), otherwise `Direct`. Search-engine crawlers get `Search bot`. Empty string on identifications without attribution, such as rate-limit marker rows. The set is open: keep values added in later versions and treat them as `Other`.") + referrer_domain: StrictStr = Field(description="Registrable domain of the referrer without `www.`. For search-engine crawlers, the crawler name (for example `GoogleBot`).") + landing_url: StrictStr = Field(description="Landing page URL without the `#fragment`. It keeps the query string, which can contain personal data: store it with care.") + click_id_type: StrictStr = Field(description="Ad click identifier found in the landing URL. Known values: `gclid`, `gbraid`, `wbraid`, `msclkid`, `ttclid`, `fbclid` and the empty string when there is none. `fbclid` counts only together with a Meta referrer or a Meta `utm_source`. The set is open: keep values added in later versions.") + utm_source: StrictStr = Field(description="`utm_source` query parameter, lowercased.") + utm_medium: StrictStr = Field(description="`utm_medium` query parameter, lowercased.") + utm_campaign: StrictStr = Field(description="`utm_campaign` query parameter as sent.") + utm_content: StrictStr = Field(description="`utm_content` query parameter as sent.") + utm_term: StrictStr = Field(description="`utm_term` query parameter as sent.") + __properties: ClassVar[List[str]] = ["channel", "referrer_domain", "landing_url", "click_id_type", "utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"] + + 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 TrafficSource 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 TrafficSource from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "channel": obj.get("channel"), + "referrer_domain": obj.get("referrer_domain"), + "landing_url": obj.get("landing_url"), + "click_id_type": obj.get("click_id_type"), + "utm_source": obj.get("utm_source"), + "utm_medium": obj.get("utm_medium"), + "utm_campaign": obj.get("utm_campaign"), + "utm_content": obj.get("utm_content"), + "utm_term": obj.get("utm_term") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/models/webhook_ping_event.py b/generated/shieldlabs_generated/models/webhook_ping_event.py new file mode 100644 index 0000000..16e60b9 --- /dev/null +++ b/generated/shieldlabs_generated/models/webhook_ping_event.py @@ -0,0 +1,112 @@ +# 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, field_validator +from typing import Any, ClassVar, Dict, List +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class WebhookPingEvent(BaseModel): + """ + Body of a `webhook.ping` delivery, sent when you verify an endpoint. It has no `data`. The keys arrive sorted alphabetically and `created_at` has second precision. + """ # noqa: E501 + event_type: StrictStr = Field(description="Event type.") + schema_version: Annotated[str, Field(min_length=1, strict=True)] = Field(description="Version of the webhook payload contract. Every event sent today carries `2026-06-01`. Accept other values, so that a future version does not break your handler.") + created_at: datetime = Field(description="When the ping was sent, with second precision.") + __properties: ClassVar[List[str]] = ["event_type", "schema_version", "created_at"] + + @field_validator('event_type') + def event_type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['webhook.ping']): + raise ValueError("must be one of enum values ('webhook.ping')") + 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 WebhookPingEvent 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 WebhookPingEvent from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "event_type": obj.get("event_type"), + "schema_version": obj.get("schema_version"), + "created_at": obj.get("created_at") + }) + return _obj + + diff --git a/generated/shieldlabs_generated/rest.py b/generated/shieldlabs_generated/rest.py new file mode 100644 index 0000000..bd95dc9 --- /dev/null +++ b/generated/shieldlabs_generated/rest.py @@ -0,0 +1,264 @@ +# 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 + + +import io +import json +import re +import ssl + +import urllib3 + +from shieldlabs_generated.exceptions import ApiException, ApiValueError + +SUPPORTED_SOCKS_PROXIES = {"socks5", "socks5h", "socks4", "socks4a"} +RESTResponseType = urllib3.HTTPResponse + + +def is_socks_proxy_url(url): + if url is None: + return False + split_section = url.split("://") + if len(split_section) < 2: + return False + else: + return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES + + +class RESTResponse(io.IOBase): + + def __init__(self, resp) -> None: + self.response = resp + self.status = resp.status + self.reason = resp.reason + self.data = None + + def read(self): + if self.data is None: + self.data = self.response.data + return self.data + + @property + def headers(self): + """Returns a dictionary of response headers.""" + return self.response.headers + + def getheaders(self): + """Returns a dictionary of the response headers; use ``headers`` instead.""" + return self.response.headers + + def getheader(self, name, default=None): + """Returns a given response header; use ``headers.get()`` instead.""" + return self.response.headers.get(name, default) + + +class RESTClientObject: + + def __init__(self, configuration) -> None: + # urllib3.PoolManager will pass all kw parameters to connectionpool + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501 + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501 + # Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501 + + # cert_reqs + if configuration.verify_ssl: + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_NONE + + pool_args = { + "cert_reqs": cert_reqs, + "ca_certs": configuration.ssl_ca_cert, + "cert_file": configuration.cert_file, + "key_file": configuration.key_file, + "ca_cert_data": configuration.ca_cert_data, + } + if configuration.assert_hostname is not None: + pool_args['assert_hostname'] = ( + configuration.assert_hostname + ) + + if configuration.retries is not None: + pool_args['retries'] = configuration.retries + + if configuration.tls_server_name: + pool_args['server_hostname'] = configuration.tls_server_name + + + if configuration.socket_options is not None: + pool_args['socket_options'] = configuration.socket_options + + if configuration.connection_pool_maxsize is not None: + pool_args['maxsize'] = configuration.connection_pool_maxsize + + # https pool manager + self.pool_manager: urllib3.PoolManager + + if configuration.proxy: + if is_socks_proxy_url(configuration.proxy): + from urllib3.contrib.socks import SOCKSProxyManager + pool_args["proxy_url"] = configuration.proxy + pool_args["headers"] = configuration.proxy_headers + self.pool_manager = SOCKSProxyManager(**pool_args) + else: + pool_args["proxy_url"] = configuration.proxy + pool_args["proxy_headers"] = configuration.proxy_headers + self.pool_manager = urllib3.ProxyManager(**pool_args) + else: + self.pool_manager = urllib3.PoolManager(**pool_args) + + def request( + self, + method, + url, + headers=None, + body=None, + post_params=None, + _request_timeout=None + ): + """Perform requests. + + :param method: http request method + :param url: http request url + :param headers: http request headers + :param body: request json body, for `application/json` + :param post_params: request post parameters, + `application/x-www-form-urlencoded` + and `multipart/form-data` + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + """ + method = method.upper() + assert method in [ + 'GET', + 'HEAD', + 'DELETE', + 'POST', + 'PUT', + 'PATCH', + 'OPTIONS' + ] + + if post_params and body: + raise ApiValueError( + "body parameter cannot be used with post_params parameter." + ) + + post_params = post_params or {} + headers = headers or {} + + timeout = None + if _request_timeout: + if isinstance(_request_timeout, (int, float)): + timeout = urllib3.Timeout(total=_request_timeout) + elif ( + isinstance(_request_timeout, tuple) + and len(_request_timeout) == 2 + ): + timeout = urllib3.Timeout( + connect=_request_timeout[0], + read=_request_timeout[1] + ) + + try: + # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` + if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: + + # no content type provided or payload is json + content_type = headers.get('Content-Type') + if ( + not content_type + or re.search('json', content_type, re.IGNORECASE) + ): + request_body = None + if body is not None: + request_body = json.dumps(body) + r = self.pool_manager.request( + method, + url, + body=request_body, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif content_type == 'application/x-www-form-urlencoded': + r = self.pool_manager.request( + method, + url, + fields=post_params, + encode_multipart=False, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif content_type == 'multipart/form-data': + # must del headers['Content-Type'], or the correct + # Content-Type which generated by urllib3 will be + # overwritten. + del headers['Content-Type'] + # Ensures that dict objects are serialized + post_params = [(a, json.dumps(b)) if isinstance(b, dict) else (a,b) for a, b in post_params] + r = self.pool_manager.request( + method, + url, + fields=post_params, + encode_multipart=True, + timeout=timeout, + headers=headers, + preload_content=False + ) + # Pass a `string` parameter directly in the body to support + # other content types than JSON when `body` argument is + # provided in serialized form. + elif isinstance(body, str) or isinstance(body, bytes): + r = self.pool_manager.request( + method, + url, + body=body, + timeout=timeout, + headers=headers, + preload_content=False + ) + elif headers['Content-Type'].startswith('text/') and isinstance(body, bool): + request_body = "true" if body else "false" + r = self.pool_manager.request( + method, + url, + body=request_body, + preload_content=False, + timeout=timeout, + headers=headers) + else: + # Cannot generate the request from given parameters + msg = """Cannot prepare a request message for provided + arguments. Please check that your arguments match + declared content type.""" + raise ApiException(status=0, reason=msg) + # For `GET`, `HEAD` + else: + r = self.pool_manager.request( + method, + url, + fields={}, + timeout=timeout, + headers=headers, + preload_content=False + ) + except urllib3.exceptions.SSLError as e: + msg = "\n".join([type(e).__name__, str(e)]) + raise ApiException(status=0, reason=msg) + + return RESTResponse(r) diff --git a/pyproject.toml b/pyproject.toml index 3866c69..ceec1cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ exclude_also = [ [tool.ruff] line-length = 100 +extend-exclude = ["generated"] target-version = "py39" src = ["src", "tests"] diff --git a/resources/shieldlabs-api.yaml b/resources/shieldlabs-api.yaml new file mode 100644 index 0000000..2838959 --- /dev/null +++ b/resources/shieldlabs-api.yaml @@ -0,0 +1,2462 @@ +openapi: 3.1.0 +info: + title: ShieldLabs API + version: 1.0.1 + summary: Identification results and risk scoring for your backend. + description: |- + The ShieldLabs API gives your backend the result of every identification the ShieldLabs agent + runs in a browser: the Risk Score, the risk signals behind it, the detection flags and the + identifiers (request, visitor, device, session, cookie and User HID) it belongs to. + + ## How it fits + + 1. **Browser.** The ShieldLabs agent, loaded from `cdn.shieldlabs.ai`, runs an identification + and hands your page a request ID. The browser never receives a Risk Score, a visitor ID or + a device ID. + 2. **Your backend.** Your page sends the request ID along with the protected action (signup, + login, checkout). Your backend reads the verdict for it from the **History API**, or + receives it in a signed `identification.scored` **webhook**. + 3. **Decision.** Your backend acts on `risk_score`, the three risk bands, `detection_flags` + and the identifiers, for example by counting how many accounts one `device_id` has used + (skipping the `user_hid` values that do not identify a user, listed under Identifiers). + + Scoring is asynchronous. The webhook usually arrives about 300 ms after the browser check; when + follow-up network checks run, it is sent when they finish, at most about 10 seconds later. The + History row appears about 1-3 seconds after the browser call and can be refined for up to about + 10 seconds as follow-up checks finish. Start the identification when the user begins the + protected action (for example when the signup form opens), then either poll the History API by + `request_id` with a short backoff or wait for the webhook. Let one identification authorize one + protected action: reject request IDs you have already used and identifications older than your + freshness window. + + ## Hosts and credentials + + | API | Host | Paths | Credentials | + |---|---|---|---| + | History API | `https://account.shieldlabs.ai` | `/api/v1/...` | `Authorization: Bearer ` (`sec_...`, one per domain) | + | Management API | `https://api.shieldlabs.ai` | `/v1/...` | `X-Shield-Domain: ` and `Authorization: Bearer ` | + | Health | both hosts | `/health` | none | + + Every operation declares its own server, so generated clients send each call to the right + host. The History API serves its paths from the host root: the full URL is + `https://account.shieldlabs.ai/api/v1/history/{search_type}/{value}`, and a base URL that + already ends in `/api` produces `/api/api/v1/...` and a `404`. + + The two credentials are not interchangeable. Keep the Private API Key and the Secret Key on your + server; only the Public Key belongs in the browser. All keys are in the analytics dashboard at + https://app.shieldlabs.ai. + + ## Rate limits + + - **History API:** about 15 requests per second per domain, shared by every caller of that + domain. Requests over the limit get `429`; there is no ban, so retry after about a second. + - **Management API:** 15 requests per minute per client IP. The request that goes over the + limit starts a 10-minute block, during which every request gets `429`. Never retry a `429` + from this API; cache the profile instead. + - **Health:** not rate limited. + + API calls and webhook deliveries are free: only identifications made by the browser agent use + your included volume. + + ## Errors + + Error bodies are not uniform. Branch on the HTTP status first, then try to parse the body as + JSON whatever its content type. + + | API | Status | Body | + |---|---|---| + | History API | 401 | JSON text `{"error":"..."}`, sent as `text/plain` | + | History API | 429 | `{"error":"too many requests"}` | + | History API | 500 | `{"error":"..."}`. A malformed UUID or IPv4 value always ends here: validate before sending and do not retry it | + | Management API | 401 | empty | + | Management API | 400 | a bare JSON string or `null` (deprecated history endpoint) | + | Management API | 429, 503 | `{"error":"..."}` | + | Both | 404 | `404 page not found` as `text/plain` when no route matches | + | Both | 502, 504 | an HTML page from the edge proxy | + + Retry `429` (History API only), `5xx` and network errors with backoff. Do not retry `400`, + `401` or `404`, nor a History API `500` caused by a malformed value. + + ## Identifiers + + - `request_id`: one identification, created in the browser (UUID v4). It joins the browser + call, the webhook and the History row. + - `session_id`: one visit on one origin (UUID v4). + - `cookie_id`: first-party browser identifier kept by the agent (UUID v4). + - `device_id`: server-side device identifier (UUID v5). It survives cleared cookies and private + windows. The nil UUID `00000000-0000-0000-0000-000000000000` means no usable device signals. + - `visitor_id`: server-side visitor identifier (UUID v5), sticky to the device: a new cookie on + a known device keeps the visitor ID. + - `user_hid`: your hashed or pseudonymous account identifier, as passed to the agent. + `anonymous` marks anonymous checks; `fail`, `-1` and `unknown` also mean "no user". Leave + these values, `null` and the empty string out when you count accounts. Hex-encoded hashes + are the easiest values to search: see the `value` parameter of `searchHistory` for how to + encode other characters. + + Validate UUIDs with any version accepted, the nil UUID included. + + ## Risk Score, risk bands and the 999 marker + + The Risk Score (`risk_score` on webhooks, `score` in the History API) is an integer from 0 to + 100. Search-engine crawlers always score 0. The three risk bands are computed on your side; no + band field exists on the wire: + + | Band | Score | + |---|---| + | trusted | 0-29 | + | suspicious | 30-59 | + | dangerous | 60-100 | + + A value above 100 is not a score. **999** is the rate-limit marker: the visitor's IP went over + the ingest rate limit, and the identification carries exactly one signal, + `{"name":"rate_limited","weight":999}`, usually with nil identifiers. Treat every value above + 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued + while it stays blocked get no row and no webhook, so they stay unverified. + + Branch on `detection_flags` and the Risk Score. Signal names are for display and logging; + weights can be negative or change between releases, so never add them up yourself. A missing + identification means "unverified", never "clean". + + ## Countries, IP addresses and timestamps + + - `country` values are English country names from IP intelligence, such as `Germany` or + `United States`, or an empty string when unknown. + - IP fields hold IPv4 addresses. Without an IPv4 address the webhook sends `""` and the History + API sends `0.0.0.0`; such identifications cannot be searched by IP. + - Webhook timestamps (`created_at`, `observed_at`) are RFC 3339 in UTC with up to 9 fractional + digits. + - The History API `created_at` is `YYYY-MM-DD HH:MM:SS.mmm` in UTC without a zone designator; + older rows can lack the milliseconds. + - The Management API `CreatedAt` is RFC 3339 with second precision. + + ## Webhooks + + ShieldLabs sends one signed `POST` for each identification to every enabled endpoint of the + domain. A delivery has a 1-second timeout and is not retried today; a later release adds + retries that resend identical bytes. Answer 2xx within a second, process the event + asynchronously, make the handler idempotent on `data.request_id`, and use the History API for + guaranteed reads. A History row can be refined after its webhook was sent (its `ver` + increases); the webhook is not sent again. See the `identification.scored` and `webhook.ping` + entries for the signature algorithm. + + ## Compatibility + + Ignore fields you do not know, keep unknown values of string fields (such as new signal names + or channels) instead of failing, and accept webhook `schema_version` values other than + `2026-06-01`. + + Start free at https://app.shieldlabs.ai. Guides: https://docs.shieldlabs.ai. + contact: + name: ShieldLabs + url: https://docs.shieldlabs.ai + email: contact@shieldlabs.ai + license: + name: MIT + identifier: MIT +servers: + - url: https://account.shieldlabs.ai + description: History API (every operation also declares its own server) + - url: https://api.shieldlabs.ai + description: Management API (every operation also declares its own server) +tags: + - name: History API + description: 'Read identifications by one identifier on `https://account.shieldlabs.ai` with the Private API Key. The canonical way to read a verdict: by `request_id` right after a protected action, or by `device_id`, `user_hid`, `visitor_id` or `ip` for account-level checks.' + - name: Management API + description: Domain profile on `https://api.shieldlabs.ai`, authenticated with the Secret Key and the `X-Shield-Domain` header. Also serves the deprecated history endpoint until 1 January 2027. + - name: Health + description: Unauthenticated liveness checks on both API hosts. + - name: Webhooks + description: 'Signed events ShieldLabs sends to your webhook endpoints: `identification.scored` for every identification and `webhook.ping` when you verify an endpoint.' +externalDocs: + description: ShieldLabs documentation + url: https://docs.shieldlabs.ai +paths: + /api/v1/history/{search_type}/{value}: + servers: + - url: https://account.shieldlabs.ai + description: History API + get: + operationId: searchHistory + tags: + - History API + summary: Search identifications + description: |- + Returns the identifications of your domain that match one identifier, newest first, together + with the total number of matches. The Private API Key selects the domain; identifications from + its subdomains are included (`domain` holds the host, `site_domain` the registered domain). + + **Read one verdict.** After a protected action, search by `request_id` with `limit=1`. The row + appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds + as follow-up network checks finish, so start the identification when the user begins the + action (for example when the signup form opens), not when the form is submitted. An empty + `data` array means "not scored yet", never "clean". Poll with backoff (first try at once, then + wait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as + "wait longer". The official server SDKs do this for you. + + **Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how + many accounts share a device, how many devices one account uses, or what else came from one + IP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values + that do not identify a user: `anonymous`, `fail`, `-1` and `unknown`. + + **Validate before sending.** The server does not validate the path: an unknown `search_type` + returns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4 + value returns `500`, and a `limit` outside 1-100 silently becomes 20. + + **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` + only, so paging while new identifications arrive can repeat or skip rows: deduplicate on + `request_id`. + + **Latest state.** A row can be refined after the webhook was sent, for example when late + network data re-scores it; its `ver` then increases. The History API always returns the latest + version, which makes it the guaranteed read path. + + Reads are free: they do not use your included identifications. + security: + - historyApiKey: [] + parameters: + - $ref: '#/components/parameters/HistorySearchType' + - $ref: '#/components/parameters/HistoryValue' + - $ref: '#/components/parameters/HistoryLimit' + - $ref: '#/components/parameters/HistoryOffset' + responses: + '200': + description: Matching identifications, newest first. `data` is empty when nothing matched. + content: + application/json: + schema: + $ref: '#/components/schemas/HistoryPage' + examples: + page: + $ref: '#/components/examples/HistoryPage' + empty: + $ref: '#/components/examples/HistoryPageEmpty' + '401': + $ref: '#/components/responses/HistoryUnauthorized' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/HistoryTooManyRequests' + '500': + $ref: '#/components/responses/HistoryServerError' + '502': + $ref: '#/components/responses/BadGateway' + '504': + $ref: '#/components/responses/GatewayTimeout' + x-codeSamples: + - lang: Shell + label: curl + source: | + curl "https://account.shieldlabs.ai/api/v1/history/request_id/a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d?limit=1" \ + -H "Authorization: Bearer $SHIELDLABS_API_KEY" + /v1/profile: + servers: + - url: https://api.shieldlabs.ai + description: Management API + get: + operationId: getDomainProfile + tags: + - Management API + summary: Get the domain profile + description: |- + Returns the registered domain, the remaining included identifications of the account and the + masked keys. + + **Credentials.** Send the Secret Key as a Bearer token and the registered domain in + `X-Shield-Domain`. The domain is matched exactly: send it lowercase, without scheme, path, + trailing slash or a leading `www.`. + + **Rate limit.** 15 requests per minute per client IP. The request that goes over the limit + starts a 10-minute block during which every request to the Management API gets `429`. Call + this endpoint sparingly, cache the profile, and never retry a `429`. + + `Weight` can be negative when the account is over its included volume. The call is free. + security: + - managementSecretKey: [] + parameters: + - $ref: '#/components/parameters/ShieldDomain' + responses: + '200': + description: The domain profile. + content: + application/json: + schema: + $ref: '#/components/schemas/DomainProfile' + examples: + profile: + $ref: '#/components/examples/DomainProfile' + '401': + $ref: '#/components/responses/ManagementUnauthorized' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/ManagementTooManyRequests' + '502': + $ref: '#/components/responses/BadGateway' + '503': + $ref: '#/components/responses/ManagementServerBusy' + '504': + $ref: '#/components/responses/GatewayTimeout' + x-codeSamples: + - lang: Shell + label: curl + source: | + curl "https://api.shieldlabs.ai/v1/profile" \ + -H "X-Shield-Domain: $SHIELDLABS_DOMAIN" \ + -H "Authorization: Bearer $SHIELDLABS_SECRET_KEY" + /v1/history/{type}/{value}: + servers: + - url: https://api.shieldlabs.ai + description: Management API + get: + operationId: searchHistoryDeprecated + tags: + - Management API + summary: Search history by identifier (deprecated) + deprecated: true + x-sunset: '2027-01-01' + description: |- + **Deprecated.** This endpoint stops working after Sat, 01 Jan 2027 00:00:00 GMT. Use + `searchHistory` on the History API instead: `https://account.shieldlabs.ai/api/v1/history`. + Every answer of this route except `429` and `503` carries `Deprecation: true`, a `Sunset` + header and a `Link` header with `rel="successor-version"` pointing there. The plain-text `404` + for a path that matches no route and the edge proxy errors do not carry them. + + Differences from the History API: the answer is a bare array of PascalCase objects; only rows + whose request host equals `X-Shield-Domain` are returned (no subdomain traffic); `limit` + defaults to 100 and there is no `offset`. It uses the Management API credentials and rate limit + (15 requests per minute per client IP, then a 10-minute block). The call is free. + security: + - managementSecretKey: [] + parameters: + - $ref: '#/components/parameters/ShieldDomain' + - $ref: '#/components/parameters/DeprecatedHistoryType' + - $ref: '#/components/parameters/DeprecatedHistoryValue' + - $ref: '#/components/parameters/DeprecatedHistoryLimit' + responses: + '200': + description: Matching identifications, newest first. + headers: + Deprecation: + $ref: '#/components/headers/Deprecation' + Sunset: + $ref: '#/components/headers/Sunset' + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/LegacySnapshot' + examples: + list: + $ref: '#/components/examples/LegacySnapshotList' + empty: + $ref: '#/components/examples/LegacySnapshotListEmpty' + '400': + description: The value failed validation (a bare JSON string) or the query failed (the JSON literal `null`, for example for an IPv6 `ip` value). Do not retry. + headers: + Deprecation: + $ref: '#/components/headers/Deprecation' + Sunset: + $ref: '#/components/headers/Sunset' + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + $ref: '#/components/schemas/LegacyErrorMessage' + examples: + invalidUuid: + $ref: '#/components/examples/ManagementBadRequestUuid' + invalidIp: + $ref: '#/components/examples/ManagementBadRequestIp' + emptyValue: + $ref: '#/components/examples/ManagementBadRequestEmpty' + queryFailed: + $ref: '#/components/examples/ManagementBadRequestNull' + '401': + description: Empty body, no `Content-Type`. Missing or malformed headers, unknown or disabled domain, or a wrong Secret Key. Do not retry. + headers: + Deprecation: + $ref: '#/components/headers/Deprecation' + Sunset: + $ref: '#/components/headers/Sunset' + Link: + $ref: '#/components/headers/Link' + '404': + description: '`application/json`: the `type` is not supported (a bare JSON string); this answer carries the deprecation headers. `text/plain`: no route matches the path, for example because the value is empty or contains `/`; this answer comes from the router and carries no deprecation headers. Do not retry.' + headers: + Deprecation: + $ref: '#/components/headers/Deprecation' + Sunset: + $ref: '#/components/headers/Sunset' + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + $ref: '#/components/schemas/LegacyErrorMessage' + examples: + unsupportedType: + $ref: '#/components/examples/ManagementUnsupportedType' + text/plain: + schema: + $ref: '#/components/schemas/PlainText' + examples: + notFound: + $ref: '#/components/examples/NotFoundText' + '429': + $ref: '#/components/responses/ManagementTooManyRequests' + '502': + $ref: '#/components/responses/BadGateway' + '503': + $ref: '#/components/responses/ManagementServerBusy' + '504': + $ref: '#/components/responses/GatewayTimeout' + /health: + servers: + - url: https://account.shieldlabs.ai + description: History API host + - url: https://api.shieldlabs.ai + description: Management API host + get: + operationId: getHealth + tags: + - Health + summary: Check service health + description: |- + Liveness check. Returns `{"status":"ok"}` while the service answers. Available on both API + hosts: `https://account.shieldlabs.ai/health` for the History API and + `https://api.shieldlabs.ai/health` for the Management API. No authentication, not rate + limited, not billed. + security: [] + responses: + '200': + description: The service is up. + content: + application/json: + schema: + $ref: '#/components/schemas/HealthStatus' + examples: + ok: + $ref: '#/components/examples/HealthOk' + '404': + description: No route matches the path. The health check lives at the host root, so `/api/health` gets this answer. Plain text body. + content: + text/plain: + schema: + $ref: '#/components/schemas/PlainText' + examples: + notFound: + $ref: '#/components/examples/NotFoundText' + '502': + $ref: '#/components/responses/BadGateway' + '504': + $ref: '#/components/responses/GatewayTimeout' + x-codeSamples: + - lang: Shell + label: curl + source: | + curl "https://account.shieldlabs.ai/health" +webhooks: + identification.scored: + post: + operationId: identificationScored + tags: + - Webhooks + summary: Identification scored + description: |- + Sent to every enabled webhook endpoint of your domain once for each identification, when its + scoring is final: usually about 300 ms after the browser check, and at most about 10 seconds + later when follow-up network checks run. + + **Verify, then parse.** Compute HMAC-SHA256 over the raw request body and compare it with + `X-Shield-Signature` before you parse the JSON: + - key: the endpoint's signing secret as UTF-8 bytes, including the `whsec_` prefix (not hex- + or base64-decoded, not stripped); + - message: the exact bytes received; re-serializing parsed JSON changes them (for example, `&` + arrives escaped as `\u0026`); + - expected header: `sha256=` followed by the lowercase hex digest, compared in constant time. + + There is no timestamp, delivery ID or event-type header. Rotating a secret replaces it at + once, so accept both the old and the new secret until your deployment has switched. + + **Respond fast.** Answer any 2xx status within 1 second and process the event asynchronously; + do not redirect. Today each identification is delivered once per endpoint, with no retries. A + later release adds retries that resend identical bytes, so make your handler idempotent on + `data.request_id`. + + **Latest state.** The event is a snapshot taken when scoring finished. The History row can + still be refined afterwards (its `ver` increases) and no second event is sent. Use the History + API for guaranteed reads and for the latest state. + + **Test deliveries.** The Test button in the analytics dashboard sends a fixed sample with keys + sorted alphabetically, second-precision timestamps and two-letter country values. Its + `detection_flags` lack `browser_automation` and `search_bot`: parse missing flags as `false`. + + Deliveries are free and do not use your included identifications. + security: [] + parameters: + - $ref: '#/components/parameters/ShieldSignature' + requestBody: + required: true + description: The event as compact JSON. Verify the signature over these exact bytes. + content: + application/json: + schema: + $ref: '#/components/schemas/IdentificationScoredEvent' + examples: + scored: + $ref: '#/components/examples/IdentificationScored' + rateLimited: + $ref: '#/components/examples/IdentificationScoredRateLimited' + testDelivery: + $ref: '#/components/examples/IdentificationScoredTestDelivery' + responses: + 2XX: + description: Delivery accepted. The response body is ignored. + 4XX: + description: Delivery rejected, for example with `401` when the signature does not verify. Any status other than 2xx, and a timeout after 1 second, counts as a failed delivery; failed deliveries are not retried today. + webhook.ping: + post: + operationId: webhookPing + tags: + - Webhooks + summary: Endpoint verification + description: |- + Sent when you verify an endpoint in the analytics dashboard. It carries no `data`. A 2xx + answer within 5 seconds marks the endpoint as verified; anything else marks the verification + as failed. + + The body is signed exactly like `identification.scored`. Its keys are sorted alphabetically + and `created_at` has second precision. Worked example with the test secret + `whsec_00112233445566778899aabbccddeeff`: the body + + ```json + {"created_at":"2026-09-30T12:34:56Z","event_type":"webhook.ping","schema_version":"2026-06-01"} + ``` + + arrives with `X-Shield-Signature: sha256=ea2685733d254f7028fb031c4214583b0650de01e6c8c93131236024edd9fdd8`. + security: [] + parameters: + - $ref: '#/components/parameters/ShieldSignature' + requestBody: + required: true + description: The ping as compact JSON with sorted keys. + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPingEvent' + examples: + ping: + $ref: '#/components/examples/WebhookPing' + responses: + 2XX: + description: Endpoint verified. The response body is ignored. + 4XX: + description: Verification failed. Any status other than 2xx, and a timeout after 5 seconds, fails it. +components: + securitySchemes: + historyApiKey: + type: http + scheme: bearer + bearerFormat: sec_xxxxxxxx-xxxxxxxx-xxxxxxxx + description: 'Private API Key of one domain, sent as `Authorization: Bearer `. Keys look like `sec_` followed by three groups of eight lowercase letters or digits separated by `-`. Create and rotate it in the analytics dashboard. It reads the History API of that domain only; keep it on your server.' + managementSecretKey: + type: http + scheme: bearer + description: 'Secret Key of the domain, sent as `Authorization: Bearer ` together with the registered domain in the `X-Shield-Domain` header. Treat the key as opaque. Find it in the analytics dashboard; keep it on your server.' + parameters: + HistorySearchType: + name: search_type + in: path + required: true + description: |- + Identifier to search by. Only these seven values are supported: + - `request_id`: one identification (read a verdict); + - `device_id`: every identification of one device; + - `user_hid`: every identification of one account; + - `visitor_id`: every identification of one visitor; + - `ip`: every identification from one public IPv4 address; + - `session_id`: every identification of one visit; + - `cookie_id`: every identification with one browser cookie. + + The server does not reject other values: it ignores them and returns the latest identifications + of the whole domain, so restrict the value on your side. + schema: + type: string + enum: + - request_id + - device_id + - user_hid + - visitor_id + - ip + - session_id + - cookie_id + examples: + requestId: + summary: Read one verdict + value: request_id + deviceId: + summary: All identifications of one device + value: device_id + HistoryValue: + name: value + in: path + required: true + description: |- + Value of the identifier, validated on your side before sending: + - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, + the nil UUID included, matching + `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it + lowercase. + - `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched. + - `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the + server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and + percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including + `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so + `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching + rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build + this path yourself when yours does. A User HID that contains `/` cannot be searched, and most + HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern + rejects these values. Hex-encoded hashes need no escaping at all. + + The server does not validate the value: a malformed UUID or IPv4 address gets a `500`. + schema: + type: string + minLength: 1 + pattern: ^(?:[^/.][^/]*|\.[^/.][^/]*|\.\.[^/]+)$ + examples: + requestId: + summary: A request ID + value: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + ip: + summary: A public IPv4 address + value: 203.0.113.24 + userHid: + summary: A User HID + value: 9f86d081884c7d659a2feaa0c55ad015 + HistoryLimit: + name: limit + in: query + required: false + description: Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side. + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + examples: + one: + summary: Read one verdict + value: 1 + page: + summary: A full page + value: 100 + HistoryOffset: + name: offset + in: query + required: false + description: 'Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`.' + schema: + type: integer + minimum: 0 + default: 0 + examples: + first: + summary: First page + value: 0 + second: + summary: Second page of 100 + value: 100 + ShieldDomain: + name: X-Shield-Domain + in: header + required: true + description: 'Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.' + schema: + type: string + minLength: 1 + maxLength: 253 + examples: + domain: + summary: A registered domain + value: example.com + DeprecatedHistoryType: + name: type + in: path + required: true + description: Identifier to search by. Other values get a `404` with a bare JSON string such as `"auto is not supported"`. + schema: + type: string + enum: + - request_id + - device_id + - user_hid + - visitor_id + - ip + - session_id + - cookie_id + examples: + requestId: + summary: Search by request ID + value: request_id + DeprecatedHistoryValue: + name: value + in: path + required: true + description: Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. + schema: + type: string + minLength: 1 + examples: + requestId: + summary: A request ID + value: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + DeprecatedHistoryLimit: + name: limit + in: query + required: false + description: Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`. + schema: + type: integer + minimum: 1 + maximum: 100 + default: 100 + examples: + ten: + summary: Ten rows + value: 10 + ShieldSignature: + name: X-Shield-Signature + in: header + required: true + description: |- + `sha256=` followed by the lowercase hex HMAC-SHA256 of the raw request body: + - key: your endpoint's signing secret as UTF-8 bytes, the `whsec_` prefix included (not hex- or + base64-decoded, not stripped); + - message: the exact bytes of the body as received. + + Compare it with your own digest in constant time, before parsing the JSON. The example values + are the signatures of the example bodies (in their compact form as sent) with the test secret + `whsec_00112233445566778899aabbccddeeff`. + schema: + type: string + pattern: ^sha256=[0-9a-f]{64}$ + examples: + identificationScored: + summary: Signature of the identification.scored example + value: sha256=397ff9bd26888e9e86addc2d920a8c5b2037251a3a1181f3b4810ca6c5f78062 + webhookPing: + summary: Signature of the webhook.ping example + value: sha256=ea2685733d254f7028fb031c4214583b0650de01e6c8c93131236024edd9fdd8 + schemas: + RequestId: + type: string + format: uuid + 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. + examples: + - a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + SessionId: + type: string + format: uuid + 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. + examples: + - b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e + CookieId: + type: string + format: uuid + description: First-party browser identifier kept by the ShieldLabs agent (UUID v4). A missing or malformed value is stored as the nil UUID. + examples: + - c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f + DeviceId: + type: string + format: uuid + 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.' + examples: + - d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a + - 00000000-0000-0000-0000-000000000000 + VisitorId: + type: string + format: uuid + 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.' + examples: + - e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b + Ipv4: + type: string + format: ipv4 + description: Dotted IPv4 address. `0.0.0.0` when no IPv4 address is known (for example for visitors on IPv6); such identifications cannot be searched by IP. + examples: + - 203.0.113.24 + - 0.0.0.0 + OperatingSystem: + type: string + description: 'Operating system name, for example `Windows`, `Mac OS X`, `Linux`, `Android`, `IOS (iPhone)`, `IOS (iPad)`, `ChromeOS` or `Unknown`. Open set: display it, do not branch on it.' + examples: + - Windows + - Mac OS X + - Android + Browser: + type: string + description: 'Browser name, for example `Chrome`, `Safari`, `Firefox`, `Microsoft Edge`, `Opera`, `Samsung Internet`, `Brave`, `Chrome (iOS)`, `Safari (iOS)` or `Unknown`. Open set: display it, do not branch on it.' + examples: + - Chrome + - Safari + DeviceType: + type: string + description: 'Device class from the browser. Known values: `desktop`, `mobile`, `tablet` and `unknown` (the class could not be determined). The set is open: keep values added in later versions and treat them as `unknown`.' + x-extensible-enum: + - desktop + - mobile + - tablet + - unknown + examples: + - desktop + Country: + type: string + description: English country name from IP intelligence, for example `Germany` or `United States` (not an ISO code). Empty string when the country is unknown. + examples: + - Netherlands + - United States + - '' + ConnectionType: + type: string + description: |- + How the visitor connected. Known values: + - `direct`: a regular connection; + - `mobile`: a mobile carrier network; + - `vpn`: a VPN; + - `proxy`: a proxy, datacenter or hosting network (search-engine crawlers are reported here too); + - `tor`: the Tor network; + - `privacy_relay`: a privacy relay such as iCloud Private Relay; + - `browser_vpn_proxy`: a VPN or proxy built into the browser or one of its extensions; + - `unknown`: not enough data. + + The value can say `vpn` while `detection_flags.vpn` is `false` (IP intelligence classified the + network, but the scored VPN check did not fire). Branch on `detection_flags` for decisions. + The set is open: keep values added in later versions and treat them as `unknown`. + x-extensible-enum: + - direct + - mobile + - vpn + - proxy + - tor + - privacy_relay + - browser_vpn_proxy + - unknown + examples: + - direct + RiskScore: + type: integer + minimum: 0 + description: |- + Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0. + + Risk bands are computed on your side from the score; no band field exists on the wire: + - trusted: 0-29 + - suspicious: 30-59 + - dangerous: 60-100 + + A value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the + ingest rate limit, and the identification carries exactly one signal, + `{"name":"rate_limited","weight":999}`, usually with nil identifiers. Treat every value above + 100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued + while it stays blocked get no row and no webhook, so they stay unverified. + + The score usually equals the sum of the signal weights capped at 100, but carried-forward + verdicts and corrections make that unreliable: never recompute or validate it yourself. + examples: + - 0 + - 35 + - 80 + - 999 + ScoreDetail: + type: object + description: One entry behind the score, in the PascalCase shape the server stores. `Value` is the weight (0 for informational entries); `Description` is free text for display, never branch on it. + required: + - Value + - Description + properties: + Value: + type: integer + description: Weight of the entry. Can be negative; 0 for informational entries. + examples: + - 10 + Description: + type: string + description: Human-readable description, for example `Is proxy` or `Antidetect browser (turn_block)`. + examples: + - Is proxy + HistoryTimestamp: + type: string + pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?$ + description: Time of the identification as `YYYY-MM-DD HH:MM:SS.mmm` in UTC, without a zone designator (not RFC 3339). Older rows can lack the milliseconds. + examples: + - '2026-09-30 12:34:56.123' + - '2026-09-30 13:05:12' + NetworkClass: + type: string + description: 'Connection class of one IP address from IP intelligence. Known values: `direct`, `mobile`, `vpn`, `proxy`, `tor`, `privacy_relay` and the empty string when unknown. The set is open: keep values added in later versions.' + x-extensible-enum: + - direct + - mobile + - vpn + - proxy + - tor + - privacy_relay + - '' + examples: + - direct + TrafficChannel: + type: string + description: 'Marketing channel of the visit. Known values: `Google Ads`, `Meta`, `TikTok`, `LinkedIn`, `X`, `Pinterest`, `Microsoft Ads`, `Organic Search`, `Search bot`, `Referral`, `Direct`, `Other` and the empty string. Resolved in this order: a click ID, then UTM parameters, then the referrer (search engines give `Organic Search`, social networks give the platform name, other sites give `Referral`), otherwise `Direct`. Search-engine crawlers get `Search bot`. Empty string on identifications without attribution, such as rate-limit marker rows. The set is open: keep values added in later versions and treat them as `Other`.' + x-extensible-enum: + - Google Ads + - Meta + - TikTok + - LinkedIn + - X + - Pinterest + - Microsoft Ads + - Organic Search + - Search bot + - Referral + - Direct + - Other + - '' + examples: + - Google Ads + - Direct + TrafficChannelGroup: + type: string + description: 'Group of the marketing channel. Known values: `Paid Search`, `Paid Social`, `Organic`, `Bot`, `Social`, `Referral`, `Direct` and `Other`. History API only; not part of the webhook. The set is open: keep values added in later versions and treat them as `Other`.' + x-extensible-enum: + - Paid Search + - Paid Social + - Organic + - Bot + - Social + - Referral + - Direct + - Other + examples: + - Paid Search + TrafficReason: + type: string + description: 'Why the channel was chosen. Known values: `gclid_present`, `msclkid_present`, `ttclid_present`, `fbclid_present`, `utm_match`, `referrer_search_engine`, `ip_crawler_detected`, `referrer_social`, `external_referrer` and `no_source_detected`. History API only; not part of the webhook. The set is open: keep values added in later versions.' + x-extensible-enum: + - gclid_present + - msclkid_present + - ttclid_present + - fbclid_present + - utm_match + - referrer_search_engine + - ip_crawler_detected + - referrer_social + - external_referrer + - no_source_detected + examples: + - gclid_present + ClickIdType: + type: string + description: 'Ad click identifier found in the landing URL. Known values: `gclid`, `gbraid`, `wbraid`, `msclkid`, `ttclid`, `fbclid` and the empty string when there is none. `fbclid` counts only together with a Meta referrer or a Meta `utm_source`. The set is open: keep values added in later versions.' + x-extensible-enum: + - gclid + - gbraid + - wbraid + - msclkid + - ttclid + - fbclid + - '' + examples: + - gclid + - '' + HistoryRow: + type: object + additionalProperties: true + description: |- + 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. + required: + - request_id + - session_id + - cookie_id + - 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 + properties: + request_id: + $ref: '#/components/schemas/RequestId' + session_id: + $ref: '#/components/schemas/SessionId' + cookie_id: + $ref: '#/components/schemas/CookieId' + domain: + type: string + description: Host the identification came from. Can be a subdomain of your registered domain. + examples: + - shop.example.com + site_domain: + type: string + description: Your registered domain, present when the identification came from a subdomain. Omitted when empty. + examples: + - example.com + user_hid: + type: string + description: User HID exactly as it was passed to the agent (hashed or pseudonymous account identifier). `anonymous` for anonymous checks; `fail`, `-1` and `unknown` also mean "no user". Empty string when no value was stored. Leave the empty string and these values out when you count accounts. + examples: + - 9f86d081884c7d659a2feaa0c55ad015 + - anonymous + device_id: + $ref: '#/components/schemas/DeviceId' + visitor_id: + $ref: '#/components/schemas/VisitorId' + ip: + $ref: '#/components/schemas/Ipv4' + description: Public IPv4 address of the HTTP request; `0.0.0.0` when none (for example IPv6 visitors). + os: + $ref: '#/components/schemas/OperatingSystem' + browser: + $ref: '#/components/schemas/Browser' + device_type: + $ref: '#/components/schemas/DeviceType' + country: + $ref: '#/components/schemas/Country' + description: Country of `ip` as an English country name, or an empty string. + connection_type: + $ref: '#/components/schemas/ConnectionType' + score: + $ref: '#/components/schemas/RiskScore' + score_details: + type: string + contentMediaType: application/json + contentSchema: + type: array + items: + $ref: '#/components/schemas/ScoreDetail' + description: |- + The entries behind `score` as a JSON-encoded **string** holding an array of + `{"Value": , "Description": }`. Parse it before use. Scored entries come + first, followed by informational entries with `Value` 0, which can be long. Empty string + when no details were stored. + + The webhook `signals` are the entries with a non-zero `Value`, in the same order, with each + description turned into a signal name (for example `Is proxy` becomes `proxy`). Descriptions + are free text for display: never branch on them. + examples: + - '[{"Value":10,"Description":"Is proxy"},{"Value":0,"Description":"Check Incomplete"}]' + - '' + created_at: + $ref: '#/components/schemas/HistoryTimestamp' + ver: + type: integer + format: int64 + description: Version of the row in Unix milliseconds. It increases every time the row is refined, for example when late network data re-scores it after the webhook was sent. + examples: + - 1790771696123 + web_rtc_ip: + $ref: '#/components/schemas/Ipv4' + description: Local IP address observed by the ShieldLabs network check; `0.0.0.0` when none. + web_rtc_country: + $ref: '#/components/schemas/Country' + description: Country of `web_rtc_ip`, or an empty string. + web_rtc_connection_type: + $ref: '#/components/schemas/NetworkClass' + description: Connection class of `web_rtc_ip`, or an empty string. + webrtc_leak_ip: + $ref: '#/components/schemas/Ipv4' + description: Local network address leaked by the browser; `0.0.0.0` when none. + webrtc_leak_country: + $ref: '#/components/schemas/Country' + description: Country of `webrtc_leak_ip`, or an empty string. + webrtc_leak_connection_type: + $ref: '#/components/schemas/NetworkClass' + description: Connection class of `webrtc_leak_ip`, or an empty string. + webrtc_leak_source: + type: string + description: 'Which check found the local network leak. Known values: `scanner`, `shield`, `none` and the empty string. `none` or an empty string when there is no leak; the webhook `local_ip` then uses `web_rtc_ip`. The set is open: keep values added in later versions.' + x-extensible-enum: + - scanner + - shield + - none + - '' + examples: + - none + is_vpn: + type: boolean + description: Same meaning as `detection_flags.vpn`. + is_tor: + type: boolean + description: Same meaning as `detection_flags.tor`. + is_proxy: + type: boolean + description: Same meaning as `detection_flags.proxy`. + is_datacenter: + type: boolean + description: Same meaning as `detection_flags.datacenter_ip`. + is_abuser: + type: boolean + description: Same meaning as `detection_flags.abuser`. + is_privacy_relay: + type: boolean + description: Same meaning as `detection_flags.privacy_relay`. + is_stun_not_checked: + type: boolean + description: Same meaning as `detection_flags.stun_not_checked`. + check_incomplete: + type: boolean + description: Same meaning as `detection_flags.check_incomplete`. Always `false` for search-engine crawlers. + is_antidetect: + type: boolean + description: Same meaning as `detection_flags.anti_detect_browser`. + is_os_mismatch: + type: boolean + description: Same meaning as `detection_flags.os_mismatch`. + is_os_not_detected: + type: boolean + description: Same meaning as `detection_flags.os_not_detected`. + is_timezone_mismatch: + type: boolean + description: Same meaning as `detection_flags.timezone_mismatch`. + is_js_disabled: + type: boolean + description: Same meaning as `detection_flags.javascript_disabled`. Always `false` for search-engine crawlers. + is_browser_automation: + type: boolean + description: Same meaning as `detection_flags.browser_automation`. + is_incognito: + type: boolean + description: Same meaning as `detection_flags.incognito`. Always `false` for search-engine crawlers. + is_search_bot: + type: boolean + description: Same meaning as `detection_flags.search_bot`. + is_suspicious_paid_click: + type: boolean + description: Same meaning as `detection_flags.suspicious_paid_click`. Omitted when `false`. + entry_url: + type: string + description: Landing page URL without the `#fragment` (webhook `traffic_source.landing_url`). Omitted when empty. It keeps the query string, which can contain personal data. + examples: + - https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123 + utm_source: + type: string + description: '`utm_source`, lowercased. Omitted when empty.' + examples: + - google + utm_medium: + type: string + description: '`utm_medium`, lowercased. Omitted when empty.' + examples: + - cpc + utm_campaign: + type: string + description: '`utm_campaign` as sent. Omitted when empty.' + examples: + - spring_launch + utm_content: + type: string + description: '`utm_content` as sent. Omitted when empty.' + examples: + - banner_a + utm_term: + type: string + description: '`utm_term` as sent. Omitted when empty.' + examples: + - device intelligence + traffic_channel: + $ref: '#/components/schemas/TrafficChannel' + description: Marketing channel (webhook `traffic_source.channel`). Omitted when empty. + traffic_channel_group: + $ref: '#/components/schemas/TrafficChannelGroup' + description: Group of the marketing channel. Omitted when empty. + traffic_reason: + $ref: '#/components/schemas/TrafficReason' + description: Why the channel was chosen. Omitted when empty. + referrer_domain: + type: string + description: Registrable domain of the referrer without `www.`; the crawler name (for example `GoogleBot`) for search-engine crawlers. Omitted when empty. + examples: + - news.example.org + click_id_type: + $ref: '#/components/schemas/ClickIdType' + description: Ad click identifier type found in the landing URL. Omitted when empty. + HistoryPage: + type: object + description: One page of identifications, newest first. + required: + - data + - total + properties: + data: + type: array + description: Identifications on this page, ordered by `created_at` descending. Empty when nothing matched. + items: + $ref: '#/components/schemas/HistoryRow' + total: + type: integer + minimum: 0 + description: Number of identifications that match the search in total, across all pages. Page with `offset` while it is below `total`. + examples: + - 37 + ErrorBody: + type: object + description: Error object sent by the History API and by the Management API rate and load limits. + required: + - error + properties: + error: + type: string + description: Human-readable error message. Branch on the HTTP status, not on this text. + examples: + - too many requests + ErrorBodyText: + type: string + contentMediaType: application/json + contentSchema: + $ref: '#/components/schemas/ErrorBody' + description: 'JSON text followed by a newline, sent with `Content-Type: text/plain; charset=utf-8`. Parse it as JSON: it holds `{"error": "..."}`.' + examples: + - | + {"error":"invalid api key"} + PlainText: + type: string + description: Plain text body. + examples: + - 404 page not found + HtmlText: + type: string + description: HTML error page from the edge proxy. Do not parse it; branch on the status. + examples: + -

502 Bad Gateway

+ MaskedKey: + type: string + pattern: ^(\*+.{4}|.{0,4})$ + description: A key with every character except the last four replaced by `*`, keeping the original length. Keys of four characters or fewer are returned as they are. + examples: + - '****************************a3f8' + DomainProfile: + type: object + description: Profile of the registered domain. The keys are PascalCase on the wire. Ignore keys you do not know. + required: + - Domain + - Weight + - Callback + - PublicKey + - Secret + - CreatedAt + properties: + Domain: + type: string + description: The registered domain, as sent in `X-Shield-Domain`. + examples: + - example.com + Weight: + type: integer + description: Remaining included identifications of the account (shared by its domains). Can be negative when the account is over its included volume. + examples: + - 148230 + Callback: + type: string + description: 'Legacy field kept for compatibility, normally an empty string. Webhook deliveries do not use it: configure webhook endpoints in the analytics dashboard.' + examples: + - '' + PublicKey: + $ref: '#/components/schemas/MaskedKey' + description: The domain's Public Key, masked. + Secret: + $ref: '#/components/schemas/MaskedKey' + description: The domain's Secret Key, masked. + CreatedAt: + type: string + format: date-time + description: When the domain was registered, RFC 3339 in UTC with second precision. `0001-01-01T00:00:00Z` when unknown. + examples: + - '2026-01-15T09:00:00Z' + LegacySnapshot: + type: object + additionalProperties: true + description: One identification as returned by the deprecated Management API history endpoint (PascalCase keys). Also carries diagnostic network fields that are not part of the stable contract. Use the History API row instead. + required: + - RequestID + - SessionID + - CookieID + - DeviceID + - VisitorID + - IP + - ConnectionType + - WebRtcHIP + - WebRtcCountry + - WebRtcConnectionType + - OS + - Browser + - DeviceType + - Country + - UserHID + - Score + - Details + - LastRequestTime + properties: + RequestID: + $ref: '#/components/schemas/RequestId' + SessionID: + $ref: '#/components/schemas/SessionId' + CookieID: + $ref: '#/components/schemas/CookieId' + DeviceID: + $ref: '#/components/schemas/DeviceId' + VisitorID: + $ref: '#/components/schemas/VisitorId' + IP: + $ref: '#/components/schemas/Ipv4' + description: Public IPv4 address of the HTTP request. + ConnectionType: + $ref: '#/components/schemas/ConnectionType' + WebRtcHIP: + $ref: '#/components/schemas/Ipv4' + description: Local IP address observed by the ShieldLabs network check (not hashed); `0.0.0.0` when none. + WebRtcCountry: + $ref: '#/components/schemas/Country' + description: Country of `WebRtcHIP`, or an empty string. + WebRtcConnectionType: + $ref: '#/components/schemas/NetworkClass' + description: Connection class of `WebRtcHIP`, or an empty string. + OS: + $ref: '#/components/schemas/OperatingSystem' + Browser: + $ref: '#/components/schemas/Browser' + DeviceType: + $ref: '#/components/schemas/DeviceType' + Country: + $ref: '#/components/schemas/Country' + description: Country of `IP` as an English country name, or an empty string. + UserHID: + type: string + description: User HID as passed to the agent; `anonymous` for anonymous checks. + examples: + - 9f86d081884c7d659a2feaa0c55ad015 + Score: + $ref: '#/components/schemas/RiskScore' + Details: + type: array + description: Every entry behind `Score`, informational entries with `Value` 0 included (unlike the History API, this is a parsed array, not a string). + items: + $ref: '#/components/schemas/ScoreDetail' + LastRequestTime: + type: string + format: date-time + description: Time of the identification, RFC 3339 with fractional seconds. + examples: + - '2026-09-30T12:34:56.123Z' + LegacyErrorMessage: + type: + - string + - 'null' + description: A bare JSON string with the error message, or the JSON literal `null` (an unexpected database error, for example for an IPv6 value). + examples: + - fail parse uuid + - null + HealthStatus: + type: object + description: Liveness status. + required: + - status + properties: + status: + type: string + const: ok + description: Always `ok` when the service answers. + SchemaVersion: + type: string + minLength: 1 + description: Version of the webhook payload contract. Every event sent today carries `2026-06-01`. Accept other values, so that a future version does not break your handler. + examples: + - '2026-06-01' + Rfc3339Timestamp: + type: string + format: date-time + pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?Z$ + 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. + examples: + - '2026-09-30T12:34:57.482913041Z' + - '2026-09-30T12:34:56Z' + UserHid: + type: + - string + - 'null' + description: |- + User HID: your hashed or pseudonymous account identifier, exactly as it was passed to the + ShieldLabs agent. Pass a hashed value, never a raw email address or database ID. + + Values that do not identify a user: + - `anonymous`: an anonymous check; + - `fail`: the agent sent no value; + - `-1` and `unknown`: rows created by ShieldLabs itself, such as rate-limit marker rows. + + `null` only when the stored value is an empty string. Leave `null` and the values above out + when you count the accounts of one device, visitor or IP address. + examples: + - 9f86d081884c7d659a2feaa0c55ad015 + - anonymous + - null + Ipv4OrEmpty: + type: string + pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})?$ + description: Dotted IPv4 address, or an empty string when no IPv4 address is known (for example for visitors on IPv6). + examples: + - 203.0.113.24 + - '' + IpInfo: + type: object + description: An IPv4 address and its country. Both keys are always present and can be empty strings. + required: + - ip + - country + properties: + ip: + $ref: '#/components/schemas/Ipv4OrEmpty' + country: + $ref: '#/components/schemas/Country' + TrafficSource: + type: object + description: Where the visit came from. All nine keys are always present; values can be empty strings. + required: + - channel + - referrer_domain + - landing_url + - click_id_type + - utm_source + - utm_medium + - utm_campaign + - utm_content + - utm_term + properties: + channel: + $ref: '#/components/schemas/TrafficChannel' + referrer_domain: + type: string + description: Registrable domain of the referrer without `www.`. For search-engine crawlers, the crawler name (for example `GoogleBot`). + examples: + - google.com + landing_url: + type: string + description: 'Landing page URL without the `#fragment`. It keeps the query string, which can contain personal data: store it with care.' + examples: + - https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123 + click_id_type: + $ref: '#/components/schemas/ClickIdType' + utm_source: + type: string + description: '`utm_source` query parameter, lowercased.' + examples: + - google + utm_medium: + type: string + description: '`utm_medium` query parameter, lowercased.' + examples: + - cpc + utm_campaign: + type: string + description: '`utm_campaign` query parameter as sent.' + examples: + - spring_launch + utm_content: + type: string + description: '`utm_content` query parameter as sent.' + examples: + - '' + utm_term: + type: string + description: '`utm_term` query parameter as sent.' + examples: + - '' + Signal: + type: object + description: One weighted risk signal behind the Risk Score. Only signals with a non-zero weight are listed, in scoring order. The same name can appear twice, and weights can be negative. + required: + - name + - weight + properties: + name: + type: string + minLength: 1 + 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. + examples: + - proxy + - antidetect_browser + weight: + type: integer + description: Points the signal contributed. Can be negative (`stun_late_correction` is -30) and is 999 for `rate_limited`. Weights can change between releases; never add them up yourself. + examples: + - 10 + - -30 + DetectionFlags: + type: object + description: |- + Stable yes/no verdicts for the identification. Always all 19 keys. Branch on these flags and on + the Risk Score; signal names are for display and logging. + + When `search_bot` is `true`, `incognito`, `check_incomplete`, `ip_mismatch` and + `javascript_disabled` are always `false`. + required: + - vpn + - privacy_relay + - browser_vpn_proxy + - tor + - proxy + - datacenter_ip + - abuser + - os_mismatch + - os_not_detected + - timezone_mismatch + - anti_detect_browser + - browser_automation + - ip_mismatch + - incognito + - search_bot + - suspicious_paid_click + - javascript_disabled + - stun_not_checked + - check_incomplete + properties: + vpn: + type: boolean + description: A VPN was detected (scored `vpn` signal). + privacy_relay: + type: boolean + description: A privacy relay such as iCloud Private Relay was detected. + browser_vpn_proxy: + type: boolean + description: A VPN or proxy built into the browser or one of its extensions. `true` exactly when `connection_type` is `browser_vpn_proxy`. + tor: + type: boolean + description: The request came through the Tor network. + proxy: + type: boolean + description: A proxy was detected. + datacenter_ip: + type: boolean + description: The public IP belongs to a datacenter or hosting range. + abuser: + type: boolean + description: The public IP has a record of abuse in IP intelligence. + os_mismatch: + type: boolean + description: The operating system seen on the network differs from the one the browser reports. + os_not_detected: + type: boolean + description: The operating system could not be determined from the User-Agent or the network. + timezone_mismatch: + type: boolean + description: The browser timezone differs from the timezone of the IP location. + anti_detect_browser: + type: boolean + description: An anti-detect browser was detected. + browser_automation: + type: boolean + description: Browser automation was detected, for example a WebDriver-controlled browser. + ip_mismatch: + type: boolean + description: 'The public IP differs from the local IP found by the browser network check. Informational: it does not add to the score.' + incognito: + type: boolean + description: The browser runs in a private window. + search_bot: + type: boolean + description: A search-engine crawler. Its Risk Score is always 0. + suspicious_paid_click: + type: boolean + description: The visit came from a paid ad click (Google Ads, Meta, TikTok, Microsoft Ads, LinkedIn, Pinterest or X) and the Risk Score is 60 or more (the 999 marker included). + javascript_disabled: + type: boolean + description: JavaScript, or the browser APIs the checks need, were unavailable. + stun_not_checked: + type: boolean + description: The browser network (STUN) check did not complete. Cleared again when a late network result arrives. + check_incomplete: + type: boolean + description: Part of the browser checks timed out, so the verdict rests on partial data. Informational. + IdentificationScoredData: + type: object + description: The scored identification. Every key is always present (no key is ever omitted); only `user_hid` can be `null`. + required: + - 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 + properties: + request_id: + $ref: '#/components/schemas/RequestId' + visitor_id: + $ref: '#/components/schemas/VisitorId' + device_id: + $ref: '#/components/schemas/DeviceId' + session_id: + $ref: '#/components/schemas/SessionId' + cookie_id: + $ref: '#/components/schemas/CookieId' + user_hid: + $ref: '#/components/schemas/UserHid' + domain: + type: string + description: Registered domain of your site (the request host when no registered domain matched). + examples: + - example.com + public_ip: + $ref: '#/components/schemas/IpInfo' + description: Public IPv4 address of the HTTP request and its country. `ip` is empty when the request did not arrive over IPv4. + local_ip: + $ref: '#/components/schemas/IpInfo' + description: 'Local IP address found by the browser network check (WebRTC): the leaked address when a local network leak was found, otherwise the address ShieldLabs observed. Both keys are empty when the check found nothing.' + connection_type: + $ref: '#/components/schemas/ConnectionType' + os: + $ref: '#/components/schemas/OperatingSystem' + browser: + $ref: '#/components/schemas/Browser' + device_type: + $ref: '#/components/schemas/DeviceType' + traffic_source: + $ref: '#/components/schemas/TrafficSource' + risk_score: + $ref: '#/components/schemas/RiskScore' + signals: + type: array + description: Weighted risk signals behind `risk_score`, in scoring order. Can be empty. The rate-limit marker carries exactly one entry, `{"name":"rate_limited","weight":999}`. + items: + $ref: '#/components/schemas/Signal' + detection_flags: + $ref: '#/components/schemas/DetectionFlags' + observed_at: + $ref: '#/components/schemas/Rfc3339Timestamp' + description: When scoring finished and the event was built (not the page view time); identical to the envelope `created_at`. RFC 3339 in UTC with up to 9 fractional digits. + 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.' + required: + - event_type + - schema_version + - created_at + - data + properties: + event_type: + type: string + const: identification.scored + description: Event type. Ignore events whose type you do not know instead of failing. + schema_version: + $ref: '#/components/schemas/SchemaVersion' + created_at: + $ref: '#/components/schemas/Rfc3339Timestamp' + description: When the event was built. Equal to `data.observed_at`. + data: + $ref: '#/components/schemas/IdentificationScoredData' + 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. + required: + - event_type + - schema_version + - created_at + properties: + event_type: + type: string + const: webhook.ping + description: Event type. + schema_version: + $ref: '#/components/schemas/SchemaVersion' + created_at: + $ref: '#/components/schemas/Rfc3339Timestamp' + description: When the ping was sent, with second precision. + examples: + HistoryPage: + summary: Five identifications + description: 'One page of five identifications out of 37 matches: a dangerous paid click through a proxy with an anti-detect browser, a trusted anonymous visit, a VPN visit with a local network leak and a late network correction, a rate-limit marker (999) and a search-engine crawler.' + value: + data: + - request_id: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + session_id: b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e + cookie_id: c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f + domain: shop.example.com + site_domain: example.com + user_hid: 9f86d081884c7d659a2feaa0c55ad015 + device_id: d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a + visitor_id: e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b + ip: 203.0.113.24 + os: Windows + browser: Chrome + device_type: desktop + country: Netherlands + connection_type: proxy + score: 80 + score_details: '[{"Value":10,"Description":"Is proxy"},{"Value":10,"Description":"Is datacenter"},{"Value":60,"Description":"Antidetect browser (turn_block)"},{"Value":0,"Description":"Check Incomplete"}]' + created_at: '2026-09-30 12:34:56.123' + ver: 1790771696123 + web_rtc_ip: 198.51.100.23 + web_rtc_country: Germany + web_rtc_connection_type: direct + scanner_web_rtc_ip: 0.0.0.0 + scanner_web_rtc_country: '' + scanner_web_rtc_connection_type: '' + webrtc_leak_ip: 0.0.0.0 + webrtc_leak_country: '' + webrtc_leak_connection_type: '' + webrtc_leak_source: none + tcp_mss: 1460 + mtu_value: 1500 + mtu_hint: direct + is_vpn: false + is_tor: false + is_proxy: true + is_datacenter: true + is_abuser: false + is_privacy_relay: false + is_stun_not_checked: false + check_incomplete: false + is_antidetect: true + is_os_mismatch: false + is_os_not_detected: false + is_timezone_mismatch: false + is_js_disabled: false + is_browser_automation: false + is_incognito: false + is_search_bot: false + stun_request_seen: true + is_scanner_stun_passed: false + stun_flow_status: ok + entry_url: https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123 + utm_source: google + utm_medium: cpc + traffic_channel: Google Ads + traffic_channel_group: Paid Search + traffic_reason: gclid_present + click_id_type: gclid + is_suspicious_paid_click: true + - request_id: 7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b + session_id: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d + cookie_id: f0e1d2c3-b4a5-4968-8776-655443322110 + domain: example.com + user_hid: anonymous + device_id: 5d9a1f3e-7b2c-5e4d-8f6a-9b0c1d2e3f4a + visitor_id: 3c4d5e6f-7a8b-5c9d-8e0f-1a2b3c4d5e6f + ip: 192.0.2.44 + os: Mac OS X + browser: Safari + device_type: desktop + country: United States + connection_type: direct + score: 10 + score_details: '[{"Value":10,"Description":"Browser timezone ≠ IP-timezone"}]' + created_at: '2026-09-30 12:40:01.007' + ver: 1790772001007 + web_rtc_ip: 192.0.2.44 + web_rtc_country: United States + web_rtc_connection_type: direct + scanner_web_rtc_ip: 0.0.0.0 + scanner_web_rtc_country: '' + scanner_web_rtc_connection_type: '' + webrtc_leak_ip: 0.0.0.0 + webrtc_leak_country: '' + webrtc_leak_connection_type: '' + webrtc_leak_source: '' + tcp_mss: 1460 + mtu_value: 1500 + mtu_hint: direct + is_vpn: false + is_tor: false + is_proxy: false + is_datacenter: false + is_abuser: false + is_privacy_relay: false + is_stun_not_checked: false + check_incomplete: false + is_antidetect: false + is_os_mismatch: false + is_os_not_detected: false + is_timezone_mismatch: true + is_js_disabled: false + is_browser_automation: false + is_incognito: true + is_search_bot: false + stun_request_seen: true + is_scanner_stun_passed: false + stun_flow_status: ok + - request_id: 9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0 + session_id: b0c1d2e3-f4a5-4b6c-9d7e-8f9a0b1c2d3e + cookie_id: c3d4e5f6-a7b8-4c9d-8e0f-a1b2c3d4e5f6 + domain: example.com + user_hid: '' + device_id: e1f2a3b4-c5d6-5e7f-8a9b-0c1d2e3f4a5b + visitor_id: d2e3f4a5-b6c7-5d8e-9f0a-1b2c3d4e5f6a + ip: 198.51.100.7 + os: Android + browser: Chrome + device_type: mobile + country: France + connection_type: vpn + score: 45 + score_details: '[{"Value":15,"Description":"Is VPN"},{"Value":30,"Description":"Stun is not checked"},{"Value":-30,"Description":"Stun passed (late arrival, corrected)"},{"Value":30,"Description":"Sticky verdict: Stun is not checked (request 11111111-2222-4333-8444-555555555555)"},{"Value":0,"Description":"IP ≠ leakIP (198.51.100.7 ≠ 203.0.113.9, source=scanner)"}]' + created_at: '2026-09-30 13:05:12' + ver: 1790773512000 + web_rtc_ip: 0.0.0.0 + web_rtc_country: '' + web_rtc_connection_type: '' + scanner_web_rtc_ip: 203.0.113.9 + scanner_web_rtc_country: Spain + scanner_web_rtc_connection_type: direct + webrtc_leak_ip: 203.0.113.9 + webrtc_leak_country: Spain + webrtc_leak_connection_type: direct + webrtc_leak_source: scanner + tcp_mss: 1380 + mtu_value: 1420 + mtu_hint: vpn_likely + is_vpn: true + is_tor: false + is_proxy: false + is_datacenter: false + is_abuser: false + is_privacy_relay: false + is_stun_not_checked: true + check_incomplete: true + is_antidetect: false + is_os_mismatch: false + is_os_not_detected: false + is_timezone_mismatch: false + is_js_disabled: false + is_browser_automation: false + is_incognito: false + is_search_bot: false + stun_request_seen: false + is_scanner_stun_passed: true + stun_flow_status: reply_without_request + entry_url: https://example.com/pricing + referrer_domain: news.example.org + traffic_channel: Referral + traffic_channel_group: Referral + traffic_reason: external_referrer + - request_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d + session_id: 00000000-0000-0000-0000-000000000000 + cookie_id: 00000000-0000-0000-0000-000000000000 + domain: example.com + user_hid: '-1' + device_id: 00000000-0000-0000-0000-000000000000 + visitor_id: 00000000-0000-0000-0000-000000000000 + ip: 203.0.113.200 + os: Unknown + browser: Unknown + device_type: desktop + country: '' + connection_type: unknown + score: 999 + score_details: '[{"Value":999,"Description":"User has been banned 1H, to many requests"}]' + created_at: '2026-09-30 13:10:00.500' + ver: 1790773800500 + web_rtc_ip: 0.0.0.0 + web_rtc_country: '' + web_rtc_connection_type: '' + scanner_web_rtc_ip: 0.0.0.0 + scanner_web_rtc_country: '' + scanner_web_rtc_connection_type: '' + webrtc_leak_ip: 0.0.0.0 + webrtc_leak_country: '' + webrtc_leak_connection_type: '' + webrtc_leak_source: '' + tcp_mss: 0 + mtu_value: 0 + mtu_hint: '' + is_vpn: false + is_tor: false + is_proxy: false + is_datacenter: false + is_abuser: false + is_privacy_relay: false + is_stun_not_checked: false + check_incomplete: false + is_antidetect: false + is_os_mismatch: false + is_os_not_detected: false + is_timezone_mismatch: false + is_js_disabled: false + is_browser_automation: false + is_incognito: false + is_search_bot: false + stun_request_seen: false + is_scanner_stun_passed: false + stun_flow_status: '' + - request_id: 4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f + session_id: 5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d + cookie_id: 6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e + domain: example.com + user_hid: anonymous + device_id: 7c8d9e0f-1a2b-5c3d-8e4f-5a6b7c8d9e0f + visitor_id: 8d9e0f1a-2b3c-5d4e-9f5a-6b7c8d9e0f1a + ip: 198.51.100.66 + os: Linux + browser: Chrome + device_type: desktop + country: United States + connection_type: proxy + score: 0 + score_details: '' + created_at: '2026-09-30 13:20:30.250' + ver: 1790774430250 + web_rtc_ip: 203.0.113.77 + web_rtc_country: United States + web_rtc_connection_type: direct + scanner_web_rtc_ip: 0.0.0.0 + scanner_web_rtc_country: '' + scanner_web_rtc_connection_type: '' + webrtc_leak_ip: 0.0.0.0 + webrtc_leak_country: '' + webrtc_leak_connection_type: '' + webrtc_leak_source: none + tcp_mss: 1460 + mtu_value: 1500 + mtu_hint: direct + is_vpn: false + is_tor: false + is_proxy: false + is_datacenter: false + is_abuser: false + is_privacy_relay: false + is_stun_not_checked: false + check_incomplete: false + is_antidetect: false + is_os_mismatch: false + is_os_not_detected: false + is_timezone_mismatch: false + is_js_disabled: false + is_browser_automation: false + is_incognito: false + is_search_bot: true + stun_request_seen: false + is_scanner_stun_passed: false + stun_flow_status: '' + referrer_domain: GoogleBot + traffic_channel: Search bot + traffic_channel_group: Bot + traffic_reason: ip_crawler_detected + total: 37 + HistoryPageEmpty: + summary: Nothing matched + description: No identification matched. When searching by `request_id` right after a protected action, this means "not scored yet" (or an invalid request ID), never "clean". + value: + data: [] + total: 0 + HistoryUnauthorizedMissingHeader: + summary: Missing or malformed Authorization header + description: JSON text sent as `text/plain`, followed by a newline. + value: | + {"error":"missing or invalid authorization header"} + HistoryUnauthorizedInvalidKey: + summary: Unknown, deleted or disabled key + description: JSON text sent as `text/plain`, followed by a newline. + value: | + {"error":"invalid api key"} + NotFoundText: + summary: No route matched + description: Plain text body of an unrouted path. + value: 404 page not found + HistoryTooManyRequests: + summary: Soft rate limit reached + description: More than about 15 requests in the current second for this domain. Retry after about a second. + value: + error: too many requests + HistoryInvalidValue: + summary: Malformed UUID value + description: 'The raw database error for a value that is not a UUID. It repeats for the same request: validate the value instead of retrying.' + value: + error: 'code: 53, message: Cannot convert string ''abc'' to type UUID' + HistoryKeyLookupFailed: + summary: Key lookup failed + description: A transient error while checking the key, sent as JSON text. Retry with backoff. + value: | + {"error":"internal error"} + BadGatewayHtml: + summary: Bad gateway + description: HTML page from the edge proxy. + value:

502 Bad Gateway

+ GatewayTimeoutHtml: + summary: Gateway timeout + description: HTML page from the edge proxy. + value:

504 Gateway Time-out

+ DomainProfile: + summary: Profile of example.com + description: A domain with 148,230 remaining included identifications and masked keys. + value: + Domain: example.com + Weight: 148230 + Callback: '' + PublicKey: '****************************a3f8' + Secret: '****************************9c2d' + CreatedAt: '2026-01-15T09:00:00Z' + ManagementTooManyRequests: + summary: Rate limit reached or block active + description: 'Do not retry: every request gets this answer until the 10-minute block ends.' + value: + error: too many requests + ManagementServerBusy: + summary: Too many requests in flight + description: Retry with backoff. + value: + error: server is busy + LegacySnapshotList: + summary: One identification (deprecated shape) + description: The PascalCase array returned by the deprecated endpoint, for the same identification as the first History API example row. + value: + - RequestID: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + SessionID: b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e + CookieID: c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f + DeviceID: d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a + VisitorID: e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b + IP: 203.0.113.24 + ConnectionType: proxy + TcpMss: 1460 + MtuValue: 1500 + MtuHint: direct + WebRtcHIP: 198.51.100.23 + WebRtcCountry: Germany + WebRtcConnectionType: direct + OS: Windows + Browser: Chrome + DeviceType: desktop + Country: Netherlands + UserHID: 9f86d081884c7d659a2feaa0c55ad015 + Score: 80 + Details: + - Value: 10 + Description: Is proxy + - Value: 10 + Description: Is datacenter + - Value: 60 + Description: Antidetect browser (turn_block) + LastRequestTime: '2026-09-30T12:34:56.123Z' + LegacySnapshotListEmpty: + summary: Nothing matched + description: An empty array. + value: [] + ManagementBadRequestUuid: + summary: Value is not a UUID + description: A bare JSON string. + value: fail parse uuid + ManagementBadRequestIp: + summary: Value is not an IP address + description: A bare JSON string. + value: invalid IP address + ManagementBadRequestEmpty: + summary: Empty value + description: A bare JSON string. + value: value cannot be empty + ManagementBadRequestNull: + summary: Database error + description: The JSON literal `null`, for example for an IPv6 `ip` value. + value: null + ManagementUnsupportedType: + summary: Unsupported identifier type + description: A bare JSON string naming the type that was sent. + value: auto is not supported + HealthOk: + summary: Service is up + description: The only successful answer. + value: + status: ok + 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. + value: + event_type: identification.scored + schema_version: '2026-06-01' + created_at: '2026-09-30T12:34:57.482913041Z' + data: + request_id: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + visitor_id: e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b + device_id: d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a + session_id: b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e + cookie_id: c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f + user_hid: 9f86d081884c7d659a2feaa0c55ad015 + domain: example.com + public_ip: + ip: 203.0.113.24 + country: Netherlands + local_ip: + ip: 198.51.100.23 + country: Germany + connection_type: proxy + os: Windows + browser: Chrome + device_type: desktop + traffic_source: + channel: Google Ads + referrer_domain: google.com + landing_url: https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123 + click_id_type: gclid + utm_source: google + utm_medium: cpc + utm_campaign: '' + utm_content: '' + utm_term: '' + risk_score: 80 + signals: + - name: proxy + weight: 10 + - name: datacenter_ip + weight: 10 + - name: antidetect_browser + weight: 60 + detection_flags: + vpn: false + privacy_relay: false + browser_vpn_proxy: false + tor: false + proxy: true + datacenter_ip: true + abuser: false + os_mismatch: false + os_not_detected: false + timezone_mismatch: false + anti_detect_browser: true + browser_automation: false + ip_mismatch: true + incognito: false + search_bot: false + suspicious_paid_click: true + javascript_disabled: false + stun_not_checked: false + check_incomplete: false + observed_at: '2026-09-30T12:34:57.482913041Z' + IdentificationScoredRateLimited: + summary: Rate-limit marker (999) + description: 'The 999 rate-limit marker: one `rate_limited` signal, nil identifiers and no attribution. It is not a Risk Score.' + value: + event_type: identification.scored + schema_version: '2026-06-01' + created_at: '2026-09-30T13:10:00.5Z' + data: + request_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d + visitor_id: 00000000-0000-0000-0000-000000000000 + device_id: 00000000-0000-0000-0000-000000000000 + session_id: 00000000-0000-0000-0000-000000000000 + cookie_id: 00000000-0000-0000-0000-000000000000 + user_hid: '-1' + domain: example.com + public_ip: + ip: 203.0.113.200 + country: '' + local_ip: + ip: '' + country: '' + connection_type: unknown + os: Unknown + browser: Unknown + 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: 999 + signals: + - name: rate_limited + weight: 999 + detection_flags: + 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 + suspicious_paid_click: false + javascript_disabled: false + stun_not_checked: false + check_incomplete: false + observed_at: '2026-09-30T13:10:00.5Z' + IdentificationScoredTestDelivery: + summary: Test delivery from the analytics dashboard + description: 'The fixed sample sent by the Test button: keys sorted alphabetically, second-precision timestamps, two-letter country values and only 17 detection flags (`browser_automation` and `search_bot` are missing). It differs from the schema in exactly those two flags: parse missing flags as `false`.' + value: + created_at: '2026-09-30T12:34:56Z' + data: + browser: Chrome + connection_type: proxy + cookie_id: 2c9d1e8f-4b7a-4c3e-9d2f-1a8b7c6d5e4f + detection_flags: + abuser: true + anti_detect_browser: false + browser_vpn_proxy: false + check_incomplete: false + datacenter_ip: true + incognito: false + ip_mismatch: false + javascript_disabled: false + os_mismatch: false + os_not_detected: false + privacy_relay: false + proxy: true + stun_not_checked: false + suspicious_paid_click: false + timezone_mismatch: false + tor: false + vpn: false + device_id: 6f1e2d3c-4b5a-5968-8776-655443322110 + device_type: desktop + domain: example.com + local_ip: + country: BY + ip: 198.51.100.10 + observed_at: '2026-09-30T12:34:56Z' + os: Windows + public_ip: + country: BY + ip: 203.0.113.10 + request_id: 13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11 + risk_score: 30 + session_id: 3a2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d + signals: + - name: proxy + weight: 10 + - name: datacenter_ip + weight: 10 + - name: abuser + weight: 10 + traffic_source: + channel: Direct + click_id_type: '' + landing_url: https://example.com/ + referrer_domain: '' + utm_campaign: '' + utm_content: '' + utm_medium: '' + utm_source: '' + utm_term: '' + user_hid: null + visitor_id: 7a6b5c4d-3e2f-5a1b-9c8d-7e6f5a4b3c2d + event_type: identification.scored + schema_version: '2026-06-01' + WebhookPing: + summary: Endpoint verification + description: The ping sent when you verify an endpoint. It has no `data`. + value: + created_at: '2026-09-30T12:34:56Z' + event_type: webhook.ping + schema_version: '2026-06-01' + responses: + HistoryUnauthorized: + description: 'The `Authorization` header is missing or is not a Bearer token, or the Private API Key is unknown, deleted or belongs to a disabled domain. The body is JSON text sent with `Content-Type: text/plain; charset=utf-8`: parse it as JSON anyway. Do not retry.' + content: + text/plain: + schema: + $ref: '#/components/schemas/ErrorBodyText' + examples: + missingHeader: + $ref: '#/components/examples/HistoryUnauthorizedMissingHeader' + invalidKey: + $ref: '#/components/examples/HistoryUnauthorizedInvalidKey' + NotFound: + description: No route matches the method and path, for example because the base URL repeats part of the path or a path value is empty or contains `/`. Plain text body. Check the URL; do not retry. + content: + text/plain: + schema: + $ref: '#/components/schemas/PlainText' + examples: + notFound: + $ref: '#/components/examples/NotFoundText' + HistoryTooManyRequests: + description: 'More than about 15 requests in the current second for this domain (all callers of the domain share the limit). There is no ban: retry after about a second, with backoff.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBody' + examples: + tooManyRequests: + $ref: '#/components/examples/HistoryTooManyRequests' + HistoryServerError: + description: |- + Server error. + - `application/json`: the query failed. A malformed UUID or IPv4 value always ends here with the + raw database message, so validate the path before sending and do not retry such a request. + Other failures are transient. + - `text/plain` (JSON text): the key lookup failed. Transient: retry with backoff. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBody' + examples: + invalidValue: + $ref: '#/components/examples/HistoryInvalidValue' + text/plain: + schema: + $ref: '#/components/schemas/ErrorBodyText' + examples: + keyLookupFailed: + $ref: '#/components/examples/HistoryKeyLookupFailed' + BadGateway: + description: The edge proxy could not reach the service. HTML body. Retry with backoff. + content: + text/html: + schema: + $ref: '#/components/schemas/HtmlText' + examples: + badGateway: + $ref: '#/components/examples/BadGatewayHtml' + GatewayTimeout: + description: The service did not answer the edge proxy in time. HTML body. Retry with backoff. + content: + text/html: + schema: + $ref: '#/components/schemas/HtmlText' + examples: + gatewayTimeout: + $ref: '#/components/examples/GatewayTimeoutHtml' + ManagementUnauthorized: + description: Empty body, no `Content-Type`. `X-Shield-Domain` or `Authorization` is missing or malformed, the domain is unknown or disabled, or the Secret Key is wrong. Do not retry. + ManagementTooManyRequests: + description: 'More than 15 requests in the current minute from your IP, or a 10-minute block is active. The request that exceeds the limit starts the block, and every request during it gets this answer. Do not retry: wait for the block to end and cache results to stay under the limit.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBody' + examples: + tooManyRequests: + $ref: '#/components/examples/ManagementTooManyRequests' + ManagementServerBusy: + description: Too many requests are in flight on the server. Retry with backoff. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBody' + examples: + serverBusy: + $ref: '#/components/examples/ManagementServerBusy' + headers: + Deprecation: + description: Marks the endpoint as deprecated. The value is the literal `true`. + schema: + type: string + const: 'true' + examples: + deprecated: + summary: Deprecated endpoint + value: 'true' + Sunset: + description: HTTP date after which the endpoint stops working. + schema: + type: string + const: Sat, 01 Jan 2027 00:00:00 GMT + examples: + sunset: + summary: Sunset date + value: Sat, 01 Jan 2027 00:00:00 GMT + Link: + description: Points to the replacement endpoint with `rel="successor-version"`. + schema: + type: string + const: ; rel="successor-version" + examples: + successor: + summary: Successor endpoint + value: ; rel="successor-version" diff --git a/sync.sh b/sync.sh new file mode 100755 index 0000000..624350a --- /dev/null +++ b/sync.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + +schemaUrl="${1:-https://raw.githubusercontent.com/ShieldLabs-ai/shieldlabs-openapi/main/dist/shieldlabs-api.yaml}" +schemaDestination="./resources/shieldlabs-api.yaml" + +mkdir -p "$(dirname "$schemaDestination")" + +echo "Downloading $schemaUrl to $schemaDestination" +curl -fSL --retry 3 --proto-redir '=https' --connect-timeout 10 --max-time 120 \ + -o "$schemaDestination" "$schemaUrl" + +echo "OpenAPI schema download complete. Run ./generate.sh to refresh generated/."