Skip to content

feat: Add Connect Applications and Client Secrets module#585

Merged
gjtorikian merged 3 commits intoworkos:mainfrom
blackmad-cradle:add-connect-applications-module
Mar 6, 2026
Merged

feat: Add Connect Applications and Client Secrets module#585
gjtorikian merged 3 commits intoworkos:mainfrom
blackmad-cradle:add-connect-applications-module

Conversation

@blackmad-cradle
Copy link
Contributor

Implement WorkOS Connect (M2M) endpoints for managing applications and client secrets via client.connect.

Endpoints:

  • GET/POST /connect/applications (list, create)
  • GET/PUT/DELETE /connect/applications/:id (get, update, delete)
  • GET/POST /connect/applications/:id/client_secrets (list, create)
  • DELETE /connect/client_secrets/:id (delete)

Description

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required.

Implement WorkOS Connect (M2M) endpoints for managing applications
and client secrets via client.connect.

Endpoints:
- GET/POST /connect/applications (list, create)
- GET/PUT/DELETE /connect/applications/:id (get, update, delete)
- GET/POST /connect/applications/:id/client_secrets (list, create)
- DELETE /connect/client_secrets/:id (delete)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@blackmad-cradle blackmad-cradle requested review from a team as code owners March 6, 2026 19:53
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR adds a new connect module to the WorkOS Python SDK, implementing full CRUD for Connect Applications and Client Secrets via client.connect. The implementation is consistent with the existing module patterns (lazy-initialised property, Protocol-based interface, sync + async parity, WorkOSListResource pagination) and is well-tested.

Key changes:

  • New src/workos/connect.py with ConnectModule (Protocol), Connect (sync), and AsyncConnect implementations covering 8 endpoints
  • New Pydantic models: ConnectApplication, ClientSecret, RedirectUri, and associated list-filter TypedDicts
  • ClientSecret and ConnectApplication added to the ListableResource TypeVar in list_resource.py
  • client.connect and async_client.connect properties wired up; abstract property added to BaseClient
  • Comprehensive test coverage including auto-pagination tests for both resource types

Issues found:

  • update_application includes None for every unset optional field in the PUT body. The description docstring explicitly states "Pass None to clear", which indicates the API honours null as a clear instruction. The same behaviour for scopes and redirect_uris is undocumented, raising a risk that a caller updating only name could silently clear those fields. Consider filtering out None values for fields not intended to be clearable, or documenting the API's exact semantics for each nullable field.
  • create_application similarly sends null for all unset optional fields, which may cause unexpected API validation errors for fields the API does not accept as null.
  • The MockConnectApplication fixture types application_type as str rather than the ApplicationType literal, which weakens static type checking in tests.

Confidence Score: 3/5

  • Safe to merge with caution — the null-value behaviour in update/create payloads should be clarified or fixed before shipping to avoid silent data loss.
  • The module structure and test coverage are solid and consistent with the rest of the SDK. The main risk is the update_application behaviour where unset optional fields are sent as null, which — given that description=None is explicitly documented as clearing the field — could silently clear scopes or redirect_uris when callers only intend to update name. Until the API semantics for those fields are confirmed and documented (or null-stripping is implemented), there is a non-trivial risk of unintended data mutation for end users.
  • Pay close attention to src/workos/connect.py — specifically the JSON payloads constructed in update_application (lines 350–355) and create_application (lines 322–331), as well as their async counterparts.

Important Files Changed

Filename Overview
src/workos/connect.py New Connect module implementing sync/async CRUD for applications and client secrets; follows existing codebase patterns but update_application (and to a lesser extent create_application) include null for all unset optional fields, which may silently clear data depending on API behaviour.
src/workos/types/connect/connect_application.py Well-defined Pydantic model for ConnectApplication; uses LiteralOrUntyped for forward-compatible application_type handling and nests RedirectUri correctly.
src/workos/types/connect/client_secret.py Clean model for ClientSecret; secret is correctly typed as Optional (only present on creation response).
src/workos/types/connect/list_filters.py Minimal filter TypedDicts extending ListArgs; ClientSecretListFilters is an empty stub but structurally correct and consistent with the codebase pattern.
src/workos/types/list_resource.py Correctly adds ClientSecret and ConnectApplication to the ListableResource TypeVar; no issues.
tests/test_connect.py Good test coverage for all endpoints including auto-pagination; tests for update don't assert absence of unintended null fields in the request payload.
tests/utils/fixtures/mock_connect_application.py Good fixture covering both m2m and oauth variants; application_type parameter typed as str instead of ApplicationType literal, losing static type safety.
src/workos/_base_client.py Minimal, correct addition of abstract connect property to BaseClient.
src/workos/async_client.py Lazy-initialised AsyncConnect property added correctly following existing module pattern.
src/workos/client.py Lazy-initialised Connect property added correctly following existing module pattern.
src/workos/types/connect/init.py Re-exports ConnectApplication and ClientSecret via wildcard imports; consistent with other type packages in the codebase.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant client.connect
    participant WorkOS API

    Caller->>client.connect: create_application(name, application_type, ...)
    client.connect->>WorkOS API: POST /connect/applications
    WorkOS API-->>client.connect: ConnectApplication JSON
    client.connect-->>Caller: ConnectApplication

    Caller->>client.connect: get_application(application_id)
    client.connect->>WorkOS API: GET /connect/applications/:id
    WorkOS API-->>client.connect: ConnectApplication JSON
    client.connect-->>Caller: ConnectApplication

    Caller->>client.connect: update_application(application_id, name, ...)
    client.connect->>WorkOS API: PUT /connect/applications/:id
    WorkOS API-->>client.connect: ConnectApplication JSON
    client.connect-->>Caller: ConnectApplication

    Caller->>client.connect: list_applications(organization_id, ...)
    client.connect->>WorkOS API: GET /connect/applications
    WorkOS API-->>client.connect: List[ConnectApplication] + pagination
    client.connect-->>Caller: ConnectApplicationsListResource

    Caller->>client.connect: create_client_secret(application_id)
    client.connect->>WorkOS API: POST /connect/applications/:id/client_secrets
    WorkOS API-->>client.connect: ClientSecret JSON (with secret)
    client.connect-->>Caller: ClientSecret

    Caller->>client.connect: list_client_secrets(application_id, ...)
    client.connect->>WorkOS API: GET /connect/applications/:id/client_secrets
    WorkOS API-->>client.connect: List[ClientSecret] (no secret field)
    client.connect-->>Caller: ClientSecretsListResource

    Caller->>client.connect: delete_client_secret(client_secret_id)
    client.connect->>WorkOS API: DELETE /connect/client_secrets/:id
    WorkOS API-->>client.connect: 202 No Content
    client.connect-->>Caller: None

    Caller->>client.connect: delete_application(application_id)
    client.connect->>WorkOS API: DELETE /connect/applications/:id
    WorkOS API-->>client.connect: 202 No Content
    client.connect-->>Caller: None
