workos 5.22.0__tar.gz → 5.24.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. {workos-5.22.0 → workos-5.24.0}/PKG-INFO +1 -1
  2. {workos-5.22.0 → workos-5.24.0}/tests/test_organizations.py +13 -9
  3. {workos-5.22.0 → workos-5.24.0}/tests/test_user_management.py +23 -0
  4. workos-5.24.0/tests/test_vault.py +460 -0
  5. {workos-5.22.0 → workos-5.24.0}/workos/__about__.py +1 -1
  6. {workos-5.22.0 → workos-5.24.0}/workos/async_client.py +7 -0
  7. {workos-5.22.0 → workos-5.24.0}/workos/client.py +7 -0
  8. {workos-5.22.0 → workos-5.24.0}/workos/types/events/event.py +15 -0
  9. {workos-5.22.0 → workos-5.24.0}/workos/types/events/event_type.py +3 -0
  10. {workos-5.22.0 → workos-5.24.0}/workos/types/list_resource.py +2 -0
  11. {workos-5.22.0 → workos-5.24.0}/workos/types/organizations/organization_domain.py +3 -0
  12. workos-5.24.0/workos/types/vault/__init__.py +2 -0
  13. workos-5.24.0/workos/types/vault/key.py +25 -0
  14. workos-5.24.0/workos/types/vault/object.py +38 -0
  15. {workos-5.22.0 → workos-5.24.0}/workos/user_management.py +4 -0
  16. workos-5.24.0/workos/utils/crypto_provider.py +39 -0
  17. workos-5.24.0/workos/vault.py +515 -0
  18. {workos-5.22.0 → workos-5.24.0}/workos.egg-info/PKG-INFO +1 -1
  19. {workos-5.22.0 → workos-5.24.0}/workos.egg-info/SOURCES.txt +6 -0
  20. {workos-5.22.0 → workos-5.24.0}/LICENSE +0 -0
  21. {workos-5.22.0 → workos-5.24.0}/README.md +0 -0
  22. {workos-5.22.0 → workos-5.24.0}/setup.cfg +0 -0
  23. {workos-5.22.0 → workos-5.24.0}/setup.py +0 -0
  24. {workos-5.22.0 → workos-5.24.0}/tests/test_async_http_client.py +0 -0
  25. {workos-5.22.0 → workos-5.24.0}/tests/test_audit_logs.py +0 -0
  26. {workos-5.22.0 → workos-5.24.0}/tests/test_client.py +0 -0
  27. {workos-5.22.0 → workos-5.24.0}/tests/test_directory_sync.py +0 -0
  28. {workos-5.22.0 → workos-5.24.0}/tests/test_events.py +0 -0
  29. {workos-5.22.0 → workos-5.24.0}/tests/test_fga.py +0 -0
  30. {workos-5.22.0 → workos-5.24.0}/tests/test_mfa.py +0 -0
  31. {workos-5.22.0 → workos-5.24.0}/tests/test_passwordless.py +0 -0
  32. {workos-5.22.0 → workos-5.24.0}/tests/test_portal.py +0 -0
  33. {workos-5.22.0 → workos-5.24.0}/tests/test_session.py +0 -0
  34. {workos-5.22.0 → workos-5.24.0}/tests/test_sso.py +0 -0
  35. {workos-5.22.0 → workos-5.24.0}/tests/test_sync_http_client.py +0 -0
  36. {workos-5.22.0 → workos-5.24.0}/tests/test_webhooks.py +0 -0
  37. {workos-5.22.0 → workos-5.24.0}/tests/test_widgets.py +0 -0
  38. {workos-5.22.0 → workos-5.24.0}/workos/__init__.py +0 -0
  39. {workos-5.22.0 → workos-5.24.0}/workos/_base_client.py +0 -0
  40. {workos-5.22.0 → workos-5.24.0}/workos/_client_configuration.py +0 -0
  41. {workos-5.22.0 → workos-5.24.0}/workos/audit_logs.py +0 -0
  42. {workos-5.22.0 → workos-5.24.0}/workos/directory_sync.py +0 -0
  43. {workos-5.22.0 → workos-5.24.0}/workos/events.py +0 -0
  44. {workos-5.22.0 → workos-5.24.0}/workos/exceptions.py +0 -0
  45. {workos-5.22.0 → workos-5.24.0}/workos/fga.py +0 -0
  46. {workos-5.22.0 → workos-5.24.0}/workos/mfa.py +0 -0
  47. {workos-5.22.0 → workos-5.24.0}/workos/organizations.py +0 -0
  48. {workos-5.22.0 → workos-5.24.0}/workos/passwordless.py +0 -0
  49. {workos-5.22.0 → workos-5.24.0}/workos/portal.py +0 -0
  50. {workos-5.22.0 → workos-5.24.0}/workos/py.typed +0 -0
  51. {workos-5.22.0 → workos-5.24.0}/workos/session.py +0 -0
  52. {workos-5.22.0 → workos-5.24.0}/workos/sso.py +0 -0
  53. {workos-5.22.0 → workos-5.24.0}/workos/types/__init__.py +0 -0
  54. {workos-5.22.0 → workos-5.24.0}/workos/types/audit_logs/__init__.py +0 -0
  55. {workos-5.22.0 → workos-5.24.0}/workos/types/audit_logs/audit_log_event.py +0 -0
  56. {workos-5.22.0 → workos-5.24.0}/workos/types/audit_logs/audit_log_event_actor.py +0 -0
  57. {workos-5.22.0 → workos-5.24.0}/workos/types/audit_logs/audit_log_event_context.py +0 -0
  58. {workos-5.22.0 → workos-5.24.0}/workos/types/audit_logs/audit_log_event_target.py +0 -0
  59. {workos-5.22.0 → workos-5.24.0}/workos/types/audit_logs/audit_log_export.py +0 -0
  60. {workos-5.22.0 → workos-5.24.0}/workos/types/audit_logs/audit_log_metadata.py +0 -0
  61. {workos-5.22.0 → workos-5.24.0}/workos/types/directory_sync/__init__.py +0 -0
  62. {workos-5.22.0 → workos-5.24.0}/workos/types/directory_sync/directory.py +0 -0
  63. {workos-5.22.0 → workos-5.24.0}/workos/types/directory_sync/directory_group.py +0 -0
  64. {workos-5.22.0 → workos-5.24.0}/workos/types/directory_sync/directory_state.py +0 -0
  65. {workos-5.22.0 → workos-5.24.0}/workos/types/directory_sync/directory_type.py +0 -0
  66. {workos-5.22.0 → workos-5.24.0}/workos/types/directory_sync/directory_user.py +0 -0
  67. {workos-5.22.0 → workos-5.24.0}/workos/types/directory_sync/list_filters.py +0 -0
  68. {workos-5.22.0 → workos-5.24.0}/workos/types/events/__init__.py +0 -0
  69. {workos-5.22.0 → workos-5.24.0}/workos/types/events/authentication_payload.py +0 -0
  70. {workos-5.22.0 → workos-5.24.0}/workos/types/events/connection_payload_with_legacy_fields.py +0 -0
  71. {workos-5.22.0 → workos-5.24.0}/workos/types/events/directory_group_membership_payload.py +0 -0
  72. {workos-5.22.0 → workos-5.24.0}/workos/types/events/directory_group_with_previous_attributes.py +0 -0
  73. {workos-5.22.0 → workos-5.24.0}/workos/types/events/directory_payload.py +0 -0
  74. {workos-5.22.0 → workos-5.24.0}/workos/types/events/directory_payload_with_legacy_fields.py +0 -0
  75. {workos-5.22.0 → workos-5.24.0}/workos/types/events/directory_user_with_previous_attributes.py +0 -0
  76. {workos-5.22.0 → workos-5.24.0}/workos/types/events/event_model.py +0 -0
  77. {workos-5.22.0 → workos-5.24.0}/workos/types/events/list_filters.py +0 -0
  78. {workos-5.22.0 → workos-5.24.0}/workos/types/events/organization_domain_verification_failed_payload.py +0 -0
  79. {workos-5.22.0 → workos-5.24.0}/workos/types/events/previous_attributes.py +0 -0
  80. {workos-5.22.0 → workos-5.24.0}/workos/types/events/session_created_payload.py +0 -0
  81. {workos-5.22.0 → workos-5.24.0}/workos/types/fga/__init__.py +0 -0
  82. {workos-5.22.0 → workos-5.24.0}/workos/types/fga/authorization_resource_types.py +0 -0
  83. {workos-5.22.0 → workos-5.24.0}/workos/types/fga/authorization_resources.py +0 -0
  84. {workos-5.22.0 → workos-5.24.0}/workos/types/fga/check.py +0 -0
  85. {workos-5.22.0 → workos-5.24.0}/workos/types/fga/list_filters.py +0 -0
  86. {workos-5.22.0 → workos-5.24.0}/workos/types/fga/warnings.py +0 -0
  87. {workos-5.22.0 → workos-5.24.0}/workos/types/fga/warrant.py +0 -0
  88. {workos-5.22.0 → workos-5.24.0}/workos/types/metadata.py +0 -0
  89. {workos-5.22.0 → workos-5.24.0}/workos/types/mfa/__init__.py +0 -0
  90. {workos-5.22.0 → workos-5.24.0}/workos/types/mfa/authentication_challenge.py +0 -0
  91. {workos-5.22.0 → workos-5.24.0}/workos/types/mfa/authentication_challenge_verification_response.py +0 -0
  92. {workos-5.22.0 → workos-5.24.0}/workos/types/mfa/authentication_factor.py +0 -0
  93. {workos-5.22.0 → workos-5.24.0}/workos/types/mfa/authentication_factor_totp_and_challenge_response.py +0 -0
  94. {workos-5.22.0 → workos-5.24.0}/workos/types/mfa/enroll_authentication_factor_type.py +0 -0
  95. {workos-5.22.0 → workos-5.24.0}/workos/types/organizations/__init__.py +0 -0
  96. {workos-5.22.0 → workos-5.24.0}/workos/types/organizations/domain_data_input.py +0 -0
  97. {workos-5.22.0 → workos-5.24.0}/workos/types/organizations/list_filters.py +0 -0
  98. {workos-5.22.0 → workos-5.24.0}/workos/types/organizations/organization.py +0 -0
  99. {workos-5.22.0 → workos-5.24.0}/workos/types/organizations/organization_common.py +0 -0
  100. {workos-5.22.0 → workos-5.24.0}/workos/types/passwordless/__init__.py +0 -0
  101. {workos-5.22.0 → workos-5.24.0}/workos/types/passwordless/passwordless_session.py +0 -0
  102. {workos-5.22.0 → workos-5.24.0}/workos/types/passwordless/passwordless_session_type.py +0 -0
  103. {workos-5.22.0 → workos-5.24.0}/workos/types/portal/__init__.py +0 -0
  104. {workos-5.22.0 → workos-5.24.0}/workos/types/portal/portal_link.py +0 -0
  105. {workos-5.22.0 → workos-5.24.0}/workos/types/portal/portal_link_intent.py +0 -0
  106. {workos-5.22.0 → workos-5.24.0}/workos/types/portal/portal_link_intent_options.py +0 -0
  107. {workos-5.22.0 → workos-5.24.0}/workos/types/roles/__init__.py +0 -0
  108. {workos-5.22.0 → workos-5.24.0}/workos/types/roles/role.py +0 -0
  109. {workos-5.22.0 → workos-5.24.0}/workos/types/sso/__init__.py +0 -0
  110. {workos-5.22.0 → workos-5.24.0}/workos/types/sso/connection.py +0 -0
  111. {workos-5.22.0 → workos-5.24.0}/workos/types/sso/connection_domain.py +0 -0
  112. {workos-5.22.0 → workos-5.24.0}/workos/types/sso/profile.py +0 -0
  113. {workos-5.22.0 → workos-5.24.0}/workos/types/sso/sso_provider_type.py +0 -0
  114. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/__init__.py +0 -0
  115. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/authenticate_with_common.py +0 -0
  116. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/authentication_response.py +0 -0
  117. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/email_verification.py +0 -0
  118. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/impersonator.py +0 -0
  119. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/invitation.py +0 -0
  120. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/list_filters.py +0 -0
  121. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/magic_auth.py +0 -0
  122. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/oauth_tokens.py +0 -0
  123. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/organization_membership.py +0 -0
  124. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/password_hash_type.py +0 -0
  125. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/password_reset.py +0 -0
  126. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/screen_hint.py +0 -0
  127. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/session.py +0 -0
  128. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/user.py +0 -0
  129. {workos-5.22.0 → workos-5.24.0}/workos/types/user_management/user_management_provider_type.py +0 -0
  130. {workos-5.22.0 → workos-5.24.0}/workos/types/webhooks/__init__.py +0 -0
  131. {workos-5.22.0 → workos-5.24.0}/workos/types/webhooks/webhook.py +0 -0
  132. {workos-5.22.0 → workos-5.24.0}/workos/types/webhooks/webhook_model.py +0 -0
  133. {workos-5.22.0 → workos-5.24.0}/workos/types/webhooks/webhook_payload.py +0 -0
  134. {workos-5.22.0 → workos-5.24.0}/workos/types/widgets/__init__.py +0 -0
  135. {workos-5.22.0 → workos-5.24.0}/workos/types/widgets/widget_scope.py +0 -0
  136. {workos-5.22.0 → workos-5.24.0}/workos/types/widgets/widget_token_response.py +0 -0
  137. {workos-5.22.0 → workos-5.24.0}/workos/types/workos_model.py +0 -0
  138. {workos-5.22.0 → workos-5.24.0}/workos/typing/__init__.py +0 -0
  139. {workos-5.22.0 → workos-5.24.0}/workos/typing/literals.py +0 -0
  140. {workos-5.22.0 → workos-5.24.0}/workos/typing/sync_or_async.py +0 -0
  141. {workos-5.22.0 → workos-5.24.0}/workos/typing/untyped_literal.py +0 -0
  142. {workos-5.22.0 → workos-5.24.0}/workos/typing/webhooks.py +0 -0
  143. {workos-5.22.0 → workos-5.24.0}/workos/utils/__init__.py +0 -0
  144. {workos-5.22.0 → workos-5.24.0}/workos/utils/_base_http_client.py +0 -0
  145. {workos-5.22.0 → workos-5.24.0}/workos/utils/http_client.py +0 -0
  146. {workos-5.22.0 → workos-5.24.0}/workos/utils/pagination_order.py +0 -0
  147. {workos-5.22.0 → workos-5.24.0}/workos/utils/request_helper.py +0 -0
  148. {workos-5.22.0 → workos-5.24.0}/workos/webhooks.py +0 -0
  149. {workos-5.22.0 → workos-5.24.0}/workos/widgets.py +0 -0
  150. {workos-5.22.0 → workos-5.24.0}/workos.egg-info/dependency_links.txt +0 -0
  151. {workos-5.22.0 → workos-5.24.0}/workos.egg-info/not-zip-safe +0 -0
  152. {workos-5.22.0 → workos-5.24.0}/workos.egg-info/requires.txt +0 -0
  153. {workos-5.22.0 → workos-5.24.0}/workos.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: workos
