Skip to content

feat: make vault events available in SDK#588

Open
vivekkhimani wants to merge 3 commits intoworkos:mainfrom
vivekkhimani:vivek/vault-events
Open

feat: make vault events available in SDK#588
vivekkhimani wants to merge 3 commits intoworkos:mainfrom
vivekkhimani:vivek/vault-events

Conversation

@vivekkhimani
Copy link
Contributor

@vivekkhimani vivekkhimani commented Mar 12, 2026

Description

  • Add all 9 Vault event types to the Events module:
    • vault.data.created
    • vault.data.deleted
    • vault.data.read
    • vault.data.updated
    • vault.dek.decrypted
    • vault.dek.read
    • vault.kek.created
    • vault.metadata.read
    • vault.names.listed
  • Fix EventRole missing created_at / updated_at fields that the API returns on role events
  • Fix OrganizationCommon missing external_id field that the API returns on organization events
    (it previously existed only on the Organization subclass, not on the event payload model)

Closes #573


Vault event details

Seven payload classes cover all nine events. Where two events share the same shape, they reuse a payload class (following the same pattern as ConnectionPayloadWithLegacyFields, which is used for both connection.activated and connection.deactivated).

Shared payload mappings:

  • VaultDataCreatedPayload
    • vault.data.created
    • vault.data.updated
  • VaultDataDeletedPayload
    • vault.data.deleted
    • vault.metadata.read

All payloads were verified against:
https://workos.com/docs/events#vault


Existing event fixes

While auditing Vault events against the docs, every other event type was also reviewed. Two gaps were identified and fixed (please let me know if you want to not do it as a part of this PR):

1. EventRole

EventRole was missing the fields:

  • created_at
  • updated_at

Both were added as Optional[str] to remain consistent with the SDK’s defensive typing approach and avoid breaking existing consumers.

2. OrganizationCommon

OrganizationCommon was missing the external_id field.

Changes made:

  • Moved external_id from Organization (where it already existed) into OrganizationCommon
  • Removed the redundant redeclaration in Organization

This ensures organization event payloads include external_id.


Test plan

  • Added 3 Vault event tests covering different payload shapes:
    • data.created with key_context
    • dek.read with key_ids
    • names.listed with actor-only fields
  • uv run ruff format . && uv run ruff check . passes
  • uv run mypy passes (strict mode, 163 source files)
  • Full test suite passes (753 tests)

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.

@vivekkhimani vivekkhimani changed the title vault events feat: make vault events available in SDK Mar 12, 2026
id: str
object: Literal["organization"]
name: str
external_id: Optional[str] = None
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me know if you all disagree moving this up in OrganizationCommon

@vivekkhimani vivekkhimani marked this pull request as ready for review March 12, 2026 17:58
@vivekkhimani vivekkhimani requested review from a team as code owners March 12, 2026 17:58
@vivekkhimani vivekkhimani requested a review from gcarvelli March 12, 2026 17:58
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR expands the Events module with 9 new Vault event types, and fixes two pre-existing gaps in EventRole (missing created_at/updated_at) and OrganizationCommon (missing external_id). The implementation is logically sound and follows established SDK patterns.

Key changes:

  • New vault_payload.py: 7 payload classes cover all 9 vault event types; where two events share the same shape, a single payload class is reused (matching the ConnectionPayloadWithLegacyFields pattern).
  • event.py / event_model.py / event_type.py: 9 new VaultXxxEvent classes added and registered in the discriminated Event union; all 7 new payload types added to the EventPayload TypeVar.
  • VaultMetadataReadEvent.data typed as VaultDataDeletedPayload: Intentional class reuse, but the "Deleted" name is semantically misleading for a metadata-read event — a type alias (VaultMetadataReadPayload = VaultDataDeletedPayload) would preserve the implementation while giving the data field a correctly-named type.
  • OrganizationCommon: external_id promoted from Organization subclass — backward-compatible and fixes event payload gaps.
  • EventRole: created_at / updated_at added as Optional[str] — defensive and consistent with SDK typing conventions.
  • Tests: 3 tests cover 3 distinct payload shapes; VaultDekDecryptedPayload and VaultKekCreatedPayload (both unique shapes) are not directly tested, and no tests confirm discriminator resolution for vault.data.updated, vault.data.deleted, or vault.metadata.read.

