diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 56ee145..6772f01 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.70.0"
+ ".": "0.71.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 2018924..ef32d38 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 122
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d9b82fc5346c9be1bf9c2b18792fdcdec6a80a86153ca765c9e93e597b46fa24.yml
-openapi_spec_hash: 9cbaab975acfa421b795d11aa635c57e
-config_hash: 99b2b2a25e8067ad9c9214e38e01d64c
+configured_endpoints: 124
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4fb45d71a99648425c84bdc8e5780920105cede4ee2d4eac67276d0609ac1e94.yml
+openapi_spec_hash: 1f04cb5b36e92db81dfa264c2a59c32a
+config_hash: fb167e754ebb3a14649463725891c9d0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acc9883..35090e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.71.0 (2026-06-26)
+
+Full Changelog: [v0.70.0...v0.71.0](https://github.com/kernel/kernel-python-sdk/compare/v0.70.0...v0.71.0)
+
+### Features
+
+* Add auth connection event timeline endpoint ([871432f](https://github.com/kernel/kernel-python-sdk/commit/871432f827c95b11d132f60b3547c9986e266f6b))
+* Expose audit logs in public SDK ([9c8e9ac](https://github.com/kernel/kernel-python-sdk/commit/9c8e9ace1cdddc6ce727b38fec62beaba6c6fa20))
+
## 0.70.0 (2026-06-24)
Full Changelog: [v0.69.0...v0.70.0](https://github.com/kernel/kernel-python-sdk/compare/v0.69.0...v0.70.0)
diff --git a/api.md b/api.md
index 88588c4..5c1c6b9 100644
--- a/api.md
+++ b/api.md
@@ -300,6 +300,7 @@ from kernel.types.auth import (
LoginResponse,
ManagedAuth,
ManagedAuthCreateRequest,
+ ManagedAuthTimelineEvent,
ManagedAuthUpdateRequest,
SubmitFieldsRequest,
SubmitFieldsResponse,
@@ -317,6 +318,7 @@ Methods:
- client.auth.connections.follow(id) -> ConnectionFollowResponse
- client.auth.connections.login(id, \*\*params) -> LoginResponse
- client.auth.connections.submit(id, \*\*params) -> SubmitFieldsResponse
+- client.auth.connections.timeline(id, \*\*params) -> SyncOffsetPagination[ManagedAuthTimelineEvent]
# Proxies
@@ -441,6 +443,18 @@ Methods:
- client.organization.limits.retrieve() -> OrgLimits
- client.organization.limits.update(\*\*params) -> OrgLimits
+# AuditLogs
+
+Types:
+
+```python
+from kernel.types import AuditLogEntry
+```
+
+Methods:
+
+- client.audit_logs.list(\*\*params) -> SyncPageTokenPagination[AuditLogEntry]
+
# APIKeys
Types:
diff --git a/pyproject.toml b/pyproject.toml
index 84fce8f..9187ed7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.70.0"
+version = "0.71.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index 977b83d..0238b20 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -54,6 +54,7 @@
browsers,
profiles,
projects,
+ audit_logs,
extensions,
credentials,
deployments,
@@ -67,6 +68,7 @@
from .resources.api_keys import APIKeysResource, AsyncAPIKeysResource
from .resources.profiles import ProfilesResource, AsyncProfilesResource
from .resources.auth.auth import AuthResource, AsyncAuthResource
+ from .resources.audit_logs import AuditLogsResource, AsyncAuditLogsResource
from .resources.extensions import ExtensionsResource, AsyncExtensionsResource
from .resources.credentials import CredentialsResource, AsyncCredentialsResource
from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource
@@ -275,6 +277,13 @@ def organization(self) -> OrganizationResource:
return OrganizationResource(self)
+ @cached_property
+ def audit_logs(self) -> AuditLogsResource:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AuditLogsResource
+
+ return AuditLogsResource(self)
+
@cached_property
def api_keys(self) -> APIKeysResource:
"""Create and manage API keys for organization and project-scoped access."""
@@ -620,6 +629,13 @@ def organization(self) -> AsyncOrganizationResource:
return AsyncOrganizationResource(self)
+ @cached_property
+ def audit_logs(self) -> AsyncAuditLogsResource:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AsyncAuditLogsResource
+
+ return AsyncAuditLogsResource(self)
+
@cached_property
def api_keys(self) -> AsyncAPIKeysResource:
"""Create and manage API keys for organization and project-scoped access."""
@@ -873,6 +889,13 @@ def organization(self) -> organization.OrganizationResourceWithRawResponse:
return OrganizationResourceWithRawResponse(self._client.organization)
+ @cached_property
+ def audit_logs(self) -> audit_logs.AuditLogsResourceWithRawResponse:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AuditLogsResourceWithRawResponse
+
+ return AuditLogsResourceWithRawResponse(self._client.audit_logs)
+
@cached_property
def api_keys(self) -> api_keys.APIKeysResourceWithRawResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -976,6 +999,13 @@ def organization(self) -> organization.AsyncOrganizationResourceWithRawResponse:
return AsyncOrganizationResourceWithRawResponse(self._client.organization)
+ @cached_property
+ def audit_logs(self) -> audit_logs.AsyncAuditLogsResourceWithRawResponse:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AsyncAuditLogsResourceWithRawResponse
+
+ return AsyncAuditLogsResourceWithRawResponse(self._client.audit_logs)
+
@cached_property
def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithRawResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -1079,6 +1109,13 @@ def organization(self) -> organization.OrganizationResourceWithStreamingResponse
return OrganizationResourceWithStreamingResponse(self._client.organization)
+ @cached_property
+ def audit_logs(self) -> audit_logs.AuditLogsResourceWithStreamingResponse:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AuditLogsResourceWithStreamingResponse
+
+ return AuditLogsResourceWithStreamingResponse(self._client.audit_logs)
+
@cached_property
def api_keys(self) -> api_keys.APIKeysResourceWithStreamingResponse:
"""Create and manage API keys for organization and project-scoped access."""
@@ -1182,6 +1219,13 @@ def organization(self) -> organization.AsyncOrganizationResourceWithStreamingRes
return AsyncOrganizationResourceWithStreamingResponse(self._client.organization)
+ @cached_property
+ def audit_logs(self) -> audit_logs.AsyncAuditLogsResourceWithStreamingResponse:
+ """Read audit log records for the authenticated organization."""
+ from .resources.audit_logs import AsyncAuditLogsResourceWithStreamingResponse
+
+ return AsyncAuditLogsResourceWithStreamingResponse(self._client.audit_logs)
+
@cached_property
def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithStreamingResponse:
"""Create and manage API keys for organization and project-scoped access."""
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 6d6da8f..0bf9994 100644
--- a/src/kernel/_version.py
+++ b/src/kernel/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "kernel"
-__version__ = "0.70.0" # x-release-please-version
+__version__ = "0.71.0" # x-release-please-version
diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py
index 39d5a08..98152da 100644
--- a/src/kernel/pagination.py
+++ b/src/kernel/pagination.py
@@ -9,13 +9,93 @@
from ._models import BaseModel
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
-__all__ = ["SyncOffsetPagination", "AsyncOffsetPagination"]
+__all__ = ["SyncPageTokenPagination", "AsyncPageTokenPagination", "SyncOffsetPagination", "AsyncOffsetPagination"]
_BaseModelT = TypeVar("_BaseModelT", bound=BaseModel)
_T = TypeVar("_T")
+class SyncPageTokenPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+ next_page_token: Optional[str] = None
+ has_more: Optional[bool] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ next_page_token = self.next_page_token
+ if not next_page_token:
+ return None
+
+ return PageInfo(params={"page_token": next_page_token})
+
+ @classmethod
+ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
+ return cls.construct(
+ None,
+ **{
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
+ "next_page_token": response.headers.get("X-Next-Page-Token"),
+ "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")),
+ },
+ )
+
+
+class AsyncPageTokenPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+ items: List[_T]
+ next_page_token: Optional[str] = None
+ has_more: Optional[bool] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ items = self.items
+ if not items:
+ return []
+ return items
+
+ @override
+ def has_next_page(self) -> bool:
+ has_more = self.has_more
+ if has_more is not None and has_more is False:
+ return False
+
+ return super().has_next_page()
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ next_page_token = self.next_page_token
+ if not next_page_token:
+ return None
+
+ return PageInfo(params={"page_token": next_page_token})
+
+ @classmethod
+ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
+ return cls.construct(
+ None,
+ **{
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
+ "next_page_token": response.headers.get("X-Next-Page-Token"),
+ "has_more": maybe_coerce_boolean(response.headers.get("X-Has-More")),
+ },
+ )
+
+
class SyncOffsetPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
items: List[_T]
has_more: Optional[bool] = None
diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py
index 1497ad6..586c499 100644
--- a/src/kernel/resources/__init__.py
+++ b/src/kernel/resources/__init__.py
@@ -56,6 +56,14 @@
ProjectsResourceWithStreamingResponse,
AsyncProjectsResourceWithStreamingResponse,
)
+from .audit_logs import (
+ AuditLogsResource,
+ AsyncAuditLogsResource,
+ AuditLogsResourceWithRawResponse,
+ AsyncAuditLogsResourceWithRawResponse,
+ AuditLogsResourceWithStreamingResponse,
+ AsyncAuditLogsResourceWithStreamingResponse,
+)
from .extensions import (
ExtensionsResource,
AsyncExtensionsResource,
@@ -186,6 +194,12 @@
"AsyncOrganizationResourceWithRawResponse",
"OrganizationResourceWithStreamingResponse",
"AsyncOrganizationResourceWithStreamingResponse",
+ "AuditLogsResource",
+ "AsyncAuditLogsResource",
+ "AuditLogsResourceWithRawResponse",
+ "AsyncAuditLogsResourceWithRawResponse",
+ "AuditLogsResourceWithStreamingResponse",
+ "AsyncAuditLogsResourceWithStreamingResponse",
"APIKeysResource",
"AsyncAPIKeysResource",
"APIKeysResourceWithRawResponse",
diff --git a/src/kernel/resources/audit_logs.py b/src/kernel/resources/audit_logs.py
new file mode 100644
index 0000000..bce8b71
--- /dev/null
+++ b/src/kernel/resources/audit_logs.py
@@ -0,0 +1,269 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from datetime import datetime
+
+import httpx
+
+from ..types import audit_log_list_params
+from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from .._utils import maybe_transform
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..pagination import SyncPageTokenPagination, AsyncPageTokenPagination
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.audit_log_entry import AuditLogEntry
+
+__all__ = ["AuditLogsResource", "AsyncAuditLogsResource"]
+
+
+class AuditLogsResource(SyncAPIResource):
+ """Read audit log records for the authenticated organization."""
+
+ @cached_property
+ def with_raw_response(self) -> AuditLogsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AuditLogsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AuditLogsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AuditLogsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ *,
+ end: Union[str, datetime],
+ start: Union[str, datetime],
+ auth_strategy: str | Omit = omit,
+ exclude_method: str | Omit = omit,
+ limit: int | Omit = omit,
+ method: str | Omit = omit,
+ page_token: str | Omit = omit,
+ search: str | Omit = omit,
+ search_user_id: SequenceNotStr[str] | Omit = omit,
+ service: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncPageTokenPagination[AuditLogEntry]:
+ """API for searching audit logs.
+
+ Limited to at most 30 day search, returns up to
+ 100 records per page. Not recommended for bulk export.
+
+ Args:
+ end: Upper bound (exclusive) for the audit record timestamp.
+
+ start: Lower bound (inclusive) for the audit record timestamp.
+
+ auth_strategy: Filter by authentication strategy.
+
+ exclude_method: Filter out results by HTTP method.
+
+ limit: Maximum number of results to return.
+
+ method: Filter by HTTP method.
+
+ page_token: Opaque page token from X-Next-Page-Token for the next page of older records.
+
+ search: Free-text search over path, user ID, email, client IP, and status.
+
+ search_user_id: Additional user IDs to OR into free-text search.
+
+ service: Filter by service name.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/audit-logs",
+ page=SyncPageTokenPagination[AuditLogEntry],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "end": end,
+ "start": start,
+ "auth_strategy": auth_strategy,
+ "exclude_method": exclude_method,
+ "limit": limit,
+ "method": method,
+ "page_token": page_token,
+ "search": search,
+ "search_user_id": search_user_id,
+ "service": service,
+ },
+ audit_log_list_params.AuditLogListParams,
+ ),
+ ),
+ model=AuditLogEntry,
+ )
+
+
+class AsyncAuditLogsResource(AsyncAPIResource):
+ """Read audit log records for the authenticated organization."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncAuditLogsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncAuditLogsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncAuditLogsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncAuditLogsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ *,
+ end: Union[str, datetime],
+ start: Union[str, datetime],
+ auth_strategy: str | Omit = omit,
+ exclude_method: str | Omit = omit,
+ limit: int | Omit = omit,
+ method: str | Omit = omit,
+ page_token: str | Omit = omit,
+ search: str | Omit = omit,
+ search_user_id: SequenceNotStr[str] | Omit = omit,
+ service: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[AuditLogEntry, AsyncPageTokenPagination[AuditLogEntry]]:
+ """API for searching audit logs.
+
+ Limited to at most 30 day search, returns up to
+ 100 records per page. Not recommended for bulk export.
+
+ Args:
+ end: Upper bound (exclusive) for the audit record timestamp.
+
+ start: Lower bound (inclusive) for the audit record timestamp.
+
+ auth_strategy: Filter by authentication strategy.
+
+ exclude_method: Filter out results by HTTP method.
+
+ limit: Maximum number of results to return.
+
+ method: Filter by HTTP method.
+
+ page_token: Opaque page token from X-Next-Page-Token for the next page of older records.
+
+ search: Free-text search over path, user ID, email, client IP, and status.
+
+ search_user_id: Additional user IDs to OR into free-text search.
+
+ service: Filter by service name.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._get_api_list(
+ "/audit-logs",
+ page=AsyncPageTokenPagination[AuditLogEntry],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "end": end,
+ "start": start,
+ "auth_strategy": auth_strategy,
+ "exclude_method": exclude_method,
+ "limit": limit,
+ "method": method,
+ "page_token": page_token,
+ "search": search,
+ "search_user_id": search_user_id,
+ "service": service,
+ },
+ audit_log_list_params.AuditLogListParams,
+ ),
+ ),
+ model=AuditLogEntry,
+ )
+
+
+class AuditLogsResourceWithRawResponse:
+ def __init__(self, audit_logs: AuditLogsResource) -> None:
+ self._audit_logs = audit_logs
+
+ self.list = to_raw_response_wrapper(
+ audit_logs.list,
+ )
+
+
+class AsyncAuditLogsResourceWithRawResponse:
+ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
+ self._audit_logs = audit_logs
+
+ self.list = async_to_raw_response_wrapper(
+ audit_logs.list,
+ )
+
+
+class AuditLogsResourceWithStreamingResponse:
+ def __init__(self, audit_logs: AuditLogsResource) -> None:
+ self._audit_logs = audit_logs
+
+ self.list = to_streamed_response_wrapper(
+ audit_logs.list,
+ )
+
+
+class AsyncAuditLogsResourceWithStreamingResponse:
+ def __init__(self, audit_logs: AsyncAuditLogsResource) -> None:
+ self._audit_logs = audit_logs
+
+ self.list = async_to_streamed_response_wrapper(
+ audit_logs.list,
+ )
diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py
index c901fdb..24a560c 100644
--- a/src/kernel/resources/auth/connections.py
+++ b/src/kernel/resources/auth/connections.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from typing import Any, Dict, cast
+from typing_extensions import Literal
import httpx
@@ -24,12 +25,14 @@
connection_create_params,
connection_submit_params,
connection_update_params,
+ connection_timeline_params,
)
from ..._base_client import AsyncPaginator, make_request_options
from ...types.auth.managed_auth import ManagedAuth
from ...types.auth.login_response import LoginResponse
from ...types.auth.submit_fields_response import SubmitFieldsResponse
from ...types.auth.connection_follow_response import ConnectionFollowResponse
+from ...types.auth.managed_auth_timeline_event import ManagedAuthTimelineEvent
__all__ = ["ConnectionsResource", "AsyncConnectionsResource"]
@@ -554,6 +557,62 @@ def submit(
cast_to=SubmitFieldsResponse,
)
+ def timeline(
+ self,
+ id: str,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ type: Literal["login", "reauth", "health_check"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[ManagedAuthTimelineEvent]:
+ """
+ Returns a chronological timeline of events for an auth connection — login
+ attempts, automatic re-auth attempts, and health checks. Events are returned
+ newest-first.
+
+ Args:
+ limit: Maximum number of events to return
+
+ offset: Number of events to skip
+
+ type: Filter the timeline to a single event type.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get_api_list(
+ path_template("/auth/connections/{id}/timeline", id=id),
+ page=SyncOffsetPagination[ManagedAuthTimelineEvent],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "type": type,
+ },
+ connection_timeline_params.ConnectionTimelineParams,
+ ),
+ ),
+ model=ManagedAuthTimelineEvent,
+ )
+
class AsyncConnectionsResource(AsyncAPIResource):
"""Create and manage auth connections for automated credential capture and login."""
@@ -1075,6 +1134,62 @@ async def submit(
cast_to=SubmitFieldsResponse,
)
+ def timeline(
+ self,
+ id: str,
+ *,
+ limit: int | Omit = omit,
+ offset: int | Omit = omit,
+ type: Literal["login", "reauth", "health_check"] | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[ManagedAuthTimelineEvent, AsyncOffsetPagination[ManagedAuthTimelineEvent]]:
+ """
+ Returns a chronological timeline of events for an auth connection — login
+ attempts, automatic re-auth attempts, and health checks. Events are returned
+ newest-first.
+
+ Args:
+ limit: Maximum number of events to return
+
+ offset: Number of events to skip
+
+ type: Filter the timeline to a single event type.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ return self._get_api_list(
+ path_template("/auth/connections/{id}/timeline", id=id),
+ page=AsyncOffsetPagination[ManagedAuthTimelineEvent],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "offset": offset,
+ "type": type,
+ },
+ connection_timeline_params.ConnectionTimelineParams,
+ ),
+ ),
+ model=ManagedAuthTimelineEvent,
+ )
+
class ConnectionsResourceWithRawResponse:
def __init__(self, connections: ConnectionsResource) -> None:
@@ -1104,6 +1219,9 @@ def __init__(self, connections: ConnectionsResource) -> None:
self.submit = to_raw_response_wrapper(
connections.submit,
)
+ self.timeline = to_raw_response_wrapper(
+ connections.timeline,
+ )
class AsyncConnectionsResourceWithRawResponse:
@@ -1134,6 +1252,9 @@ def __init__(self, connections: AsyncConnectionsResource) -> None:
self.submit = async_to_raw_response_wrapper(
connections.submit,
)
+ self.timeline = async_to_raw_response_wrapper(
+ connections.timeline,
+ )
class ConnectionsResourceWithStreamingResponse:
@@ -1164,6 +1285,9 @@ def __init__(self, connections: ConnectionsResource) -> None:
self.submit = to_streamed_response_wrapper(
connections.submit,
)
+ self.timeline = to_streamed_response_wrapper(
+ connections.timeline,
+ )
class AsyncConnectionsResourceWithStreamingResponse:
@@ -1194,3 +1318,6 @@ def __init__(self, connections: AsyncConnectionsResource) -> None:
self.submit = async_to_streamed_response_wrapper(
connections.submit,
)
+ self.timeline = async_to_streamed_response_wrapper(
+ connections.timeline,
+ )
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 660875d..357afc9 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -24,6 +24,7 @@
from .browser_pool import BrowserPool as BrowserPool
from .browser_usage import BrowserUsage as BrowserUsage
from .app_list_params import AppListParams as AppListParams
+from .audit_log_entry import AuditLogEntry as AuditLogEntry
from .created_api_key import CreatedAPIKey as CreatedAPIKey
from .browser_pool_ref import BrowserPoolRef as BrowserPoolRef
from .app_list_response import AppListResponse as AppListResponse
@@ -41,6 +42,7 @@
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
from .api_key_update_params import APIKeyUpdateParams as APIKeyUpdateParams
+from .audit_log_list_params import AuditLogListParams as AuditLogListParams
from .browser_create_params import BrowserCreateParams as BrowserCreateParams
from .browser_curl_response import BrowserCurlResponse as BrowserCurlResponse
from .browser_list_response import BrowserListResponse as BrowserListResponse
diff --git a/src/kernel/types/audit_log_entry.py b/src/kernel/types/audit_log_entry.py
new file mode 100644
index 0000000..dd99809
--- /dev/null
+++ b/src/kernel/types/audit_log_entry.py
@@ -0,0 +1,45 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["AuditLogEntry"]
+
+
+class AuditLogEntry(BaseModel):
+ auth_strategy: str
+ """Authentication strategy used for the request."""
+
+ client_ip: str
+ """Client IP address."""
+
+ domain: str
+ """Request host."""
+
+ duration_ms: int
+ """Request duration in milliseconds."""
+
+ email: str
+ """Email of the authenticated user at request time, if any."""
+
+ method: str
+ """HTTP method."""
+
+ path: str
+ """Request path."""
+
+ route: str
+ """Matched API route pattern, if available."""
+
+ status: int
+ """HTTP response status code."""
+
+ timestamp: datetime
+ """UTC time when the request was received."""
+
+ user_agent: str
+ """User agent header."""
+
+ user_id: str
+ """ID of the authenticated user, if any."""
diff --git a/src/kernel/types/audit_log_list_params.py b/src/kernel/types/audit_log_list_params.py
new file mode 100644
index 0000000..f1e18f5
--- /dev/null
+++ b/src/kernel/types/audit_log_list_params.py
@@ -0,0 +1,44 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Union
+from datetime import datetime
+from typing_extensions import Required, Annotated, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = ["AuditLogListParams"]
+
+
+class AuditLogListParams(TypedDict, total=False):
+ end: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
+ """Upper bound (exclusive) for the audit record timestamp."""
+
+ start: Required[Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]]
+ """Lower bound (inclusive) for the audit record timestamp."""
+
+ auth_strategy: str
+ """Filter by authentication strategy."""
+
+ exclude_method: str
+ """Filter out results by HTTP method."""
+
+ limit: int
+ """Maximum number of results to return."""
+
+ method: str
+ """Filter by HTTP method."""
+
+ page_token: str
+ """Opaque page token from X-Next-Page-Token for the next page of older records."""
+
+ search: str
+ """Free-text search over path, user ID, email, client IP, and status."""
+
+ search_user_id: SequenceNotStr[str]
+ """Additional user IDs to OR into free-text search."""
+
+ service: str
+ """Filter by service name."""
diff --git a/src/kernel/types/auth/__init__.py b/src/kernel/types/auth/__init__.py
index db89794..a70816f 100644
--- a/src/kernel/types/auth/__init__.py
+++ b/src/kernel/types/auth/__init__.py
@@ -11,3 +11,5 @@
from .connection_submit_params import ConnectionSubmitParams as ConnectionSubmitParams
from .connection_update_params import ConnectionUpdateParams as ConnectionUpdateParams
from .connection_follow_response import ConnectionFollowResponse as ConnectionFollowResponse
+from .connection_timeline_params import ConnectionTimelineParams as ConnectionTimelineParams
+from .managed_auth_timeline_event import ManagedAuthTimelineEvent as ManagedAuthTimelineEvent
diff --git a/src/kernel/types/auth/connection_timeline_params.py b/src/kernel/types/auth/connection_timeline_params.py
new file mode 100644
index 0000000..79c4f5d
--- /dev/null
+++ b/src/kernel/types/auth/connection_timeline_params.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["ConnectionTimelineParams"]
+
+
+class ConnectionTimelineParams(TypedDict, total=False):
+ limit: int
+ """Maximum number of events to return"""
+
+ offset: int
+ """Number of events to skip"""
+
+ type: Literal["login", "reauth", "health_check"]
+ """Filter the timeline to a single event type."""
diff --git a/src/kernel/types/auth/managed_auth_timeline_event.py b/src/kernel/types/auth/managed_auth_timeline_event.py
new file mode 100644
index 0000000..1f79a52
--- /dev/null
+++ b/src/kernel/types/auth/managed_auth_timeline_event.py
@@ -0,0 +1,77 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+
+__all__ = ["ManagedAuthTimelineEvent"]
+
+
+class ManagedAuthTimelineEvent(BaseModel):
+ """
+ A single event in an auth connection's history — a login attempt, an automatic re-auth attempt, or a health check.
+ """
+
+ id: str
+ """Identifier of the underlying login/reauth session or health check."""
+
+ status: Literal["IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED", "FAILED", "AUTHENTICATED", "NEEDS_AUTH"]
+ """Outcome of the event.
+
+ For login/reauth events this is the flow status (IN_PROGRESS, SUCCESS, EXPIRED,
+ CANCELED, FAILED). For health_check events it is the observed session state
+ (AUTHENTICATED, NEEDS_AUTH).
+ """
+
+ timestamp: datetime
+ """When the event occurred."""
+
+ type: Literal["login", "reauth", "health_check"]
+ """The kind of event.
+
+ "login" and "reauth" are authentication attempts; "health_check" is a periodic
+ session-validity check.
+ """
+
+ error_code: Optional[str] = None
+ """Machine-readable error code. Present when a login/reauth event failed."""
+
+ error_message: Optional[str] = None
+ """Human-readable error message. Present when a login/reauth event failed."""
+
+ previous_status: Optional[Literal["AUTHENTICATED", "NEEDS_AUTH"]] = None
+ """The session state observed before this event.
+
+ Present for health_check events that recorded a prior state.
+ """
+
+ replay_id: Optional[str] = None
+ """
+ Replay recording ID for the event's browser session, if session recording was
+ enabled.
+ """
+
+ step: Optional[
+ Literal[
+ "INITIALIZED",
+ "DISCOVERING",
+ "AWAITING_INPUT",
+ "AWAITING_EXTERNAL_ACTION",
+ "AWAITING_HUMAN_INTERVENTION",
+ "SUBMITTING",
+ "COMPLETED",
+ "EXPIRED",
+ ]
+ ] = None
+ """The step the flow reached. Present for login/reauth events."""
+
+ updated_at: Optional[datetime] = None
+ """When the event was last updated. Present for login/reauth events."""
+
+ website_error: Optional[str] = None
+ """Visible error message from the website (e.g., 'Incorrect password').
+
+ Present when the website displayed an error during the attempt.
+ """
diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py
index 2b9ce1c..b92cd79 100644
--- a/tests/api_resources/auth/test_connections.py
+++ b/tests/api_resources/auth/test_connections.py
@@ -14,6 +14,7 @@
ManagedAuth,
LoginResponse,
SubmitFieldsResponse,
+ ManagedAuthTimelineEvent,
)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -430,6 +431,59 @@ def test_path_params_submit(self, client: Kernel) -> None:
id="",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_timeline(self, client: Kernel) -> None:
+ connection = client.auth.connections.timeline(
+ id="id",
+ )
+ assert_matches_type(SyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_timeline_with_all_params(self, client: Kernel) -> None:
+ connection = client.auth.connections.timeline(
+ id="id",
+ limit=100,
+ offset=0,
+ type="login",
+ )
+ assert_matches_type(SyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_timeline(self, client: Kernel) -> None:
+ response = client.auth.connections.with_raw_response.timeline(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = response.parse()
+ assert_matches_type(SyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_timeline(self, client: Kernel) -> None:
+ with client.auth.connections.with_streaming_response.timeline(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ connection = response.parse()
+ assert_matches_type(SyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_timeline(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.auth.connections.with_raw_response.timeline(
+ id="",
+ )
+
class TestAsyncConnections:
parametrize = pytest.mark.parametrize(
@@ -843,3 +897,56 @@ async def test_path_params_submit(self, async_client: AsyncKernel) -> None:
await async_client.auth.connections.with_raw_response.submit(
id="",
)
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_timeline(self, async_client: AsyncKernel) -> None:
+ connection = await async_client.auth.connections.timeline(
+ id="id",
+ )
+ assert_matches_type(AsyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_timeline_with_all_params(self, async_client: AsyncKernel) -> None:
+ connection = await async_client.auth.connections.timeline(
+ id="id",
+ limit=100,
+ offset=0,
+ type="login",
+ )
+ assert_matches_type(AsyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_timeline(self, async_client: AsyncKernel) -> None:
+ response = await async_client.auth.connections.with_raw_response.timeline(
+ id="id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_timeline(self, async_client: AsyncKernel) -> None:
+ async with async_client.auth.connections.with_streaming_response.timeline(
+ id="id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ connection = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[ManagedAuthTimelineEvent], connection, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_timeline(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.auth.connections.with_raw_response.timeline(
+ id="",
+ )
diff --git a/tests/api_resources/test_audit_logs.py b/tests/api_resources/test_audit_logs.py
new file mode 100644
index 0000000..669a887
--- /dev/null
+++ b/tests/api_resources/test_audit_logs.py
@@ -0,0 +1,134 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types import AuditLogEntry
+from kernel._utils import parse_datetime
+from kernel.pagination import SyncPageTokenPagination, AsyncPageTokenPagination
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestAuditLogs:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ audit_log = client.audit_logs.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+ assert_matches_type(SyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_with_all_params(self, client: Kernel) -> None:
+ audit_log = client.audit_logs.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ auth_strategy="auth_strategy",
+ exclude_method="exclude_method",
+ limit=1,
+ method="method",
+ page_token="page_token",
+ search="search",
+ search_user_id=["string"],
+ service="service",
+ )
+ assert_matches_type(SyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.audit_logs.with_raw_response.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ audit_log = response.parse()
+ assert_matches_type(SyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.audit_logs.with_streaming_response.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ audit_log = response.parse()
+ assert_matches_type(SyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncAuditLogs:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ audit_log = await async_client.audit_logs.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+ assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None:
+ audit_log = await async_client.audit_logs.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ auth_strategy="auth_strategy",
+ exclude_method="exclude_method",
+ limit=1,
+ method="method",
+ page_token="page_token",
+ search="search",
+ search_user_id=["string"],
+ service="service",
+ )
+ assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.audit_logs.with_raw_response.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ audit_log = await response.parse()
+ assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.audit_logs.with_streaming_response.list(
+ end=parse_datetime("2026-01-02T00:00:00Z"),
+ start=parse_datetime("2026-01-01T00:00:00Z"),
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ audit_log = await response.parse()
+ assert_matches_type(AsyncPageTokenPagination[AuditLogEntry], audit_log, path=["response"])
+
+ assert cast(Any, response.is_closed) is True