3
- Version: 5.22.0
3
+ Version: 5.24.0
4
4
  Summary: WorkOS Python Client
5
5
  Home-page: https://github.com/workos-inc/workos-python
6
6
  Author: WorkOS
@@ -38,6 +38,8 @@ class TestOrganizations:
38
38
  "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
39
39
  "verification_strategy": "dns",
40
40
  "verification_token": "token",
41
+ "created_at": datetime.datetime.now().isoformat(),
42
+ "updated_at": datetime.datetime.now().isoformat(),
41
43
  }
42
44
  ],
43
45
  }
@@ -189,15 +191,17 @@ class TestOrganizations:
189
191
  }
190
192
  assert updated_organization.id == "org_01EHT88Z8J8795GZNQ4ZP1J81T"
191
193
  assert updated_organization.name == "Example Organization"
192
- assert updated_organization.domains[0].dict() == {
193
- "domain": "example.io",
194
- "object": "organization_domain",
195
- "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8",
196
- "state": "verified",
197
- "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
198
- "verification_strategy": "dns",
199
- "verification_token": "token",
200
- }
194
+ domain = updated_organization.domains[0]
195
+ assert domain.domain == "example.io"
196
+ assert domain.object == "organization_domain"
197
+ assert domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
198
+ assert domain.state == "verified"
199
+ assert domain.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T"
200
+ assert domain.verification_strategy == "dns"
201
+ assert domain.verification_token == "token"
202
+ assert domain.verification_prefix is None
203
+ assert isinstance(domain.created_at, str)
204
+ assert isinstance(domain.updated_at, str)
201
205
 