Confidence Score: 4/5

  • This PR is safe to merge; changes are additive and backward-compatible with minor style concerns around naming and test coverage.
  • All structural changes are additive and backward-compatible. The external_id and EventRole fixes are low-risk. The vault payload field choices were verified against WorkOS docs per the PR description. The main notes are a semantic naming mismatch (VaultMetadataReadEvent.data typed as VaultDataDeletedPayload) and incomplete test coverage for 6 of 9 new event types — neither is a runtime bug.
  • src/workos/types/events/event.py (semantic type naming on VaultMetadataReadEvent) and tests/test_events.py (missing tests for VaultDekDecryptedPayload and VaultKekCreatedPayload shapes)

Important Files Changed

Filename Overview
src/workos/types/events/vault_payload.py New file defining 7 payload classes for 9 vault event types; all actor fields are required strings and field choices appear to match documented API shapes. KeyContext (a RootModel[Dict[str, str]]) is correctly reused from the vault package.
src/workos/types/events/event.py 9 new VaultXxxEvent classes added and registered in the discriminated union; VaultMetadataReadEvent.data is typed as VaultDataDeletedPayload, which is semantically misleading (intentional class-reuse per PR, but confusing for consumers). All other mappings look correct.
src/workos/types/events/event_model.py 7 vault payload types correctly added to the EventPayload TypeVar; covers all distinct payload classes used by the 9 new events (shared types are only listed once, which is correct).
src/workos/types/events/event_type.py All 9 vault event literal strings added to EventType; consistent with existing entries and alphabetically ordered within the vault prefix group.
src/workos/types/organizations/organization_common.py external_id: Optional[str] = None correctly promoted from Organization subclass to OrganizationCommon so that event payloads using the common model also expose the field. Backward-compatible change.
src/workos/types/organizations/organization.py Redundant external_id declaration removed; field is now inherited from OrganizationCommon. No functional regression since Organization still exposes the attribute via inheritance.
src/workos/types/roles/role.py created_at and updated_at added as Optional[str] to EventRole, matching the defensive typing pattern used elsewhere in the SDK.
tests/test_events.py Three tests added covering vault.data.created, vault.dek.read, and vault.names.listed; the distinct payload shapes of VaultDekDecryptedPayload and VaultKekCreatedPayload are not directly tested, and no tests confirm the event discriminator resolves correctly for vault.data.updated, vault.data.deleted, or vault.metadata.read.

Class Diagram

