diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 69d849f..4b521a4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
lint:
timeout-minutes: 10
name: lint
- runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -41,7 +41,7 @@ jobs:
permissions:
contents: read
id-token: write
- runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -78,7 +78,7 @@ jobs:
test:
timeout-minutes: 10
name: test
- runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b4e9013..6db19b9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.16.0"
+ ".": "0.17.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index ffd5937..6d52be0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 106
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/keycard/keycard-api-cdf14bee924cea2eaca3c1aaed26521cb91369d9462d22c21a0306b5786f4706.yml
-openapi_spec_hash: d8349acc1adec880977b6167a1e866bc
-config_hash: 7adc6b24545570dcc4a1bf0f714aa3e0
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/keycard/keycard-api-9033c94c3389ccc8501a1403313165fc2985801e98b166f21766cc601e33bcea.yml
+openapi_spec_hash: 85d79369d2b5eb95a49751607792b987
+config_hash: e1aa3a147cff2ff69e1f0bf2c7d0e6bb
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cefd0e4..53d78eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
# Changelog
+## 0.17.0 (2026-07-22)
+
+Full Changelog: [v0.16.0...v0.17.0](https://github.com/keycardai/keycard-python/compare/v0.16.0...v0.17.0)
+
+### Features
+
+* **applications:** allow unified-gateway and mcp-server traits (ECO-128) ([414ac68](https://github.com/keycardai/keycard-python/commit/414ac68a77bb5c54837b7d4146c2e8b8735f6370))
+* filter users by identifier in management list ([147af02](https://github.com/keycardai/keycard-python/commit/147af020e8e8c31678928137fb85386b54dd4ec6))
+* **ID-365:** support additional SSO provider configuration options ([526b229](https://github.com/keycardai/keycard-python/commit/526b22958acc91a11569ba3f8453a004eb03f6f5))
+* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([1c22f7e](https://github.com/keycardai/keycard-python/commit/1c22f7ea4ae904c4864a794dc4aaa46afa7d7e0e))
+
+
+### Bug Fixes
+
+* **ACC-613:** preserve source order of policies in draft/convert cedar_json ([6d27d21](https://github.com/keycardai/keycard-python/commit/6d27d213ac66fb9f1890409a380924c3edb44b99))
+* **ci:** resolve Stainless error diagnostics and enforce fail_on: error ([8a01934](https://github.com/keycardai/keycard-python/commit/8a01934b6ceb8636db998f0498d86ccb810cf639))
+* exact-match identifier filter on resources management list ([846f24e](https://github.com/keycardai/keycard-python/commit/846f24efdb2492e4ea0741167ebc7153af5d0d1e))
+* **internal:** resolve build failures ([15cb317](https://github.com/keycardai/keycard-python/commit/15cb317c1d1ea8462b30bff1411e52a554df0fb0))
+
+
+### Chores
+
+* de-dup and align types across API specs ([bf16103](https://github.com/keycardai/keycard-python/commit/bf161039858f079eabd6abf9102fa439633b9bc9))
+* Fixes found during Terraform work ([8389f30](https://github.com/keycardai/keycard-python/commit/8389f304ffae7c4bbe512de541a6896469be273f))
+
## 0.16.0 (2026-06-30)
Full Changelog: [v0.15.0...v0.16.0](https://github.com/keycardai/keycard-python/compare/v0.15.0...v0.16.0)
diff --git a/README.md b/README.md
index cf222f1..e364722 100644
--- a/README.md
+++ b/README.md
@@ -25,23 +25,34 @@ pip install keycardai_api
The full API of this library can be found in [api.md](api.md).
```python
+import os
from keycardai_api import KeycardAPI
-client = KeycardAPI()
+client = KeycardAPI(
+ api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted
+)
zones = client.zones.list()
print(zones.items)
```
+While you can provide an `api_key` keyword argument,
+we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/)
+to add `KEYCARD_API_API_KEY="My API Key"` to your `.env` file
+so that your API Key is not stored in source control.
+
## Async usage
Simply import `AsyncKeycardAPI` instead of `KeycardAPI` and use `await` with each API call:
```python
+import os
import asyncio
from keycardai_api import AsyncKeycardAPI
-client = AsyncKeycardAPI()
+client = AsyncKeycardAPI(
+ api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted
+)
async def main() -> None:
@@ -68,6 +79,7 @@ pip install keycardai_api[aiohttp]
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
```python
+import os
import asyncio
from keycardai_api import DefaultAioHttpClient
from keycardai_api import AsyncKeycardAPI
@@ -75,6 +87,7 @@ from keycardai_api import AsyncKeycardAPI
async def main() -> None:
async with AsyncKeycardAPI(
+ api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
zones = await client.zones.list()
diff --git a/api.md b/api.md
index 48d50a6..de50220 100644
--- a/api.md
+++ b/api.md
@@ -261,46 +261,6 @@ Methods:
- client.zones.policies.versions.list(policy_id, \*, zone_id, \*\*params) -> VersionListResponse
- client.zones.policies.versions.archive(version_id, \*, zone_id, policy_id) -> PolicyVersion
-## Packages
-
-Types:
-
-```python
-from keycardai_api.types.zones import (
- InputState,
- Package,
- PackageDraft,
- PackageInputBinding,
- PackageList,
- PackageOutputBinding,
- PackageSource,
-)
-```
-
-### Versions
-
-Types:
-
-```python
-from keycardai_api.types.zones.packages import PackageVersion, PackageVersionList
-```
-
-## Installs
-
-Types:
-
-```python
-from keycardai_api.types.zones import Install, InstallList, InstallStatus
-```
-
-## CatalogTasks
-
-Types:
-
-```python
-from keycardai_api.types.zones import Task, TaskOperation, TaskStatus
-```
-
## PolicySets
Types:
diff --git a/pyproject.toml b/pyproject.toml
index 0f72996..a2de0a3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "keycardai_api"
-version = "0.16.0"
+version = "0.17.0"
description = "The official Python library for the keycard-api API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/scripts/lint b/scripts/lint
index 97303ed..7b9389c 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -13,7 +13,7 @@ else
fi
echo "==> Running pyright"
-uv run pyright
+uv run pyright -p .
echo "==> Running mypy"
uv run mypy .
diff --git a/src/keycardai_api/_client.py b/src/keycardai_api/_client.py
index 103db47..4aa915e 100644
--- a/src/keycardai_api/_client.py
+++ b/src/keycardai_api/_client.py
@@ -160,12 +160,27 @@ def with_streaming_response(self) -> KeycardAPIWithStreamedResponse:
def qs(self) -> Querystring:
return Querystring(array_format="repeat")
+ @override
+ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]:
+ headers: dict[str, str] = {}
+ if security.get("bearer_auth", False):
+ for key, value in self._bearer_auth.items():
+ headers.setdefault(key, value)
+ return headers
+
@override
def _custom_auth(self, security: SecurityOptions) -> httpx.Auth | None:
if security.get("o_auth2", False) and self._o_auth2 is not None:
return self._o_auth2
return None
+ @property
+ def _bearer_auth(self) -> dict[str, str]:
+ api_key = self.api_key
+ if api_key is None:
+ return {}
+ return {"Authorization": f"Bearer {api_key}"}
+
@property
def _o_auth2(self) -> httpx.Auth | None:
if self.client_id and self.client_secret:
@@ -390,12 +405,27 @@ def with_streaming_response(self) -> AsyncKeycardAPIWithStreamedResponse:
def qs(self) -> Querystring:
return Querystring(array_format="repeat")
+ @override
+ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]:
+ headers: dict[str, str] = {}
+ if security.get("bearer_auth", False):
+ for key, value in self._bearer_auth.items():
+ headers.setdefault(key, value)
+ return headers
+
@override
def _custom_auth(self, security: SecurityOptions) -> httpx.Auth | None:
if security.get("o_auth2", False) and self._o_auth2 is not None:
return self._o_auth2
return None
+ @property
+ def _bearer_auth(self) -> dict[str, str]:
+ api_key = self.api_key
+ if api_key is None:
+ return {}
+ return {"Authorization": f"Bearer {api_key}"}
+
@property
def _o_auth2(self) -> httpx.Auth | None:
if self.client_id and self.client_secret:
diff --git a/src/keycardai_api/_models.py b/src/keycardai_api/_models.py
index 13d0794..90bf844 100644
--- a/src/keycardai_api/_models.py
+++ b/src/keycardai_api/_models.py
@@ -872,6 +872,7 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]:
class SecurityOptions(TypedDict, total=False):
+ bearer_auth: bool
o_auth2: bool
@@ -903,7 +904,10 @@ class FinalRequestOptions(pydantic.BaseModel):
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
follow_redirects: Union[bool, None] = None
- security: SecurityOptions = {"o_auth2": True}
+ security: SecurityOptions = {
+ "bearer_auth": True,
+ "o_auth2": True,
+ }
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
# It should be noted that we cannot use `json` here as that would override
diff --git a/src/keycardai_api/_version.py b/src/keycardai_api/_version.py
index ff81ceb..cb30031 100644
--- a/src/keycardai_api/_version.py
+++ b/src/keycardai_api/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "keycardai_api"
-__version__ = "0.16.0" # x-release-please-version
+__version__ = "0.17.0" # x-release-please-version
diff --git a/src/keycardai_api/resources/organizations/sso_connection.py b/src/keycardai_api/resources/organizations/sso_connection.py
index 4a8a44f..002dbed 100644
--- a/src/keycardai_api/resources/organizations/sso_connection.py
+++ b/src/keycardai_api/resources/organizations/sso_connection.py
@@ -104,7 +104,7 @@ def update(
client_id: str | Omit = omit,
client_secret: str | Omit = omit,
identifier: str | Omit = omit,
- protocols: Optional[SSOConnectionProtocolParam] | Omit = omit,
+ protocols: Optional[sso_connection_update_params.Protocols] | Omit = omit,
x_client_request_id: 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.
@@ -125,7 +125,8 @@ def update(
identifier: SSO provider identifier (e.g., issuer URL)
- protocols: Protocol configuration for SSO connection
+ protocols: Protocol configuration for an SSO connection update. Omit a protocol to leave it
+ unchanged.
extra_headers: Send extra headers
@@ -329,7 +330,7 @@ async def update(
client_id: str | Omit = omit,
client_secret: str | Omit = omit,
identifier: str | Omit = omit,
- protocols: Optional[SSOConnectionProtocolParam] | Omit = omit,
+ protocols: Optional[sso_connection_update_params.Protocols] | Omit = omit,
x_client_request_id: 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.
@@ -350,7 +351,8 @@ async def update(
identifier: SSO provider identifier (e.g., issuer URL)
- protocols: Protocol configuration for SSO connection
+ protocols: Protocol configuration for an SSO connection update. Omit a protocol to leave it
+ unchanged.
extra_headers: Send extra headers
diff --git a/src/keycardai_api/resources/zones/policy_schemas.py b/src/keycardai_api/resources/zones/policy_schemas.py
index eb5b9fd..a5a4f5b 100644
--- a/src/keycardai_api/resources/zones/policy_schemas.py
+++ b/src/keycardai_api/resources/zones/policy_schemas.py
@@ -36,7 +36,7 @@ class PolicySchemasResource(SyncAPIResource):
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -284,7 +284,7 @@ class AsyncPolicySchemasResource(AsyncAPIResource):
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
diff --git a/src/keycardai_api/resources/zones/resources.py b/src/keycardai_api/resources/zones/resources.py
index 2941cfe..5b19dad 100644
--- a/src/keycardai_api/resources/zones/resources.py
+++ b/src/keycardai_api/resources/zones/resources.py
@@ -271,6 +271,7 @@ def list(
before: str | Omit = omit,
credential_provider_id: str | Omit = omit,
expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit,
+ filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit,
identifier: str | Omit = omit,
limit: int | Omit = omit,
slug: str | Omit = omit,
@@ -281,8 +282,12 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ResourceListResponse:
- """
- Returns a list of resources in the specified zone
+ """Returns a list of resources in the specified zone.
+
+ Filter by exact identifier
+ via `filter[identifier]` (repeatable, OR'd). Matching is exact: identifiers are
+ unique per zone, so a filter returns at most one resource per value and never
+ performs URL prefix resolution.
Args:
after: Cursor for forward pagination
@@ -291,6 +296,8 @@ def list(
credential_provider_id: Filter resources by credential provider ID
+ filter_identifier: Filter by exact resource identifier
+
identifier: Filter resources by identifier
limit: Maximum number of items to return
@@ -318,6 +325,7 @@ def list(
"before": before,
"credential_provider_id": credential_provider_id,
"expand": expand,
+ "filter_identifier": filter_identifier,
"identifier": identifier,
"limit": limit,
"slug": slug,
@@ -606,6 +614,7 @@ async def list(
before: str | Omit = omit,
credential_provider_id: str | Omit = omit,
expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit,
+ filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit,
identifier: str | Omit = omit,
limit: int | Omit = omit,
slug: str | Omit = omit,
@@ -616,8 +625,12 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ResourceListResponse:
- """
- Returns a list of resources in the specified zone
+ """Returns a list of resources in the specified zone.
+
+ Filter by exact identifier
+ via `filter[identifier]` (repeatable, OR'd). Matching is exact: identifiers are
+ unique per zone, so a filter returns at most one resource per value and never
+ performs URL prefix resolution.
Args:
after: Cursor for forward pagination
@@ -626,6 +639,8 @@ async def list(
credential_provider_id: Filter resources by credential provider ID
+ filter_identifier: Filter by exact resource identifier
+
identifier: Filter resources by identifier
limit: Maximum number of items to return
@@ -653,6 +668,7 @@ async def list(
"before": before,
"credential_provider_id": credential_provider_id,
"expand": expand,
+ "filter_identifier": filter_identifier,
"identifier": identifier,
"limit": limit,
"slug": slug,
diff --git a/src/keycardai_api/resources/zones/users.py b/src/keycardai_api/resources/zones/users.py
index 81cd5a1..52b71d8 100644
--- a/src/keycardai_api/resources/zones/users.py
+++ b/src/keycardai_api/resources/zones/users.py
@@ -88,12 +88,24 @@ def list(
after: str | Omit = omit,
before: str | Omit = omit,
expand: Union[
- Literal["total_count", "session_count", "grant_count", "role-assignments"],
- List[Literal["total_count", "session_count", "grant_count", "role-assignments"]],
+ Literal[
+ "total_count", "session_count", "grant_count", "role-assignments", "credentials", "credentials.provider"
+ ],
+ List[
+ Literal[
+ "total_count",
+ "session_count",
+ "grant_count",
+ "role-assignments",
+ "credentials",
+ "credentials.provider",
+ ]
+ ],
]
| Omit = omit,
filter_email: Union[str, SequenceNotStr[str]] | Omit = omit,
filter_id: Union[str, SequenceNotStr[str]] | Omit = omit,
+ filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit,
limit: int | Omit = omit,
query: Union[str, SequenceNotStr[str]] | Omit = omit,
query_email: Union[str, SequenceNotStr[str]] | Omit = omit,
@@ -109,27 +121,29 @@ def list(
"""
Returns a list of users in the specified zone.
- **Rollout note:** the paginated/searchable/sortable behavior described below is
- gated behind the `user-pagination` feature flag and is currently disabled for
- most zones. While the flag is off, the response returns every user in the zone
- (capped at 100) in `items` and a fixed pagination envelope where `after_cursor`
- and `before_cursor` are `null` and `total_count` is `0`. The query parameters
- below are accepted but ignored. The flag is rolled out per-zone in Datadog and
- will become the default once Console adopts the paginated contract.
+ Note: cursor pagination, search, and sort are not yet enabled for all zones.
+ Where they are not enabled, the response returns all users in the zone (capped
+ at 100) in `items`, with `after_cursor` and `before_cursor` set to `null` and
+ `total_count` of `0`; `filter[email]` and `filter[identifier]` are still
+ applied, while the pagination, search, and sort parameters below are accepted
+ but ignored.
Use cursor pagination via `after`/`before`. Sort: comma-separated field list;
prefix with `-` for descending. Use `expand[]=total_count` to include the
matching row count, `expand[]=session_count` to include per-user session counts,
- `expand[]=grant_count` to include per-user delegated-grant counts, and
- `expand[]=role-assignments` to include each user's structured role grants.
- Filter by exact email via `filter[email]`; search via `query[email]` /
- `query[subject]` / `query[]` (substring match, OR'd across repeated values).
- `query[]` matches against email and federation credential subject. Pass
- `filter[id]` (repeatable, max 100) to restrict results to a known set of users —
- mutually exclusive with `after`/`before` (returns 400 if combined). When
- `filter[id]` is set, `limit` is ignored and the response contains every
- requested user that exists in the zone, in a single page. IDs not in the zone
- are silently omitted.
+ `expand[]=grant_count` to include per-user delegated-grant counts,
+ `expand[]=role-assignments` to include each user's structured role grants,
+ `expand[]=credentials` to include each user's authentication credentials (each
+ with its `provider_id`), and `expand[]=credentials.provider` to additionally
+ inline the full identity provider on each federation credential. Filter by exact
+ email via `filter[email]` and by exact identifier via `filter[identifier]`;
+ search via `query[email]` / `query[subject]` / `query[]` (substring match, OR'd
+ across repeated values). `query[]` matches against email and federation
+ credential subject. Pass `filter[id]` (repeatable, max 100) to restrict results
+ to a known set of users — mutually exclusive with `after`/`before` (returns 400
+ if combined). When `filter[id]` is set, `limit` is ignored and the response
+ contains every requested user that exists in the zone, in a single page. IDs not
+ in the zone are silently omitted.
Args:
after: Cursor for forward pagination
@@ -141,6 +155,8 @@ def list(
filter_id: Restrict results to users with this publicId. Repeatable, max 100. Mutually
exclusive with after/before.
+ filter_identifier: Filter by exact user identifier
+
limit: Maximum number of items to return
query: Search across email and credential subject (substring match)
@@ -176,6 +192,7 @@ def list(
"expand": expand,
"filter_email": filter_email,
"filter_id": filter_id,
+ "filter_identifier": filter_identifier,
"limit": limit,
"query": query,
"query_email": query_email,
@@ -252,12 +269,24 @@ async def list(
after: str | Omit = omit,
before: str | Omit = omit,
expand: Union[
- Literal["total_count", "session_count", "grant_count", "role-assignments"],
- List[Literal["total_count", "session_count", "grant_count", "role-assignments"]],
+ Literal[
+ "total_count", "session_count", "grant_count", "role-assignments", "credentials", "credentials.provider"
+ ],
+ List[
+ Literal[
+ "total_count",
+ "session_count",
+ "grant_count",
+ "role-assignments",
+ "credentials",
+ "credentials.provider",
+ ]
+ ],
]
| Omit = omit,
filter_email: Union[str, SequenceNotStr[str]] | Omit = omit,
filter_id: Union[str, SequenceNotStr[str]] | Omit = omit,
+ filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit,
limit: int | Omit = omit,
query: Union[str, SequenceNotStr[str]] | Omit = omit,
query_email: Union[str, SequenceNotStr[str]] | Omit = omit,
@@ -273,27 +302,29 @@ async def list(
"""
Returns a list of users in the specified zone.
- **Rollout note:** the paginated/searchable/sortable behavior described below is
- gated behind the `user-pagination` feature flag and is currently disabled for
- most zones. While the flag is off, the response returns every user in the zone
- (capped at 100) in `items` and a fixed pagination envelope where `after_cursor`
- and `before_cursor` are `null` and `total_count` is `0`. The query parameters
- below are accepted but ignored. The flag is rolled out per-zone in Datadog and
- will become the default once Console adopts the paginated contract.
+ Note: cursor pagination, search, and sort are not yet enabled for all zones.
+ Where they are not enabled, the response returns all users in the zone (capped
+ at 100) in `items`, with `after_cursor` and `before_cursor` set to `null` and
+ `total_count` of `0`; `filter[email]` and `filter[identifier]` are still
+ applied, while the pagination, search, and sort parameters below are accepted
+ but ignored.
Use cursor pagination via `after`/`before`. Sort: comma-separated field list;
prefix with `-` for descending. Use `expand[]=total_count` to include the
matching row count, `expand[]=session_count` to include per-user session counts,
- `expand[]=grant_count` to include per-user delegated-grant counts, and
- `expand[]=role-assignments` to include each user's structured role grants.
- Filter by exact email via `filter[email]`; search via `query[email]` /
- `query[subject]` / `query[]` (substring match, OR'd across repeated values).
- `query[]` matches against email and federation credential subject. Pass
- `filter[id]` (repeatable, max 100) to restrict results to a known set of users —
- mutually exclusive with `after`/`before` (returns 400 if combined). When
- `filter[id]` is set, `limit` is ignored and the response contains every
- requested user that exists in the zone, in a single page. IDs not in the zone
- are silently omitted.
+ `expand[]=grant_count` to include per-user delegated-grant counts,
+ `expand[]=role-assignments` to include each user's structured role grants,
+ `expand[]=credentials` to include each user's authentication credentials (each
+ with its `provider_id`), and `expand[]=credentials.provider` to additionally
+ inline the full identity provider on each federation credential. Filter by exact
+ email via `filter[email]` and by exact identifier via `filter[identifier]`;
+ search via `query[email]` / `query[subject]` / `query[]` (substring match, OR'd
+ across repeated values). `query[]` matches against email and federation
+ credential subject. Pass `filter[id]` (repeatable, max 100) to restrict results
+ to a known set of users — mutually exclusive with `after`/`before` (returns 400
+ if combined). When `filter[id]` is set, `limit` is ignored and the response
+ contains every requested user that exists in the zone, in a single page. IDs not
+ in the zone are silently omitted.
Args:
after: Cursor for forward pagination
@@ -305,6 +336,8 @@ async def list(
filter_id: Restrict results to users with this publicId. Repeatable, max 100. Mutually
exclusive with after/before.
+ filter_identifier: Filter by exact user identifier
+
limit: Maximum number of items to return
query: Search across email and credential subject (substring match)
@@ -340,6 +373,7 @@ async def list(
"expand": expand,
"filter_email": filter_email,
"filter_id": filter_id,
+ "filter_identifier": filter_identifier,
"limit": limit,
"query": query,
"query_email": query_email,
diff --git a/src/keycardai_api/resources/zones/zones.py b/src/keycardai_api/resources/zones/zones.py
index fcbdb8a..aef2478 100644
--- a/src/keycardai_api/resources/zones/zones.py
+++ b/src/keycardai_api/resources/zones/zones.py
@@ -188,7 +188,7 @@ def policy_schemas(self) -> PolicySchemasResource:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -556,7 +556,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResource:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -943,7 +943,7 @@ def policy_schemas(self) -> PolicySchemasResourceWithRawResponse:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -1034,7 +1034,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResourceWithRawResponse:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -1125,7 +1125,7 @@ def policy_schemas(self) -> PolicySchemasResourceWithStreamingResponse:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -1216,7 +1216,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResourceWithStreamingResponse:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
diff --git a/src/keycardai_api/types/organizations/sso_connection_protocol.py b/src/keycardai_api/types/organizations/sso_connection_protocol.py
index 5767f80..42ae9b7 100644
--- a/src/keycardai_api/types/organizations/sso_connection_protocol.py
+++ b/src/keycardai_api/types/organizations/sso_connection_protocol.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import Dict, List, Optional
from ..._models import BaseModel
@@ -13,6 +13,13 @@ class Oauth2(BaseModel):
authorization_endpoint: Optional[str] = None
"""OAuth 2.0 authorization endpoint"""
+ authorization_parameters: Optional[Dict[str, str]] = None
+ """Custom query parameters appended to authorization redirect URLs.
+
+ Use for non-standard providers (e.g. Google prompt=consent,
+ access_type=offline).
+ """
+
code_challenge_methods_supported: Optional[List[str]] = None
"""Supported PKCE code challenge methods"""
@@ -32,6 +39,18 @@ class Oauth2(BaseModel):
class Openid(BaseModel):
"""OpenID Connect protocol configuration for SSO connection"""
+ scopes: Optional[List[str]] = None
+ """Additional OIDC scopes to request from this provider during authentication (e.g.
+
+ "groups"). Merged with the default scopes (openid, profile, email).
+ """
+
+ user_identifier_claim: Optional[str] = None
+ """
+ Name of a top-level string claim in the provider's ID Token to use as the user
+ identifier on user creation. When not set, the user's Keycard ID is used.
+ """
+
userinfo_endpoint: Optional[str] = None
"""OpenID Connect UserInfo endpoint"""
diff --git a/src/keycardai_api/types/organizations/sso_connection_protocol_param.py b/src/keycardai_api/types/organizations/sso_connection_protocol_param.py
index 84d6074..2183279 100644
--- a/src/keycardai_api/types/organizations/sso_connection_protocol_param.py
+++ b/src/keycardai_api/types/organizations/sso_connection_protocol_param.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Optional
+from typing import Dict, Optional
from typing_extensions import TypedDict
from ..._types import SequenceNotStr
@@ -16,6 +16,13 @@ class Oauth2(TypedDict, total=False):
authorization_endpoint: Optional[str]
"""OAuth 2.0 authorization endpoint"""
+ authorization_parameters: Optional[Dict[str, str]]
+ """Custom query parameters appended to authorization redirect URLs.
+
+ Use for non-standard providers (e.g. Google prompt=consent,
+ access_type=offline).
+ """
+
code_challenge_methods_supported: Optional[SequenceNotStr[str]]
"""Supported PKCE code challenge methods"""
@@ -35,6 +42,18 @@ class Oauth2(TypedDict, total=False):
class Openid(TypedDict, total=False):
"""OpenID Connect protocol configuration for SSO connection"""
+ scopes: Optional[SequenceNotStr[str]]
+ """Additional OIDC scopes to request from this provider during authentication (e.g.
+
+ "groups"). Merged with the default scopes (openid, profile, email).
+ """
+
+ user_identifier_claim: Optional[str]
+ """
+ Name of a top-level string claim in the provider's ID Token to use as the user
+ identifier on user creation. When not set, the user's Keycard ID is used.
+ """
+
userinfo_endpoint: Optional[str]
"""OpenID Connect UserInfo endpoint"""
diff --git a/src/keycardai_api/types/organizations/sso_connection_update_params.py b/src/keycardai_api/types/organizations/sso_connection_update_params.py
index e8d852e..93c8454 100644
--- a/src/keycardai_api/types/organizations/sso_connection_update_params.py
+++ b/src/keycardai_api/types/organizations/sso_connection_update_params.py
@@ -2,13 +2,13 @@
from __future__ import annotations
-from typing import Optional
+from typing import Dict, Optional
from typing_extensions import Annotated, TypedDict
+from ..._types import SequenceNotStr
from ..._utils import PropertyInfo
-from .sso_connection_protocol_param import SSOConnectionProtocolParam
-__all__ = ["SSOConnectionUpdateParams"]
+__all__ = ["SSOConnectionUpdateParams", "Protocols", "ProtocolsOauth2", "ProtocolsOpenid"]
class SSOConnectionUpdateParams(TypedDict, total=False):
@@ -21,7 +21,86 @@ class SSOConnectionUpdateParams(TypedDict, total=False):
identifier: str
"""SSO provider identifier (e.g., issuer URL)"""
- protocols: Optional[SSOConnectionProtocolParam]
- """Protocol configuration for SSO connection"""
+ protocols: Optional[Protocols]
+ """Protocol configuration for an SSO connection update.
+
+ Omit a protocol to leave it unchanged.
+ """
x_client_request_id: Annotated[str, PropertyInfo(alias="X-Client-Request-ID")]
+
+
+class ProtocolsOauth2(TypedDict, total=False):
+ """OAuth 2.0 protocol configuration for an SSO connection update.
+
+ Each field is tri-state, omit to leave unchanged, send null to clear, send a value to set.
+ """
+
+ authorization_endpoint: Optional[str]
+ """OAuth 2.0 authorization endpoint. Set to null to clear."""
+
+ authorization_parameters: Optional[Dict[str, str]]
+ """Custom query parameters appended to authorization redirect URLs.
+
+ Use for non-standard providers (e.g. Google prompt=consent,
+ access_type=offline). Set to null to clear.
+ """
+
+ code_challenge_methods_supported: Optional[SequenceNotStr[str]]
+ """Supported PKCE code challenge methods. Set to null to clear."""
+
+ jwks_uri: Optional[str]
+ """JSON Web Key Set endpoint. Set to null to clear."""
+
+ registration_endpoint: Optional[str]
+ """OAuth 2.0 registration endpoint. Set to null to clear."""
+
+ scopes_supported: Optional[SequenceNotStr[str]]
+ """Supported OAuth 2.0 scopes. Set to null to clear."""
+
+ token_endpoint: Optional[str]
+ """OAuth 2.0 token endpoint. Set to null to clear."""
+
+
+class ProtocolsOpenid(TypedDict, total=False):
+ """OpenID Connect protocol configuration for an SSO connection update.
+
+ Each field is tri-state, omit to leave unchanged, send null to clear, send a value to set.
+ """
+
+ scopes: Optional[SequenceNotStr[str]]
+ """Additional OIDC scopes to request from this provider during authentication (e.g.
+
+ "groups"). Merged with the default scopes (openid, profile, email). Set to null
+ to clear.
+ """
+
+ user_identifier_claim: Optional[str]
+ """
+ Name of a top-level string claim in the provider's ID Token to use as the user
+ identifier on user creation. Set to null to clear.
+ """
+
+ userinfo_endpoint: Optional[str]
+ """OpenID Connect UserInfo endpoint. Set to null to clear."""
+
+
+class Protocols(TypedDict, total=False):
+ """Protocol configuration for an SSO connection update.
+
+ Omit a protocol to leave it unchanged.
+ """
+
+ oauth2: Optional[ProtocolsOauth2]
+ """OAuth 2.0 protocol configuration for an SSO connection update.
+
+ Each field is tri-state, omit to leave unchanged, send null to clear, send a
+ value to set.
+ """
+
+ openid: Optional[ProtocolsOpenid]
+ """OpenID Connect protocol configuration for an SSO connection update.
+
+ Each field is tri-state, omit to leave unchanged, send null to clear, send a
+ value to set.
+ """
diff --git a/src/keycardai_api/types/zones/__init__.py b/src/keycardai_api/types/zones/__init__.py
index 03942fa..283dad0 100644
--- a/src/keycardai_api/types/zones/__init__.py
+++ b/src/keycardai_api/types/zones/__init__.py
@@ -20,12 +20,9 @@
from .user_agent import UserAgent as UserAgent
from .application import Application as Application
from .base_fields import BaseFields as BaseFields
-from .task_status import TaskStatus as TaskStatus
from .zone_member import ZoneMember as ZoneMember
-from .install_status import InstallStatus as InstallStatus
from .metadata_param import MetadataParam as MetadataParam
from .schema_version import SchemaVersion as SchemaVersion
-from .task_operation import TaskOperation as TaskOperation
from .user_list_params import UserListParams as UserListParams
from .application_trait import ApplicationTrait as ApplicationTrait
from .member_add_params import MemberAddParams as MemberAddParams
diff --git a/src/keycardai_api/types/zones/application_trait.py b/src/keycardai_api/types/zones/application_trait.py
index 293bb0f..c266be7 100644
--- a/src/keycardai_api/types/zones/application_trait.py
+++ b/src/keycardai_api/types/zones/application_trait.py
@@ -4,4 +4,4 @@
__all__ = ["ApplicationTrait"]
-ApplicationTrait: TypeAlias = Literal["gateway", "mcp-provider"]
+ApplicationTrait: TypeAlias = Literal["gateway", "mcp-provider", "unified-gateway", "mcp-server"]
diff --git a/src/keycardai_api/types/zones/install_status.py b/src/keycardai_api/types/zones/install_status.py
deleted file mode 100644
index 835b898..0000000
--- a/src/keycardai_api/types/zones/install_status.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal, TypeAlias
-
-__all__ = ["InstallStatus"]
-
-InstallStatus: TypeAlias = Literal["pending", "active", "deleting", "failed", "deleted"]
diff --git a/src/keycardai_api/types/zones/packages/__init__.py b/src/keycardai_api/types/zones/packages/__init__.py
deleted file mode 100644
index f8ee8b1..0000000
--- a/src/keycardai_api/types/zones/packages/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
diff --git a/src/keycardai_api/types/zones/policies/policy_version.py b/src/keycardai_api/types/zones/policies/policy_version.py
index f9982be..d8683af 100644
--- a/src/keycardai_api/types/zones/policies/policy_version.py
+++ b/src/keycardai_api/types/zones/policies/policy_version.py
@@ -43,7 +43,9 @@ class PolicyVersion(BaseModel):
"""Cedar policy in JSON representation.
Populated by default and when `format=json` is passed; null when `format=cedar`
- narrows the response to the text representation only.
+ narrows the response to the text representation only. Serialized verbatim from
+ the stored Cedar so the order of `staticPolicies` matches the source policy
+ order (ACC-613).
"""
cedar_raw: Optional[str] = None
diff --git a/src/keycardai_api/types/zones/policy_sets/policy_set_version.py b/src/keycardai_api/types/zones/policy_sets/policy_set_version.py
index 5b89efc..996c5e2 100644
--- a/src/keycardai_api/types/zones/policy_sets/policy_set_version.py
+++ b/src/keycardai_api/types/zones/policy_sets/policy_set_version.py
@@ -41,11 +41,23 @@ class PolicySetVersion(BaseModel):
version: int
active: Optional[bool] = None
- """Whether this policy set version is currently bound with mode='active'"""
+ """Whether this policy set version is currently bound with mode='active'.
+
+ Always populated in responses; clients must treat absence as unknown rather than
+ inferring 'not bound'.
+ """
archived_at: Optional[datetime] = None
+ """Timestamp when the version was archived.
+
+ Non-null only for archived versions; null or absent means not archived.
+ """
archived_by: Optional[str] = None
+ """Identifier of the actor that archived the version.
+
+ Null or absent means not archived.
+ """
attestation: Optional[AttestationStatement] = None
"""Decoded content of an Attestation JWS payload.
diff --git a/src/keycardai_api/types/zones/resource_list_params.py b/src/keycardai_api/types/zones/resource_list_params.py
index 05e841a..04dacee 100644
--- a/src/keycardai_api/types/zones/resource_list_params.py
+++ b/src/keycardai_api/types/zones/resource_list_params.py
@@ -5,6 +5,7 @@
from typing import List, Union
from typing_extensions import Literal, Annotated, TypedDict
+from ..._types import SequenceNotStr
from ..._utils import PropertyInfo
__all__ = ["ResourceListParams"]
@@ -22,6 +23,9 @@ class ResourceListParams(TypedDict, total=False):
expand: Annotated[Union[Literal["total_count"], List[Literal["total_count"]]], PropertyInfo(alias="expand[]")]
+ filter_identifier: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="filter[identifier]")]
+ """Filter by exact resource identifier"""
+
identifier: str
"""Filter resources by identifier"""
diff --git a/src/keycardai_api/types/zones/task_operation.py b/src/keycardai_api/types/zones/task_operation.py
deleted file mode 100644
index d7278a8..0000000
--- a/src/keycardai_api/types/zones/task_operation.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal, TypeAlias
-
-__all__ = ["TaskOperation"]
-
-TaskOperation: TypeAlias = Literal["create", "delete"]
diff --git a/src/keycardai_api/types/zones/task_status.py b/src/keycardai_api/types/zones/task_status.py
deleted file mode 100644
index de09b18..0000000
--- a/src/keycardai_api/types/zones/task_status.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal, TypeAlias
-
-__all__ = ["TaskStatus"]
-
-TaskStatus: TypeAlias = Literal["pending", "running", "completed", "failed"]
diff --git a/src/keycardai_api/types/zones/user.py b/src/keycardai_api/types/zones/user.py
index a9f39ac..f99d086 100644
--- a/src/keycardai_api/types/zones/user.py
+++ b/src/keycardai_api/types/zones/user.py
@@ -1,12 +1,68 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import List, Union, Optional
from datetime import datetime
-from typing_extensions import Literal
+from typing_extensions import Literal, TypeAlias
+from .provider import Provider
from ..._models import BaseModel
-__all__ = ["User", "RoleAssignment", "RoleAssignmentScope"]
+__all__ = [
+ "User",
+ "Credential",
+ "CredentialIamUserCredentialFederation",
+ "CredentialIamUserCredentialPassword",
+ "RoleAssignment",
+ "RoleAssignmentScope",
+]
+
+
+class CredentialIamUserCredentialFederation(BaseModel):
+ """Federation credential: the user authenticates through an identity provider."""
+
+ created_at: datetime
+ """Entity creation timestamp"""
+
+ provider_id: Optional[str] = None
+ """ID of the identity provider backing this credential.
+
+ `null` when the source provider has been deleted.
+ """
+
+ type: Literal["federation"]
+
+ updated_at: datetime
+ """Entity update timestamp"""
+
+ issuer: Optional[str] = None
+ """Issuer identifier of the identity provider."""
+
+ provider: Optional[Provider] = None
+ """
+ A Provider is a system that supplies access to Resources and allows actors
+ (Users or Applications) to authenticate.
+ """
+
+ subject: Optional[str] = None
+ """Subject identifier from the identity provider."""
+
+
+class CredentialIamUserCredentialPassword(BaseModel):
+ """Password credential: the user authenticates with email and password.
+
+ The email lives on the user.
+ """
+
+ created_at: datetime
+ """Entity creation timestamp"""
+
+ type: Literal["password"]
+
+ updated_at: datetime
+ """Entity update timestamp"""
+
+
+Credential: TypeAlias = Union[CredentialIamUserCredentialFederation, CredentialIamUserCredentialPassword]
class RoleAssignmentScope(BaseModel):
@@ -77,6 +133,13 @@ class User(BaseModel):
authenticated_at: Optional[str] = None
"""Date when the user was last authenticated"""
+ credentials: Optional[List[Credential]] = None
+ """
+ Authentication credentials for this user, each carrying its identity provider
+ for federation credentials. Populated only when `expand[]=credentials` is set on
+ the listing endpoint.
+ """
+
grant_count: Optional[int] = None
"""Delegated-grant count for this user.
diff --git a/src/keycardai_api/types/zones/user_list_params.py b/src/keycardai_api/types/zones/user_list_params.py
index dfcc914..99dbba6 100644
--- a/src/keycardai_api/types/zones/user_list_params.py
+++ b/src/keycardai_api/types/zones/user_list_params.py
@@ -20,8 +20,19 @@ class UserListParams(TypedDict, total=False):
expand: Annotated[
Union[
- Literal["total_count", "session_count", "grant_count", "role-assignments"],
- List[Literal["total_count", "session_count", "grant_count", "role-assignments"]],
+ Literal[
+ "total_count", "session_count", "grant_count", "role-assignments", "credentials", "credentials.provider"
+ ],
+ List[
+ Literal[
+ "total_count",
+ "session_count",
+ "grant_count",
+ "role-assignments",
+ "credentials",
+ "credentials.provider",
+ ]
+ ],
],
PropertyInfo(alias="expand[]"),
]
@@ -35,6 +46,9 @@ class UserListParams(TypedDict, total=False):
Repeatable, max 100. Mutually exclusive with after/before.
"""
+ filter_identifier: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="filter[identifier]")]
+ """Filter by exact user identifier"""
+
limit: int
"""Maximum number of items to return"""
diff --git a/tests/api_resources/organizations/test_sso_connection.py b/tests/api_resources/organizations/test_sso_connection.py
index 8be8143..dee8785 100644
--- a/tests/api_resources/organizations/test_sso_connection.py
+++ b/tests/api_resources/organizations/test_sso_connection.py
@@ -90,13 +90,18 @@ def test_method_update_with_all_params(self, client: KeycardAPI) -> None:
protocols={
"oauth2": {
"authorization_endpoint": "https://example.com",
+ "authorization_parameters": {"foo": "string"},
"code_challenge_methods_supported": ["string"],
"jwks_uri": "https://example.com",
"registration_endpoint": "https://example.com",
"scopes_supported": ["string"],
"token_endpoint": "https://example.com",
},
- "openid": {"userinfo_endpoint": "https://example.com"},
+ "openid": {
+ "scopes": ["string"],
+ "user_identifier_claim": "user_identifier_claim",
+ "userinfo_endpoint": "https://example.com",
+ },
},
x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
)
@@ -208,13 +213,18 @@ def test_method_enable_with_all_params(self, client: KeycardAPI) -> None:
protocols={
"oauth2": {
"authorization_endpoint": "https://example.com",
+ "authorization_parameters": {"foo": "string"},
"code_challenge_methods_supported": ["string"],
"jwks_uri": "https://example.com",
"registration_endpoint": "https://example.com",
"scopes_supported": ["string"],
"token_endpoint": "https://example.com",
},
- "openid": {"userinfo_endpoint": "https://example.com"},
+ "openid": {
+ "scopes": ["string"],
+ "user_identifier_claim": "user_identifier_claim",
+ "userinfo_endpoint": "https://example.com",
+ },
},
x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
)
@@ -337,13 +347,18 @@ async def test_method_update_with_all_params(self, async_client: AsyncKeycardAPI
protocols={
"oauth2": {
"authorization_endpoint": "https://example.com",
+ "authorization_parameters": {"foo": "string"},
"code_challenge_methods_supported": ["string"],
"jwks_uri": "https://example.com",
"registration_endpoint": "https://example.com",
"scopes_supported": ["string"],
"token_endpoint": "https://example.com",
},
- "openid": {"userinfo_endpoint": "https://example.com"},
+ "openid": {
+ "scopes": ["string"],
+ "user_identifier_claim": "user_identifier_claim",
+ "userinfo_endpoint": "https://example.com",
+ },
},
x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
)
@@ -455,13 +470,18 @@ async def test_method_enable_with_all_params(self, async_client: AsyncKeycardAPI
protocols={
"oauth2": {
"authorization_endpoint": "https://example.com",
+ "authorization_parameters": {"foo": "string"},
"code_challenge_methods_supported": ["string"],
"jwks_uri": "https://example.com",
"registration_endpoint": "https://example.com",
"scopes_supported": ["string"],
"token_endpoint": "https://example.com",
},
- "openid": {"userinfo_endpoint": "https://example.com"},
+ "openid": {
+ "scopes": ["string"],
+ "user_identifier_claim": "user_identifier_claim",
+ "userinfo_endpoint": "https://example.com",
+ },
},
x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
)
diff --git a/tests/api_resources/zones/test_resources.py b/tests/api_resources/zones/test_resources.py
index da273d4..4e820a7 100644
--- a/tests/api_resources/zones/test_resources.py
+++ b/tests/api_resources/zones/test_resources.py
@@ -234,6 +234,7 @@ def test_method_list_with_all_params(self, client: KeycardAPI) -> None:
before="x",
credential_provider_id="credentialProviderId",
expand="total_count",
+ filter_identifier="string",
identifier="identifier",
limit=1,
slug="slug",
@@ -546,6 +547,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKeycardAPI)
before="x",
credential_provider_id="credentialProviderId",
expand="total_count",
+ filter_identifier="string",
identifier="identifier",
limit=1,
slug="slug",
diff --git a/tests/api_resources/zones/test_users.py b/tests/api_resources/zones/test_users.py
index 1a256ef..ffc908a 100644
--- a/tests/api_resources/zones/test_users.py
+++ b/tests/api_resources/zones/test_users.py
@@ -87,6 +87,7 @@ def test_method_list_with_all_params(self, client: KeycardAPI) -> None:
expand="total_count",
filter_email="dev@stainless.com",
filter_id="string",
+ filter_identifier="string",
limit=1,
query="x",
query_email="x",
@@ -205,6 +206,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKeycardAPI)
expand="total_count",
filter_email="dev@stainless.com",
filter_id="string",
+ filter_identifier="string",
limit=1,
query="x",
query_email="x",
diff --git a/tests/conftest.py b/tests/conftest.py
index 850730c..e57ccc4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -46,6 +46,7 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+api_key = "My API Key"
client_id = "My Client ID"
client_secret = "My Client Secret"
@@ -57,7 +58,11 @@ def client(request: FixtureRequest) -> Iterator[KeycardAPI]:
raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}")
with KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=strict
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=strict,
) as client:
yield client
@@ -84,6 +89,7 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncKeycardAPI
async with AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=strict,
diff --git a/tests/test_client.py b/tests/test_client.py
index a174064..a570787 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -39,6 +39,7 @@
T = TypeVar("T")
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+api_key = "My API Key"
client_id = "My Client ID"
client_secret = "My Client Secret"
@@ -137,6 +138,10 @@ def test_copy(self, client: KeycardAPI) -> None:
copied = client.copy()
assert id(copied) != id(client)
+ copied = client.copy(api_key="another My API Key")
+ assert copied.api_key == "another My API Key"
+ assert client.api_key == "My API Key"
+
copied = client.copy(client_id="another My Client ID")
assert copied.client_id == "another My Client ID"
assert client.client_id == "My Client ID"
@@ -164,6 +169,7 @@ def test_copy_default_options(self, client: KeycardAPI) -> None:
def test_copy_default_headers(self) -> None:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -203,6 +209,7 @@ def test_copy_default_headers(self) -> None:
def test_copy_default_query(self) -> None:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -333,6 +340,7 @@ def test_request_timeout(self, client: KeycardAPI) -> None:
def test_client_timeout_option(self) -> None:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -350,6 +358,7 @@ def test_http_client_timeout_option(self) -> None:
with httpx.Client(timeout=None) as http_client:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -366,6 +375,7 @@ def test_http_client_timeout_option(self) -> None:
with httpx.Client() as http_client:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -382,6 +392,7 @@ def test_http_client_timeout_option(self) -> None:
with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -399,6 +410,7 @@ async def test_invalid_http_client(self) -> None:
async with httpx.AsyncClient() as http_client:
KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -408,6 +420,7 @@ async def test_invalid_http_client(self) -> None:
def test_default_headers_option(self) -> None:
test_client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -419,6 +432,7 @@ def test_default_headers_option(self) -> None:
test_client2 = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -437,6 +451,7 @@ def test_default_headers_option(self) -> None:
def test_default_query_option(self) -> None:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -636,6 +651,7 @@ def mock_handler(request: httpx.Request) -> httpx.Response:
with KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -733,6 +749,7 @@ class Model(BaseModel):
def test_base_url_setter(self) -> None:
client = KeycardAPI(
base_url="https://example.com/from_init",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -747,7 +764,9 @@ def test_base_url_setter(self) -> None:
def test_base_url_env(self) -> None:
with update_env(KEYCARD_API_BASE_URL="http://localhost:5000/from/env"):
- client = KeycardAPI(client_id=client_id, client_secret=client_secret, _strict_response_validation=True)
+ client = KeycardAPI(
+ api_key=api_key, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ )
assert client.base_url == "http://localhost:5000/from/env/"
@pytest.mark.parametrize(
@@ -755,12 +774,14 @@ def test_base_url_env(self) -> None:
[
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -785,12 +806,14 @@ def test_base_url_trailing_slash(self, client: KeycardAPI) -> None:
[
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -815,12 +838,14 @@ def test_base_url_no_trailing_slash(self, client: KeycardAPI) -> None:
[
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -842,7 +867,11 @@ def test_absolute_request_url(self, client: KeycardAPI) -> None:
def test_copied_client_does_not_close_http(self) -> None:
test_client = KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
assert not test_client.is_closed()
@@ -855,7 +884,11 @@ def test_copied_client_does_not_close_http(self) -> None:
def test_client_context_manager(self) -> None:
test_client = KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
with test_client as c2:
assert c2 is test_client
@@ -879,6 +912,7 @@ def test_client_max_retries_validation(self) -> None:
with pytest.raises(TypeError, match=r"max_retries cannot be None"):
KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -893,14 +927,22 @@ class Model(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
strict_client = KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
with pytest.raises(APIResponseValidationError):
strict_client.get("/foo", cast_to=Model)
non_strict_client = KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=False
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=False,
)
response = non_strict_client.get("/foo", cast_to=Model)
@@ -1116,6 +1158,10 @@ def test_copy(self, async_client: AsyncKeycardAPI) -> None:
copied = async_client.copy()
assert id(copied) != id(async_client)
+ copied = async_client.copy(api_key="another My API Key")
+ assert copied.api_key == "another My API Key"
+ assert async_client.api_key == "My API Key"
+
copied = async_client.copy(client_id="another My Client ID")
assert copied.client_id == "another My Client ID"
assert async_client.client_id == "My Client ID"
@@ -1143,6 +1189,7 @@ def test_copy_default_options(self, async_client: AsyncKeycardAPI) -> None:
async def test_copy_default_headers(self) -> None:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1182,6 +1229,7 @@ async def test_copy_default_headers(self) -> None:
async def test_copy_default_query(self) -> None:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1314,6 +1362,7 @@ async def test_request_timeout(self, async_client: AsyncKeycardAPI) -> None:
async def test_client_timeout_option(self) -> None:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1331,6 +1380,7 @@ async def test_http_client_timeout_option(self) -> None:
async with httpx.AsyncClient(timeout=None) as http_client:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1347,6 +1397,7 @@ async def test_http_client_timeout_option(self) -> None:
async with httpx.AsyncClient() as http_client:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1363,6 +1414,7 @@ async def test_http_client_timeout_option(self) -> None:
async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1380,6 +1432,7 @@ def test_invalid_http_client(self) -> None:
with httpx.Client() as http_client:
AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1389,6 +1442,7 @@ def test_invalid_http_client(self) -> None:
async def test_default_headers_option(self) -> None:
test_client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1400,6 +1454,7 @@ async def test_default_headers_option(self) -> None:
test_client2 = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1418,6 +1473,7 @@ async def test_default_headers_option(self) -> None:
async def test_default_query_option(self) -> None:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1617,6 +1673,7 @@ async def mock_handler(request: httpx.Request) -> httpx.Response:
async with AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1718,6 +1775,7 @@ class Model(BaseModel):
async def test_base_url_setter(self) -> None:
client = AsyncKeycardAPI(
base_url="https://example.com/from_init",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1732,7 +1790,9 @@ async def test_base_url_setter(self) -> None:
async def test_base_url_env(self) -> None:
with update_env(KEYCARD_API_BASE_URL="http://localhost:5000/from/env"):
- client = AsyncKeycardAPI(client_id=client_id, client_secret=client_secret, _strict_response_validation=True)
+ client = AsyncKeycardAPI(
+ api_key=api_key, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ )
assert client.base_url == "http://localhost:5000/from/env/"
@pytest.mark.parametrize(
@@ -1740,12 +1800,14 @@ async def test_base_url_env(self) -> None:
[
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1770,12 +1832,14 @@ async def test_base_url_trailing_slash(self, client: AsyncKeycardAPI) -> None:
[
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1800,12 +1864,14 @@ async def test_base_url_no_trailing_slash(self, client: AsyncKeycardAPI) -> None
[
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1827,7 +1893,11 @@ async def test_absolute_request_url(self, client: AsyncKeycardAPI) -> None:
async def test_copied_client_does_not_close_http(self) -> None:
test_client = AsyncKeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
assert not test_client.is_closed()
@@ -1841,7 +1911,11 @@ async def test_copied_client_does_not_close_http(self) -> None:
async def test_client_context_manager(self) -> None:
test_client = AsyncKeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
async with test_client as c2:
assert c2 is test_client
@@ -1867,6 +1941,7 @@ async def test_client_max_retries_validation(self) -> None:
with pytest.raises(TypeError, match=r"max_retries cannot be None"):
AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1881,14 +1956,22 @@ class Model(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
strict_client = AsyncKeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
with pytest.raises(APIResponseValidationError):
await strict_client.get("/foo", cast_to=Model)
non_strict_client = AsyncKeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=False
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=False,
)
response = await non_strict_client.get("/foo", cast_to=Model)