202
206
  def test_delete_organization(self, capture_and_mock_http_client_request):
203
207
  request_kwargs = capture_and_mock_http_client_request(
@@ -344,6 +344,29 @@ class TestUserManagementBase(UserManagementFixtures):
344
344
  "provider": "authkit",
345
345
  }
346
346
 
347
+ def test_authorization_url_has_expected_query_params_with_provider_scopes(self):
348
+ provider = "GoogleOAuth"
349
+ provider_scopes = [
350
+ "https://www.googleapis.com/auth/calendar",
351
+ "https://www.googleapis.com/auth/admin.directory.group",
352
+ ]
353
+ redirect_uri = "https://localhost/auth/callback"
354
+ authorization_url = self.user_management.get_authorization_url(
355
+ provider=provider,
356
+ provider_scopes=provider_scopes,
357
+ redirect_uri=redirect_uri,
358
+ )
359
+
360
+ parsed_url = urlparse(authorization_url)
361
+ assert parsed_url.path == "/user_management/authorize"
362
+ assert dict(parse_qsl(str(parsed_url.query))) == {
363
+ "provider": provider,
364
+ "provider_scopes": ",".join(provider_scopes),
365
+ "client_id": self.http_client.client_id,
366
+ "redirect_uri": redirect_uri,
367
+ "response_type": RESPONSE_TYPE_CODE,
368
+ }
369
+
347
370
  def test_get_jwks_url(self):
348
371
  expected = "%ssso/jwks/%s" % (
349
372
  self.http_client.base_url,
@@ -0,0 +1,460 @@
1
+ import pytest
2
+ from tests.utils.fixtures.mock_vault_object import (
3
+ MockVaultObject,
4
+ MockObjectVersion,
5
+ MockObjectDigest,
6
+ MockObjectMetadata,
7
+ )
8
+ from tests.utils.list_resource import list_response_of
9
+ from tests.utils.syncify import syncify
10
+ from workos.vault import Vault
11
+ from workos.types.vault.key import KeyContext
12
+
13
+
14
+ class TestVault:
15
+ @pytest.fixture(autouse=True)
16
+ def setup(self, sync_http_client_for_test):
17
+ self.http_client = sync_http_client_for_test
18
+ self.vault = Vault(http_client=self.http_client)
19
+
20
+ @pytest.fixture
21
+ def mock_vault_object(self):
22
+ return MockVaultObject(
23
+ "vault_01234567890abcdef", "test-secret", "secret-value"
24
+ ).dict()
25
+
26
+ @pytest.fixture
27
+ def mock_object_digest(self):
28
+ return MockObjectDigest("vault_01234567890abcdef", "test-secret").dict()
29
+
30
+ @pytest.fixture
31
+ def mock_object_metadata(self):
32
+ return MockObjectMetadata("vault_01234567890abcdef").dict()
33
+
34
+ @pytest.fixture
35
+ def mock_vault_object_no_value(self):
36
+ mock_obj = MockVaultObject("vault_01234567890abcdef", "test-secret")
37
+ mock_obj.value = None
38
+ return mock_obj.dict()
39
+
40
+ @pytest.fixture
41
+ def mock_vault_objects_list(self):
42
+ vault_objects = [
43
+ MockObjectDigest(f"vault_{i}", f"secret-{i}").dict() for i in range(5)
44
+ ]
45
+ return {
46
+ "data": vault_objects,
47
+ "list_metadata": {"before": None, "after": None},
48
+ "object": "list",
49
+ }
50
+
51
+ @pytest.fixture
52
+ def mock_vault_objects_multiple_pages(self):
53
+ vault_objects = [
54
+ MockObjectDigest(f"vault_{i}", f"secret-{i}").dict() for i in range(25)
55
+ ]
56
+ return list_response_of(data=vault_objects)
57
+
58
+ @pytest.fixture
59
+ def mock_object_versions(self):
60
+ versions = [
61
+ MockObjectVersion(f"version_{i}", current_version=(i == 0)).dict()
62
+ for i in range(3)
63
+ ]
64
+ return {"data": versions}
65
+
66
+ @pytest.fixture
67
+ def mock_data_key(self):
68
+ return {
69
+ "id": "key_01234567890abcdef",
70
+ "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
71
+ }
72
+
73
+ @pytest.fixture
74
+ def mock_data_key_pair(self):
75
+ return {
76
+ "context": {"key": "test-key"},
77
+ "id": "key_01234567890abcdef",
78
+ "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
79
+ "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==",
80
+ }
81
+
82
+ def test_read_object_success(
83
+ self, mock_vault_object, capture_and_mock_http_client_request
84
+ ):
85
+ request_kwargs = capture_and_mock_http_client_request(
86
+ self.http_client, mock_vault_object, 200
87
+ )
88
+
89
+ vault_object = self.vault.read_object(object_id="vault_01234567890abcdef")
90
+
91
+ assert request_kwargs["method"] == "get"
92
+ assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef")
93
+ assert vault_object.id == "vault_01234567890abcdef"
94
+ assert vault_object.name == "test-secret"
95
+ assert vault_object.value == "secret-value"
96
+ assert vault_object.metadata.environment_id == "env_01234567890abcdef"
97
+
98
+ def test_read_object_missing_object_id(self):
99
+ with pytest.raises(
100
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
101
+ ):
102
+ self.vault.read_object(object_id="")
103
+
104
+ def test_read_object_none_object_id(self):
105
+ with pytest.raises(
106
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
107
+ ):
108
+ self.vault.read_object(object_id=None)
109
+
110
+ def test_list_objects_default_params(
111
+ self, mock_vault_objects_list, capture_and_mock_http_client_request
112
+ ):
113
+ request_kwargs = capture_and_mock_http_client_request(
114
+ self.http_client, mock_vault_objects_list, 200
115
+ )
116
+
117
+ vault_objects = self.vault.list_objects()
118
+
119
+ assert request_kwargs["method"] == "get"
120
+ assert request_kwargs["url"].endswith("/vault/v1/kv")
121
+ assert request_kwargs["params"]["limit"] == 10
122
+ assert "before" not in request_kwargs["params"]
123
+ assert "after" not in request_kwargs["params"]
124
+ assert len(vault_objects.data) == 5
125
+ assert vault_objects.data[0].id == "vault_0"
126
+ assert vault_objects.data[0].name == "secret-0"
127
+
128
+ def test_list_objects_with_params(
129
+ self, mock_vault_objects_list, capture_and_mock_http_client_request
130
+ ):
131
+ request_kwargs = capture_and_mock_http_client_request(
132
+ self.http_client, mock_vault_objects_list, 200
133
+ )
134
+
135
+ vault_objects = self.vault.list_objects(
136
+ limit=5, before="vault_before", after="vault_after"
137
+ )
138
+
139
+ assert request_kwargs["method"] == "get"
140
+ assert request_kwargs["url"].endswith("/vault/v1/kv")
141
+ assert request_kwargs["params"]["limit"] == 5
142
+ assert request_kwargs["params"]["before"] == "vault_before"
143
+ assert request_kwargs["params"]["after"] == "vault_after"
144
+
145
+ def test_list_objects_auto_pagination(
146
+ self, mock_vault_objects_multiple_pages, test_auto_pagination
147
+ ):
148
+ test_auto_pagination(
149
+ http_client=self.http_client,
150
+ list_function=self.vault.list_objects,
151
+ expected_all_page_data=mock_vault_objects_multiple_pages["data"],
152
+ )
153
+
154
+ def test_list_object_versions_success(
155
+ self, mock_object_versions, capture_and_mock_http_client_request
156
+ ):
157
+ request_kwargs = capture_and_mock_http_client_request(
158
+ self.http_client, mock_object_versions, 200
159
+ )
160
+
161
+ versions = self.vault.list_object_versions(object_id="vault_01234567890abcdef")
162
+
163
+ assert request_kwargs["method"] == "get"
164
+ assert request_kwargs["url"].endswith(
165
+ "/vault/v1/kv/vault_01234567890abcdef/versions"
166
+ )
167
+ assert len(versions) == 3
168
+ assert versions[0].id == "version_0"
169
+ assert versions[0].current_version is True
170
+ assert versions[1].current_version is False
171
+
172
+ def test_list_object_versions_empty_data(
173
+ self, capture_and_mock_http_client_request
174
+ ):
175
+ request_kwargs = capture_and_mock_http_client_request(
176
+ self.http_client, {"data": []}, 200
177
+ )
178
+
179
+ versions = self.vault.list_object_versions(object_id="vault_01234567890abcdef")
180
+
181
+ assert request_kwargs["method"] == "get"
182
+ assert len(versions) == 0
183
+
184
+ def test_create_object_success(
185
+ self, mock_object_metadata, capture_and_mock_http_client_request
186
+ ):
187
+ request_kwargs = capture_and_mock_http_client_request(
188
+ self.http_client, mock_object_metadata, 200
189
+ )
190
+
191
+ object_metadata = self.vault.create_object(
192
+ name="test-secret",
193
+ value="secret-value",
194
+ key_context=KeyContext({"key": "test-key"}),
195
+ )
196
+
197
+ assert request_kwargs["method"] == "post"
198
+ assert request_kwargs["url"].endswith("/vault/v1/kv")
199
+ assert request_kwargs["json"]["name"] == "test-secret"
200
+ assert request_kwargs["json"]["value"] == "secret-value"
201
+ assert request_kwargs["json"]["key_context"] == KeyContext({"key": "test-key"})
202
+ assert object_metadata.id == "vault_01234567890abcdef"
203
+
204
+ def test_create_object_missing_name(self):
205
+ with pytest.raises(
206
+ ValueError,
207
+ match="Incomplete arguments: 'name' and 'value' are required arguments",
208
+ ):
209
+ self.vault.create_object(
210
+ name="",
211
+ value="secret-value",
212
+ key_context=KeyContext({"key": "test-key"}),
213
+ )
214
+
215
+ def test_create_object_missing_value(self):
216
+ with pytest.raises(
217
+ ValueError,
218
+ match="Incomplete arguments: 'name' and 'value' are required arguments",
219
+ ):
220
+ self.vault.create_object(
221
+ name="test-secret",
222
+ value="",
223
+ key_context=KeyContext({"key": "test-key"}),
224
+ )
225
+
226
+ def test_create_object_missing_both(self):
227
+ with pytest.raises(
228
+ ValueError,
229
+ match="Incomplete arguments: 'name' and 'value' are required arguments",
230
+ ):
231
+ self.vault.create_object(
232
+ name="", value="", key_context=KeyContext({"key": "test-key"})
233
+ )
234
+
235
+ def test_update_object_with_value(
236
+ self, mock_vault_object, capture_and_mock_http_client_request
237
+ ):
238
+ request_kwargs = capture_and_mock_http_client_request(
239
+ self.http_client, mock_vault_object, 200
240
+ )
241
+
242
+ vault_object = self.vault.update_object(
243
+ object_id="vault_01234567890abcdef",
244
+ value="updated-value",
245
+ )
246
+
247
+ assert request_kwargs["method"] == "put"
248
+ assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef")
249
+ assert request_kwargs["json"]["value"] == "updated-value"
250
+ assert "version_check" not in request_kwargs["json"]
251
+ assert vault_object.id == "vault_01234567890abcdef"
252
+
253
+ def test_update_object_with_version_check(
254
+ self, mock_vault_object, capture_and_mock_http_client_request
255
+ ):
256
+ request_kwargs = capture_and_mock_http_client_request(
257
+ self.http_client, mock_vault_object, 200
258
+ )
259
+
260
+ vault_object = self.vault.update_object(
261
+ object_id="vault_01234567890abcdef",
262
+ value="updated-value",
263
+ version_check="version_123",
264
+ )
265
+
266
+ assert request_kwargs["method"] == "put"
267
+ assert request_kwargs["json"]["value"] == "updated-value"
268
+ assert request_kwargs["json"]["version_check"] == "version_123"
269
+
270
+ def test_update_object_missing_value(self):
271
+ with pytest.raises(
272
+ TypeError, match="missing 1 required keyword-only argument: 'value'"
273
+ ):
274
+ self.vault.update_object(object_id="vault_01234567890abcdef")
275
+
276
+ def test_update_object_missing_object_id(self):
277
+ with pytest.raises(
278
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
279
+ ):
280
+ self.vault.update_object(object_id="", value="test-value")
281
+
282
+ def test_update_object_none_object_id(self):
283
+ with pytest.raises(
284
+ ValueError,
285
+ match="Incomplete arguments: 'object_id' is a required argument",
286
+ ):
287
+ self.vault.update_object(object_id=None, value="updated-value")
288
+
289
+ def test_delete_object_success(self, capture_and_mock_http_client_request):
290
+ request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 204)
291
+
292
+ result = self.vault.delete_object(object_id="vault_01234567890abcdef")
293
+
294
+ assert request_kwargs["method"] == "delete"
295
+ assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef")
296
+ assert result is None
297
+
298
+ def test_delete_object_missing_object_id(self):
299
+ with pytest.raises(
300
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
301
+ ):
302
+ self.vault.delete_object(object_id="")
303
+
304
+ def test_delete_object_none_object_id(self):
305
+ with pytest.raises(
306
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
307
+ ):
308
+ self.vault.delete_object(object_id=None)
309
+
310
+ def test_create_data_key_success(
311
+ self, mock_data_key_pair, capture_and_mock_http_client_request
312
+ ):
313
+ request_kwargs = capture_and_mock_http_client_request(
314
+ self.http_client, mock_data_key_pair, 200
315
+ )
316
+
317
+ data_key_pair = self.vault.create_data_key(
318
+ key_context=KeyContext({"key": "test-key"})
319
+ )
320
+
321
+ assert request_kwargs["method"] == "post"
322
+ assert request_kwargs["url"].endswith("/vault/v1/keys/data-key")
323
+ assert request_kwargs["json"]["context"] == KeyContext({"key": "test-key"})
324
+ assert data_key_pair.data_key.id == "key_01234567890abcdef"
325
+ assert data_key_pair.encrypted_keys == "ZW5jcnlwdGVkX2tleXNfZGF0YQ=="
326
+
327
+ def test_decrypt_data_key_success(
328
+ self, mock_data_key, capture_and_mock_http_client_request
329
+ ):
330
+ request_kwargs = capture_and_mock_http_client_request(
331
+ self.http_client, mock_data_key, 200
332
+ )
333
+
334
+ data_key = self.vault.decrypt_data_key(keys="ZW5jcnlwdGVkX2tleXNfZGF0YQ==")
335
+
336
+ assert request_kwargs["method"] == "post"
337
+ assert request_kwargs["url"].endswith("/vault/v1/keys/decrypt")
338
+ assert request_kwargs["json"]["keys"] == "ZW5jcnlwdGVkX2tleXNfZGF0YQ=="
339
+ assert data_key.id == "key_01234567890abcdef"
340
+ assert data_key.key == "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
341
+
342
+ def test_encrypt_success(
343
+ self, mock_data_key_pair, capture_and_mock_http_client_request
344
+ ):
345
+ # Mock the create_data_key call
346
+ request_kwargs = capture_and_mock_http_client_request(
347
+ self.http_client, mock_data_key_pair, 200
348
+ )
349
+
350
+ plaintext = "Hello, World!"
351
+ context = KeyContext({"key": "test-key"})
352
+
353
+ encrypted_data = self.vault.encrypt(data=plaintext, key_context=context)
354
+
355
+ # Verify create_data_key was called
356
+ assert request_kwargs["method"] == "post"
357
+ assert request_kwargs["url"].endswith("/vault/v1/keys/data-key")
358
+ assert request_kwargs["json"]["context"] == KeyContext({"key": "test-key"})
359
+
360
+ # Verify we got encrypted data back
361
+ assert isinstance(encrypted_data, str)
362
+ assert len(encrypted_data) > 0
363
+
364
+ def test_encrypt_with_associated_data(
365
+ self, mock_data_key_pair, capture_and_mock_http_client_request
366
+ ):
367
+ # Mock the create_data_key call
368
+ capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200)
369
+
370
+ plaintext = "Hello, World!"
371
+ context = KeyContext({"key": "test-key"})
372
+ associated_data = "additional-context"
373
+
374
+ encrypted_data = self.vault.encrypt(
375
+ data=plaintext, key_context=context, associated_data=associated_data
376
+ )
377
+
378
+ # Verify we got encrypted data back
379
+ assert isinstance(encrypted_data, str)
380
+ assert len(encrypted_data) > 0
381
+
382
+ def test_decrypt_success(self, mock_data_key, capture_and_mock_http_client_request):
383
+ # First encrypt some data to get a valid encrypted payload
384
+ mock_data_key_pair = {
385
+ "context": {"key": "test-key"},
386
+ "id": "key_01234567890abcdef",
387
+ "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
388
+ "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==",
389
+ }
390
+
391
+ # Mock create_data_key for encryption
392
+ capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200)
393
+
394
+ plaintext = "Hello, World!"
395
+ context = KeyContext({"key": "test-key"})
396
+ encrypted_data = self.vault.encrypt(data=plaintext, key_context=context)
397
+
398
+ # Now mock decrypt_data_key for decryption
399
+ capture_and_mock_http_client_request(self.http_client, mock_data_key, 200)
400
+
401
+ # Decrypt the data
402
+ decrypted_text = self.vault.decrypt(encrypted_data=encrypted_data)
403
+
404
+ # Verify decryption worked
405
+ assert decrypted_text == plaintext
406
+
407
+ def test_decrypt_with_associated_data(
408
+ self, mock_data_key, capture_and_mock_http_client_request
409
+ ):
410
+ # First encrypt some data with associated data
411
+ mock_data_key_pair = {
412
+ "context": {"key": "test-key"},
413
+ "id": "key_01234567890abcdef",
414
+ "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
415
+ "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==",
416
+ }
417
+
418
+ # Mock create_data_key for encryption
419
+ capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200)
420
+
421
+ plaintext = "Hello, World!"
422
+ context = KeyContext({"key": "test-key"})
423
+ associated_data = "additional-context"
424
+ encrypted_data = self.vault.encrypt(
425
+ data=plaintext, key_context=context, associated_data=associated_data
426
+ )
427
+
428
+ # Now mock decrypt_data_key for decryption
429
+ capture_and_mock_http_client_request(self.http_client, mock_data_key, 200)
430
+
431
+ # Decrypt the data with the same associated data
432
+ decrypted_text = self.vault.decrypt(
433
+ encrypted_data=encrypted_data, associated_data=associated_data
434
+ )
435
+
436
+ # Verify decryption worked
437
+ assert decrypted_text == plaintext
438
+
439
+ def test_encrypt_decrypt_roundtrip(
440
+ self, mock_data_key_pair, mock_data_key, capture_and_mock_http_client_request
441
+ ):
442
+ """Test that encrypt/decrypt works correctly together"""
443
+
444
+ # Mock create_data_key for encryption
445
+ capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200)
446
+
447
+ plaintext = "This is a test message for encryption!"
448
+ context = KeyContext({"env": "test", "service": "vault"})
449
+
450
+ # Encrypt the data
451
+ encrypted_data = self.vault.encrypt(data=plaintext, key_context=context)
452
+
453
+ # Mock decrypt_data_key for decryption
454
+ capture_and_mock_http_client_request(self.http_client, mock_data_key, 200)
455
+
456
+ # Decrypt the data
457
+ decrypted_text = self.vault.decrypt(encrypted_data=encrypted_data)
458
+
459
+ # Verify roundtrip worked
460
+ assert decrypted_text == plaintext
@@ -12,7 +12,7 @@ __package_name__ = "workos"
12
12
 
13
13
  __package_url__ = "https://github.com/workos-inc/workos-python"
14
14
 
15
- __version__ = "5.22.0"
15
+ __version__ = "5.24.0"
16
16
 
17
17
  __author__ = "WorkOS"
18
18
 
@@ -14,6 +14,7 @@ from workos.user_management import AsyncUserManagement
14
14
  from workos.utils.http_client import AsyncHTTPClient
15
15
  from workos.webhooks import WebhooksModule
16
16
  from workos.widgets import WidgetsModule
17
+ from workos.vault import VaultModule
17
18
 
18
19
 
19
20
  class AsyncClient(BaseClient):
@@ -112,3 +113,9 @@ class AsyncClient(BaseClient):
112
113
  raise NotImplementedError(
113
114
  "Widgets APIs are not yet supported in the async client."
114
115
  )
116
+
117
+ @property
118
+ def vault(self) -> VaultModule:
119
+ raise NotImplementedError(
120
+ "Vault APIs are not yet supported in the async client."
121
+ )
@@ -14,6 +14,7 @@ from workos.events import Events
14
14
  from workos.user_management import UserManagement
15
15
  from workos.utils.http_client import SyncHTTPClient
16
16
  from workos.widgets import Widgets
17
+ from workos.vault import Vault
17
18
 
