From 5a04c49ef99279e6d890463d68af7558e607025a Mon Sep 17 00:00:00 2001 From: David Blackman Date: Sat, 7 Mar 2026 21:55:53 +0100 Subject: [PATCH 1/5] fix: list_client_secrets returns raw list, not paginated response The API endpoint returns a plain JSON array for client secrets, not a paginated response object. Updated to match the pattern used by other raw-list endpoints (e.g. fga.batch_check, vault.list_object_versions). Co-Authored-By: Claude Opus 4.6 --- src/workos/connect.py | 67 +++++-------------------------------------- tests/test_connect.py | 42 ++------------------------- 2 files changed, 9 insertions(+), 100 deletions(-) diff --git a/src/workos/connect.py b/src/workos/connect.py index 19db5c51..039a2a37 100644 --- a/src/workos/connect.py +++ b/src/workos/connect.py @@ -1,12 +1,8 @@ -from functools import partial from typing import Optional, Protocol, Sequence from workos.types.connect import ClientSecret, ConnectApplication from workos.types.connect.connect_application import ApplicationType -from workos.types.connect.list_filters import ( - ClientSecretListFilters, - ConnectApplicationListFilters, -) +from workos.types.connect.list_filters import ConnectApplicationListFilters from workos.types.list_resource import ListMetadata, ListPage, WorkOSListResource from workos.typing.sync_or_async import SyncOrAsync from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient @@ -26,10 +22,6 @@ ConnectApplication, ConnectApplicationListFilters, ListMetadata ] -ClientSecretsListResource = WorkOSListResource[ - ClientSecret, ClientSecretListFilters, ListMetadata -] - class ConnectModule(Protocol): """Offers methods through the WorkOS Connect service.""" @@ -145,25 +137,14 @@ def create_client_secret(self, application_id: str) -> SyncOrAsync[ClientSecret] def list_client_secrets( self, application_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[ClientSecretsListResource]: + ) -> SyncOrAsync[Sequence[ClientSecret]]: """List client secrets for a connect application. Args: application_id (str): Application ID or client ID. - Kwargs: - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided ID. (Optional) - after (str): Pagination cursor to receive records after a provided ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending order. (Optional) - Returns: - ClientSecretsListResource: Client secrets list response from WorkOS. + Sequence[ClientSecret]: Client secrets for the application. """ ... @@ -297,30 +278,13 @@ def create_client_secret(self, application_id: str) -> ClientSecret: def list_client_secrets( self, application_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> ClientSecretsListResource: - list_params: ClientSecretListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - + ) -> Sequence[ClientSecret]: response = self._http_client.request( f"{CONNECT_APPLICATIONS_PATH}/{application_id}/client_secrets", method=REQUEST_METHOD_GET, - params=list_params, ) - return WorkOSListResource[ClientSecret, ClientSecretListFilters, ListMetadata]( - list_method=partial(self.list_client_secrets, application_id), - list_args=list_params, - **ListPage[ClientSecret](**response).model_dump(), - ) + return [ClientSecret.model_validate(secret) for secret in response] def delete_client_secret(self, client_secret_id: str) -> None: self._http_client.request( @@ -447,30 +411,13 @@ async def create_client_secret(self, application_id: str) -> ClientSecret: async def list_client_secrets( self, application_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> ClientSecretsListResource: - list_params: ClientSecretListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - + ) -> Sequence[ClientSecret]: response = await self._http_client.request( f"{CONNECT_APPLICATIONS_PATH}/{application_id}/client_secrets", method=REQUEST_METHOD_GET, - params=list_params, ) - return WorkOSListResource[ClientSecret, ClientSecretListFilters, ListMetadata]( - list_method=partial(self.list_client_secrets, application_id), - list_args=list_params, - **ListPage[ClientSecret](**response).model_dump(), - ) + return [ClientSecret.model_validate(secret) for secret in response] async def delete_client_secret(self, client_secret_id: str) -> None: await self._http_client.request( diff --git a/tests/test_connect.py b/tests/test_connect.py index 8ae56dc7..66fadfaa 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -45,17 +45,7 @@ def mock_client_secret(self): @pytest.fixture def mock_client_secrets(self): - secret_list = [MockClientSecret(id=f"cs_{i}").dict() for i in range(10)] - return { - "data": secret_list, - "list_metadata": {"before": None, "after": None}, - "object": "list", - } - - @pytest.fixture - def mock_client_secrets_multiple_data_pages(self): - secrets_list = [MockClientSecret(id=f"cs_{i + 1}").dict() for i in range(40)] - return list_response_of(data=secrets_list) + return [MockClientSecret(id=f"cs_{i}").dict() for i in range(10)] # --- Application Tests --- @@ -250,9 +240,7 @@ def test_list_client_secrets( assert request_kwargs["url"].endswith( "/connect/applications/app_01ABC/client_secrets" ) - assert ( - list(map(lambda x: x.dict(), response.data)) == mock_client_secrets["data"] - ) + assert [secret.dict() for secret in response] == mock_client_secrets def test_delete_client_secret(self, capture_and_mock_http_client_request): request_kwargs = capture_and_mock_http_client_request( @@ -269,29 +257,3 @@ def test_delete_client_secret(self, capture_and_mock_http_client_request): assert request_kwargs["url"].endswith("/connect/client_secrets/cs_01ABC") assert request_kwargs["method"] == "delete" assert response is None - - def test_list_client_secrets_auto_pagination_for_single_page( - self, - mock_client_secrets, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.connect.list_client_secrets, - expected_all_page_data=mock_client_secrets["data"], - list_function_params={"application_id": "app_01ABC"}, - url_path_keys=["application_id"], - ) - - def test_list_client_secrets_auto_pagination_for_multiple_pages( - self, - mock_client_secrets_multiple_data_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.connect.list_client_secrets, - expected_all_page_data=mock_client_secrets_multiple_data_pages["data"], - list_function_params={"application_id": "app_01ABC"}, - url_path_keys=["application_id"], - ) From a9b256c1dbcdc5e81186881bc90bb50de95425f5 Mon Sep 17 00:00:00 2001 From: David Blackman Date: Sat, 7 Mar 2026 22:00:24 +0100 Subject: [PATCH 2/5] fix: redirect_uris params should accept objects, not strings The API expects redirect_uris as an array of {uri, default} objects, not plain strings. Added RedirectUriInput TypedDict and updated create_application/update_application signatures to match. Co-Authored-By: Claude Opus 4.6 --- src/workos/connect.py | 13 +++++++------ src/workos/types/connect/redirect_uri_input.py | 8 ++++++++ tests/test_connect.py | 4 ++-- 3 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 src/workos/types/connect/redirect_uri_input.py diff --git a/src/workos/connect.py b/src/workos/connect.py index 039a2a37..f8b73bf2 100644 --- a/src/workos/connect.py +++ b/src/workos/connect.py @@ -3,6 +3,7 @@ from workos.types.connect import ClientSecret, ConnectApplication from workos.types.connect.connect_application import ApplicationType from workos.types.connect.list_filters import ConnectApplicationListFilters +from workos.types.connect.redirect_uri_input import RedirectUriInput from workos.types.list_resource import ListMetadata, ListPage, WorkOSListResource from workos.typing.sync_or_async import SyncOrAsync from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient @@ -68,7 +69,7 @@ def create_application( is_first_party: bool, description: Optional[str] = None, scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[str]] = None, + redirect_uris: Optional[Sequence[RedirectUriInput]] = None, uses_pkce: Optional[bool] = None, organization_id: Optional[str] = None, ) -> SyncOrAsync[ConnectApplication]: @@ -96,7 +97,7 @@ def update_application( name: Optional[str] = None, description: Optional[str] = None, scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[str]] = None, + redirect_uris: Optional[Sequence[RedirectUriInput]] = None, ) -> SyncOrAsync[ConnectApplication]: """Update a connect application. @@ -213,7 +214,7 @@ def create_application( is_first_party: bool, description: Optional[str] = None, scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[str]] = None, + redirect_uris: Optional[Sequence[RedirectUriInput]] = None, uses_pkce: Optional[bool] = None, organization_id: Optional[str] = None, ) -> ConnectApplication: @@ -243,7 +244,7 @@ def update_application( name: Optional[str] = None, description: Optional[str] = None, scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[str]] = None, + redirect_uris: Optional[Sequence[RedirectUriInput]] = None, ) -> ConnectApplication: json = { "name": name, @@ -346,7 +347,7 @@ async def create_application( is_first_party: bool, description: Optional[str] = None, scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[str]] = None, + redirect_uris: Optional[Sequence[RedirectUriInput]] = None, uses_pkce: Optional[bool] = None, organization_id: Optional[str] = None, ) -> ConnectApplication: @@ -376,7 +377,7 @@ async def update_application( name: Optional[str] = None, description: Optional[str] = None, scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[str]] = None, + redirect_uris: Optional[Sequence[RedirectUriInput]] = None, ) -> ConnectApplication: json = { "name": name, diff --git a/src/workos/types/connect/redirect_uri_input.py b/src/workos/types/connect/redirect_uri_input.py new file mode 100644 index 00000000..755d7b68 --- /dev/null +++ b/src/workos/types/connect/redirect_uri_input.py @@ -0,0 +1,8 @@ +from typing import Optional + +from typing_extensions import TypedDict + + +class RedirectUriInput(TypedDict, total=False): + uri: str + default: Optional[bool] diff --git a/tests/test_connect.py b/tests/test_connect.py index 66fadfaa..c8bc042f 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -139,7 +139,7 @@ def test_create_oauth_application( name="Test Application", application_type="oauth", is_first_party=True, - redirect_uris=["https://example.com/callback"], + redirect_uris=[{"uri": "https://example.com/callback", "default": True}], uses_pkce=True, ) ) @@ -148,7 +148,7 @@ def test_create_oauth_application( assert request_kwargs["method"] == "post" assert request_kwargs["json"]["application_type"] == "oauth" assert request_kwargs["json"]["redirect_uris"] == [ - "https://example.com/callback" + {"uri": "https://example.com/callback", "default": True} ] def test_update_application( From 96eded532245233b23deffb6a0606c68dcc30f26 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 9 Mar 2026 12:46:24 -0400 Subject: [PATCH 3/5] Remove unused ClientSecretListFilters class The list_client_secrets endpoint no longer uses pagination, so this filter class is dead code. Co-Authored-By: Claude Opus 4.6 --- src/workos/types/connect/list_filters.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/workos/types/connect/list_filters.py b/src/workos/types/connect/list_filters.py index ff93ca1a..56bf5d48 100644 --- a/src/workos/types/connect/list_filters.py +++ b/src/workos/types/connect/list_filters.py @@ -5,7 +5,3 @@ class ConnectApplicationListFilters(ListArgs, total=False): organization_id: Optional[str] - - -class ClientSecretListFilters(ListArgs, total=False): - pass From 5acb766b773fd49ca92fdb31955c0f9ba5968c40 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 9 Mar 2026 12:46:29 -0400 Subject: [PATCH 4/5] Make uri field required in RedirectUriInput Use TypedDict inheritance pattern (Python 3.8+ compatible) so that uri is a required key while default remains optional. Co-Authored-By: Claude Opus 4.6 --- src/workos/types/connect/redirect_uri_input.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/workos/types/connect/redirect_uri_input.py b/src/workos/types/connect/redirect_uri_input.py index 755d7b68..f900adce 100644 --- a/src/workos/types/connect/redirect_uri_input.py +++ b/src/workos/types/connect/redirect_uri_input.py @@ -1,8 +1,9 @@ -from typing import Optional - from typing_extensions import TypedDict -class RedirectUriInput(TypedDict, total=False): +class _RedirectUriInputRequired(TypedDict): uri: str - default: Optional[bool] + + +class RedirectUriInput(_RedirectUriInputRequired, total=False): + default: bool From 3ddbba96a2121c6cc9efbb6c330a677c7ee71177 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 9 Mar 2026 12:58:06 -0400 Subject: [PATCH 5/5] Run ruff format on test_connect.py Co-Authored-By: Claude Opus 4.6 --- tests/test_connect.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index c8bc042f..1c0823fb 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -139,7 +139,9 @@ def test_create_oauth_application( name="Test Application", application_type="oauth", is_first_party=True, - redirect_uris=[{"uri": "https://example.com/callback", "default": True}], + redirect_uris=[ + {"uri": "https://example.com/callback", "default": True} + ], uses_pkce=True, ) )