%%{init: {'theme': 'neutral'}}%%
classDiagram
    class WorkOSModel
    class EventModel~T~ {
        +str id
        +Literal["event"] object
        +T data
        +str created_at
    }

    class VaultNamesListedPayload {
        +str actor_id
        +str actor_source
        +str actor_name
    }
    class VaultDataDeletedPayload {
        +str actor_id
        +str actor_source
        +str actor_name
        +str kv_name
    }
    class VaultDekDecryptedPayload {
        +str actor_id
        +str actor_source
        +str actor_name
        +str key_id
    }
    class VaultDataReadPayload {
        +str actor_id
        +str actor_source
        +str actor_name
        +str kv_name
        +str key_id
    }
    class VaultDataCreatedPayload {
        +str actor_id
        +str actor_source
        +str actor_name
        +str kv_name
        +str key_id
        +Optional~KeyContext~ key_context
    }
    class VaultDekReadPayload {
        +str actor_id
        +str actor_source
        +str actor_name
        +List~str~ key_ids
        +Optional~KeyContext~ key_context
    }
    class VaultKekCreatedPayload {
        +str actor_id
        +str actor_source
        +str actor_name
        +str key_name
        +str key_id
    }

    WorkOSModel <|-- VaultNamesListedPayload
    WorkOSModel <|-- VaultDataDeletedPayload
    WorkOSModel <|-- VaultDekDecryptedPayload
    WorkOSModel <|-- VaultDataReadPayload
    WorkOSModel <|-- VaultDataCreatedPayload
    WorkOSModel <|-- VaultDekReadPayload
    WorkOSModel <|-- VaultKekCreatedPayload

    EventModel~VaultNamesListedPayload~ <|-- VaultNamesListedEvent : event = "vault.names.listed"
    EventModel~VaultDataDeletedPayload~ <|-- VaultDataDeletedEvent : event = "vault.data.deleted"
    EventModel~VaultDataDeletedPayload~ <|-- VaultMetadataReadEvent : event = "vault.metadata.read"
    EventModel~VaultDekDecryptedPayload~ <|-- VaultDekDecryptedEvent : event = "vault.dek.decrypted"
    EventModel~VaultDataReadPayload~ <|-- VaultDataReadEvent : event = "vault.data.read"
    EventModel~VaultDataCreatedPayload~ <|-- VaultDataCreatedEvent : event = "vault.data.created"
    EventModel~VaultDataCreatedPayload~ <|-- VaultDataUpdatedEvent : event = "vault.data.updated"
    EventModel~VaultDekReadPayload~ <|-- VaultDekReadEvent : event = "vault.dek.read"
    EventModel~VaultKekCreatedPayload~ <|-- VaultKekCreatedEvent : event = "vault.kek.created"
Loading

Last reviewed commit: da8c1e8

Comment on lines +412 to +413
class VaultMetadataReadEvent(EventModel[VaultDataDeletedPayload]):
event: Literal["vault.metadata.read"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Semantic type mismatch on VaultMetadataReadEvent.data

VaultMetadataReadEvent.data is typed as VaultDataDeletedPayload, which is semantically misleading for SDK consumers. A user inspecting the type of event.data on a vault.metadata.read event will find it annotated as a "deleted" payload, which is confusing when navigating type hints or IDE autocomplete.

The ConnectionPayloadWithLegacyFields precedent cited in the PR description works because its name is generic/neutral; here the reused class name carries a specific (and incorrect) semantic meaning for the second event it covers. A type alias VaultMetadataReadPayload = VaultDataDeletedPayload in vault_payload.py (and exported/imported accordingly) would preserve the single-class implementation while giving the event a correctly-named data type:

# vault_payload.py
VaultMetadataReadPayload = VaultDataDeletedPayload
# event.py
class VaultMetadataReadEvent(EventModel[VaultMetadataReadPayload]):
    event: Literal["vault.metadata.read"]

@@ -86,3 +91,135 @@ def test_list_events_organization_membership_missing_custom_attributes(
event = events.data[0]
assert isinstance(event, OrganizationMembershipCreatedEvent)
assert event.data.custom_attributes == {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Test coverage gap for distinct payload shapes

The PR adds 9 new event types but only 3 are covered by tests. The two payload classes that have unique structures and are not tested at all are VaultDekDecryptedPayload (used by vault.dek.decrypted) and VaultKekCreatedPayload (used by vault.kek.created). Both have distinct field sets not covered by any of the three tests:

  • VaultDekDecryptedPayload has key_id: str (no key_ids, no key_context)
  • VaultKekCreatedPayload has key_name: str and key_id: str

Additionally, vault.data.updated (which reuses VaultDataCreatedPayload) and vault.metadata.read (which reuses VaultDataDeletedPayload) have no tests confirming the event discriminator parses correctly to the expected class. Adding fixture-backed tests for these cases would match the coverage level of other event groups in this file.

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.

Add vault events to the SDK

1 participant