18
19
 
19
20
  class SyncClient(BaseClient):
@@ -116,3 +117,9 @@ class SyncClient(BaseClient):
116
117
  if not getattr(self, "_widgets", None):
117
118
  self._widgets = Widgets(http_client=self._http_client)
118
119
  return self._widgets
120
+
121
+ @property
122
+ def vault(self) -> Vault:
123
+ if not getattr(self, "_vault", None):
124
+ self._vault = Vault(http_client=self._http_client)
125
+ return self._vault
@@ -193,6 +193,18 @@ class OrganizationDomainVerifiedEvent(EventModel[OrganizationDomain]):
193
193
  event: Literal["organization_domain.verified"]
194
194
 
195
195
 
196
+ class OrganizationDomainCreatedEvent(EventModel[OrganizationDomain]):
197
+ event: Literal["organization_domain.created"]
198
+
199
+
200
+ class OrganizationDomainUpdatedEvent(EventModel[OrganizationDomain]):
201
+ event: Literal["organization_domain.updated"]
202
+
203
+
204
+ class OrganizationDomainDeletedEvent(EventModel[OrganizationDomain]):
205
+ event: Literal["organization_domain.deleted"]
206
+
207
+
196
208
  class OrganizationMembershipCreatedEvent(EventModel[OrganizationMembership]):
197
209
  event: Literal["organization_membership.created"]
198
210
 
@@ -272,6 +284,9 @@ Event = Annotated[
272
284
  OrganizationCreatedEvent,
273
285
  OrganizationDeletedEvent,
274
286
  OrganizationUpdatedEvent,
287
+ OrganizationDomainCreatedEvent,
288
+ OrganizationDomainDeletedEvent,
289
+ OrganizationDomainUpdatedEvent,
275
290
  OrganizationDomainVerificationFailedEvent,
276
291
  OrganizationDomainVerifiedEvent,
277
292
  OrganizationMembershipCreatedEvent,
@@ -36,6 +36,9 @@ EventType = Literal[
36
36
  "organization.updated",
37
37
  "organization_domain.verification_failed",
38
38
  "organization_domain.verified",
39
+ "organization_domain.created",
40
+ "organization_domain.deleted",
41
+ "organization_domain.updated",
39
42
  "organization_membership.created",
40
43
  "organization_membership.deleted",
41
44
  "organization_membership.updated",
@@ -33,6 +33,7 @@ from workos.types.mfa import AuthenticationFactor
33
33
  from workos.types.organizations import Organization
34
34
  from workos.types.sso import ConnectionWithDomains
35
35
  from workos.types.user_management import Invitation, OrganizationMembership, User
36
+ from workos.types.vault import ObjectDigest
36
37
  from workos.types.workos_model import WorkOSModel
37
38
  from workos.utils.request_helper import DEFAULT_LIST_RESPONSE_LIMIT
38
39
 
@@ -51,6 +52,7 @@ ListableResource = TypeVar(
51
52
  AuthorizationResource,
52
53
  AuthorizationResourceType,
53
54
  User,
55
+ ObjectDigest,
54
56
  Warrant,
55
57
  WarrantQueryResult,
56
58
  )
@@ -13,3 +13,6 @@ class OrganizationDomain(WorkOSModel):
13
13
  ] = None
14
14
  verification_strategy: Optional[LiteralOrUntyped[Literal["manual", "dns"]]] = None
15
15
  verification_token: Optional[str] = None
16
+ verification_prefix: Optional[str] = None
17
+ created_at: str
18
+ updated_at: str
@@ -0,0 +1,2 @@
1
+ from .key import *
2
+ from .object import *
@@ -0,0 +1,25 @@
1
+ from typing import Dict
2
+ from pydantic import BaseModel, RootModel
3
+ from workos.types.workos_model import WorkOSModel
4
+
5
+
6
+ class KeyContext(RootModel[Dict[str, str]]):
7
+ pass
8
+
9
+
10
+ class DataKey(WorkOSModel):
11
+ id: str
12
+ key: str
13
+
14
+
15
+ class DataKeyPair(WorkOSModel):
16
+ context: KeyContext
17
+ data_key: DataKey
18
+ encrypted_keys: str
19
+
20
+
21
+ class DecodedKeys(BaseModel):
22
+ iv: bytes
23
+ tag: bytes
24
+ keys: str # Base64-encoded string
25
+ ciphertext: bytes