Loading

Last reviewed commit: 38e0333

Comment on lines +350 to +355
after: Optional[str] = None,
order: PaginationOrder = "desc",
) -> ConnectApplicationsListResource:
list_params: ConnectApplicationListFilters = {
"organization_id": organization_id,
"limit": limit,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unset optional fields sent as null may clear existing data

All optional fields default to None and are unconditionally included in the JSON payload, even when not supplied by the caller. The docstring for description explicitly states "Pass None to clear", which confirms the API does treat null as a clear-this-field instruction for at least that field.

This means a call like:

connect.update_application(application_id="app_01ABC", name="New Name")

…sends {"name": "New Name", "description": null, "scopes": null, "redirect_uris": null} to the API. Depending on how the API handles null for scopes and redirect_uris, this could silently clear those fields even though the caller only intended to update the name.

Consider filtering out None values before sending — or at minimum document explicitly in the docstring which fields treat None as "clear" versus "no-op". A common pattern is:

json = {
    k: v
    for k, v in {
        "name": name,
        "description": description,
        "scopes": scopes,
        "redirect_uris": redirect_uris,
    }.items()
    if v is not None
}

The same issue exists in the AsyncConnect.update_application implementation at lines 502–507.

Comment on lines +322 to +331

return WorkOSListResource[
ClientSecret, ClientSecretListFilters, ListMetadata
](
list_method=partial(self.list_client_secrets, application_id),
list_args=list_params,
**ListPage[ClientSecret](**response).model_dump(),
)

def delete_client_secret(self, client_secret_id: str) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None values for optional fields included in create_application payload

Optional fields (description, scopes, redirect_uris, uses_pkce, organization_id) are sent as null in the POST body even when not provided. Some REST APIs reject unknown or unexpected null values, or may exhibit unintended behaviour (e.g., failing schema validation for nullable-but-required-by-the-API arrays like scopes).

If the API gracefully ignores null for optional creation fields this is harmless, but it is safer to strip None values before sending:

json = {
    k: v
    for k, v in {
        "name": name,
        "application_type": application_type,
        "is_first_party": is_first_party,
        "description": description,
        "scopes": scopes,
        "redirect_uris": redirect_uris,
        "uses_pkce": uses_pkce,
        "organization_id": organization_id,
    }.items()
    if v is not None
}

The same applies to AsyncConnect.create_application at lines 474–483.

kwargs = {
"object": "connect_application",
"id": id,
"client_id": f"client_{id}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

application_type parameter typed as str instead of ApplicationType

The application_type parameter is typed as str rather than the ApplicationType = Literal["oauth", "m2m"] alias defined in connect_application.py. Using str loses type-safety and won't catch invalid values at the call site during static analysis.

Suggested change
"client_id": f"client_{id}",
def __init__(self, id: str, application_type: str = "m2m"):

Consider importing and using ApplicationType:

from workos.types.connect.connect_application import ApplicationType

def __init__(self, id: str, application_type: ApplicationType = "m2m"):

@gjtorikian gjtorikian changed the title Add Connect Applications and Client Secrets module feat: Add Connect Applications and Client Secrets module Mar 6, 2026
gjtorikian and others added 2 commits March 6, 2026 17:13
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass None for response_dict and 202 for status_code in delete tests
  (previously 202 was incorrectly passed as response_dict)
- Type application_type parameter as ApplicationType instead of str

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gjtorikian gjtorikian merged commit 1ad1623 into workos:main Mar 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants