hookbase 3.1.0__tar.gz → 3.2.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.
- {hookbase-3.1.0 → hookbase-3.2.0}/PKG-INFO +27 -5
- {hookbase-3.1.0 → hookbase-3.2.0}/README.md +26 -4
- hookbase-3.2.0/src/hookbase/_version.py +1 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/client.py +8 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/__init__.py +27 -0
- hookbase-3.2.0/src/hookbase/models/catalog.py +120 -0
- hookbase-3.2.0/src/hookbase/models/sources.py +283 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/__init__.py +2 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/events.py +20 -0
- hookbase-3.2.0/src/hookbase/resources/providers.py +54 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/sources.py +82 -2
- hookbase-3.2.0/tests/resources/test_source_contract.py +302 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_source_fields.py +55 -2
- hookbase-3.1.0/src/hookbase/_version.py +0 -1
- hookbase-3.1.0/src/hookbase/models/sources.py +0 -180
- {hookbase-3.1.0 → hookbase-3.2.0}/.github/workflows/python-publish.yml +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/.gitignore +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/LICENSE +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/examples/async_usage.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/examples/basic_inbound.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/examples/error_handling.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/examples/send_webhooks.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/examples/verify_webhooks.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/pyproject.toml +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/__init__.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/_client.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/_constants.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/_pagination.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/errors.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/_base.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/_wire.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/analytics.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/api_keys.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/applications.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/common.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/cron_jobs.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/deliveries.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/destinations.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/dlq.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/endpoints.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/event_types.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/events.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/filters.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/messages.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/organizations.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/portal_tokens.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/routes.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/schemas.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/subscriptions.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/transforms.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/tunnels.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/py.typed +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/_base.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/analytics.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/api_keys.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/applications.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/cron_jobs.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/deliveries.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/destinations.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/dlq.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/endpoints.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/event_types.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/filters.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/message_log.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/messages.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/organizations.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/portal_tokens.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/routes.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/schemas.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/subscriptions.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/transforms.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/tunnels.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/webhook.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/__init__.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/conftest.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/__init__.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_applications.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_cron_jobs.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_destinations.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_dlq.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_endpoints.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_messages.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_sources.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_client.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_errors.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_pagination.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_slug_contract.py +0 -0
- {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hookbase
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Official Python SDK for the Hookbase webhook management platform
|
|
5
5
|
Project-URL: Homepage, https://hookbase.app
|
|
6
6
|
Project-URL: Documentation, https://www.hookbase.app/docs/send/sdk
|
|
@@ -98,7 +98,26 @@ client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"}
|
|
|
98
98
|
client.sources.get("src_id")
|
|
99
99
|
client.sources.update("src_id", {"name": "Updated"})
|
|
100
100
|
client.sources.delete("src_id")
|
|
101
|
-
client.sources.rotate_secret("src_id")
|
|
101
|
+
client.sources.rotate_secret("src_id") # 400 for vendor-issued/public-key/built-in secrets
|
|
102
|
+
client.sources.reveal_secret("src_id")
|
|
103
|
+
|
|
104
|
+
# A provider that verifies with no secret: signing_secret comes back None
|
|
105
|
+
wise = client.sources.create({"name": "Wise", "slug": "wise", "provider": "wise"})
|
|
106
|
+
assert wise.verifies_without_secret and wise.signing_secret is None
|
|
107
|
+
|
|
108
|
+
# WhatsApp (Meta) sources also carry a verify token
|
|
109
|
+
wa = client.sources.create({"name": "WhatsApp", "slug": "whatsapp", "provider": "whatsapp"})
|
|
110
|
+
print(wa.verify_token)
|
|
111
|
+
client.sources.rotate_verify_token(wa.id)
|
|
112
|
+
|
|
113
|
+
# Notion parks its verification token until you adopt it
|
|
114
|
+
pending = client.sources.get_pending_verification("notion")
|
|
115
|
+
client.sources.adopt_pending_verification("notion")
|
|
116
|
+
|
|
117
|
+
# Provider catalog: which secret each provider needs (public, no auth needed)
|
|
118
|
+
for p in client.providers.list(category="payments").providers:
|
|
119
|
+
print(p.hookbase_provider, p.signature_verification and p.signature_verification.secret_source)
|
|
120
|
+
client.providers.get("stripe").signature_verification
|
|
102
121
|
|
|
103
122
|
# Destinations
|
|
104
123
|
client.destinations.list()
|
|
@@ -111,7 +130,7 @@ client.routes.get_circuit_status("route_id")
|
|
|
111
130
|
client.routes.reset_circuit("route_id")
|
|
112
131
|
|
|
113
132
|
# Events & Deliveries
|
|
114
|
-
client.events.list(source_id="src_id", from_date="2024-01-01")
|
|
133
|
+
client.events.list(source_id="src_id", from_date="2024-01-01") # id or slug; unknown -> 404
|
|
115
134
|
client.events.get("evt_id") # Includes payload and deliveries
|
|
116
135
|
client.deliveries.replay("del_id")
|
|
117
136
|
client.deliveries.bulk_replay(["del_1", "del_2"])
|
|
@@ -252,11 +271,14 @@ All responses are typed Pydantic models with camelCase alias support:
|
|
|
252
271
|
from hookbase.models import Source, CreateSourceParams
|
|
253
272
|
|
|
254
273
|
# Use Pydantic models for type safety
|
|
255
|
-
params = CreateSourceParams(name="GitHub", provider="github")
|
|
274
|
+
params = CreateSourceParams(name="GitHub", slug="github", provider="github")
|
|
256
275
|
source = client.sources.create(params)
|
|
257
276
|
|
|
258
277
|
# Or use plain dicts
|
|
259
|
-
source = client.sources.create({"name": "GitHub", "provider": "github"})
|
|
278
|
+
source = client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"})
|
|
279
|
+
|
|
280
|
+
# `provider` is any string (hookbase.models.KNOWN_SOURCE_PROVIDERS lists today's values), so a
|
|
281
|
+
# source with a provider newer than this package still parses.
|
|
260
282
|
```
|
|
261
283
|
|
|
262
284
|
## Development
|
|
@@ -63,7 +63,26 @@ client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"}
|
|
|
63
63
|
client.sources.get("src_id")
|
|
64
64
|
client.sources.update("src_id", {"name": "Updated"})
|
|
65
65
|
client.sources.delete("src_id")
|
|
66
|
-
client.sources.rotate_secret("src_id")
|
|
66
|
+
client.sources.rotate_secret("src_id") # 400 for vendor-issued/public-key/built-in secrets
|
|
67
|
+
client.sources.reveal_secret("src_id")
|
|
68
|
+
|
|
69
|
+
# A provider that verifies with no secret: signing_secret comes back None
|
|
70
|
+
wise = client.sources.create({"name": "Wise", "slug": "wise", "provider": "wise"})
|
|
71
|
+
assert wise.verifies_without_secret and wise.signing_secret is None
|
|
72
|
+
|
|
73
|
+
# WhatsApp (Meta) sources also carry a verify token
|
|
74
|
+
wa = client.sources.create({"name": "WhatsApp", "slug": "whatsapp", "provider": "whatsapp"})
|
|
75
|
+
print(wa.verify_token)
|
|
76
|
+
client.sources.rotate_verify_token(wa.id)
|
|
77
|
+
|
|
78
|
+
# Notion parks its verification token until you adopt it
|
|
79
|
+
pending = client.sources.get_pending_verification("notion")
|
|
80
|
+
client.sources.adopt_pending_verification("notion")
|
|
81
|
+
|
|
82
|
+
# Provider catalog: which secret each provider needs (public, no auth needed)
|
|
83
|
+
for p in client.providers.list(category="payments").providers:
|
|
84
|
+
print(p.hookbase_provider, p.signature_verification and p.signature_verification.secret_source)
|
|
85
|
+
client.providers.get("stripe").signature_verification
|
|
67
86
|
|
|
68
87
|
# Destinations
|
|
69
88
|
client.destinations.list()
|
|
@@ -76,7 +95,7 @@ client.routes.get_circuit_status("route_id")
|
|
|
76
95
|
client.routes.reset_circuit("route_id")
|
|
77
96
|
|
|
78
97
|
# Events & Deliveries
|
|
79
|
-
client.events.list(source_id="src_id", from_date="2024-01-01")
|
|
98
|
+
client.events.list(source_id="src_id", from_date="2024-01-01") # id or slug; unknown -> 404
|
|
80
99
|
client.events.get("evt_id") # Includes payload and deliveries
|
|
81
100
|
client.deliveries.replay("del_id")
|
|
82
101
|
client.deliveries.bulk_replay(["del_1", "del_2"])
|
|
@@ -217,11 +236,14 @@ All responses are typed Pydantic models with camelCase alias support:
|
|
|
217
236
|
from hookbase.models import Source, CreateSourceParams
|
|
218
237
|
|
|
219
238
|
# Use Pydantic models for type safety
|
|
220
|
-
params = CreateSourceParams(name="GitHub", provider="github")
|
|
239
|
+
params = CreateSourceParams(name="GitHub", slug="github", provider="github")
|
|
221
240
|
source = client.sources.create(params)
|
|
222
241
|
|
|
223
242
|
# Or use plain dicts
|
|
224
|
-
source = client.sources.create({"name": "GitHub", "provider": "github"})
|
|
243
|
+
source = client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"})
|
|
244
|
+
|
|
245
|
+
# `provider` is any string (hookbase.models.KNOWN_SOURCE_PROVIDERS lists today's values), so a
|
|
246
|
+
# source with a provider newer than this package still parses.
|
|
225
247
|
```
|
|
226
248
|
|
|
227
249
|
## Development
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.2.0"
|
|
@@ -26,6 +26,7 @@ from .resources import (
|
|
|
26
26
|
AsyncMessages,
|
|
27
27
|
AsyncOrganizations,
|
|
28
28
|
AsyncPortalTokens,
|
|
29
|
+
AsyncProviders,
|
|
29
30
|
AsyncRoutes,
|
|
30
31
|
AsyncSchemas,
|
|
31
32
|
AsyncSources,
|
|
@@ -43,6 +44,7 @@ from .resources import (
|
|
|
43
44
|
Messages,
|
|
44
45
|
Organizations,
|
|
45
46
|
PortalTokens,
|
|
47
|
+
Providers,
|
|
46
48
|
Routes,
|
|
47
49
|
Schemas,
|
|
48
50
|
Sources,
|
|
@@ -118,6 +120,8 @@ class Hookbase:
|
|
|
118
120
|
transforms: Transforms
|
|
119
121
|
filters: Filters
|
|
120
122
|
schemas: Schemas
|
|
123
|
+
#: The public provider catalog (what each provider signs with, which secret it needs).
|
|
124
|
+
providers: Providers
|
|
121
125
|
|
|
122
126
|
# Outbound
|
|
123
127
|
outbound: _OutboundNamespace
|
|
@@ -160,6 +164,7 @@ class Hookbase:
|
|
|
160
164
|
self.transforms = Transforms(self._transport)
|
|
161
165
|
self.filters = Filters(self._transport)
|
|
162
166
|
self.schemas = Schemas(self._transport)
|
|
167
|
+
self.providers = Providers(self._transport)
|
|
163
168
|
|
|
164
169
|
# Outbound namespace
|
|
165
170
|
self.outbound = _OutboundNamespace(self._transport)
|
|
@@ -210,6 +215,8 @@ class AsyncHookbase:
|
|
|
210
215
|
transforms: AsyncTransforms
|
|
211
216
|
filters: AsyncFilters
|
|
212
217
|
schemas: AsyncSchemas
|
|
218
|
+
#: The public provider catalog (what each provider signs with, which secret it needs).
|
|
219
|
+
providers: AsyncProviders
|
|
213
220
|
|
|
214
221
|
# Outbound
|
|
215
222
|
outbound: _AsyncOutboundNamespace
|
|
@@ -252,6 +259,7 @@ class AsyncHookbase:
|
|
|
252
259
|
self.transforms = AsyncTransforms(self._transport)
|
|
253
260
|
self.filters = AsyncFilters(self._transport)
|
|
254
261
|
self.schemas = AsyncSchemas(self._transport)
|
|
262
|
+
self.providers = AsyncProviders(self._transport)
|
|
255
263
|
|
|
256
264
|
# Outbound namespace
|
|
257
265
|
self.outbound = _AsyncOutboundNamespace(self._transport)
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
from .analytics import AnalyticsTimeline, DashboardData
|
|
2
2
|
from .api_keys import ApiKey, ApiKeyWithSecret, CreateApiKeyParams
|
|
3
3
|
from .applications import Application, CreateApplicationParams, UpdateApplicationParams
|
|
4
|
+
from .catalog import (
|
|
5
|
+
CatalogProvider,
|
|
6
|
+
CatalogProviderDetail,
|
|
7
|
+
ProviderCatalogList,
|
|
8
|
+
ProviderEvents,
|
|
9
|
+
ProviderEventType,
|
|
10
|
+
ProviderSignatureVerification,
|
|
11
|
+
ProviderSignatureVerificationSummary,
|
|
12
|
+
ProviderVerifyTokenInfo,
|
|
13
|
+
)
|
|
4
14
|
from .common import BulkDeleteResult, ImportResult, ImportResultItem
|
|
5
15
|
from .cron_jobs import (
|
|
6
16
|
CreateCronGroupParams,
|
|
@@ -89,9 +99,13 @@ from .routes import (
|
|
|
89
99
|
)
|
|
90
100
|
from .schemas import CreateSchemaParams, Schema, SchemaValidationResult, UpdateSchemaParams
|
|
91
101
|
from .sources import (
|
|
102
|
+
CLEARABLE_SOURCE_FIELDS,
|
|
103
|
+
KNOWN_SOURCE_PROVIDERS,
|
|
104
|
+
ClearableSourceField,
|
|
92
105
|
CreateSourceParams,
|
|
93
106
|
DedupStrategy,
|
|
94
107
|
IpFilterMode,
|
|
108
|
+
PendingVerification,
|
|
95
109
|
Source,
|
|
96
110
|
SourceProvider,
|
|
97
111
|
SourceWithSecret,
|
|
@@ -119,6 +133,15 @@ __all__ = [
|
|
|
119
133
|
"Application",
|
|
120
134
|
"CreateApplicationParams",
|
|
121
135
|
"UpdateApplicationParams",
|
|
136
|
+
# Catalog
|
|
137
|
+
"CatalogProvider",
|
|
138
|
+
"CatalogProviderDetail",
|
|
139
|
+
"ProviderCatalogList",
|
|
140
|
+
"ProviderEventType",
|
|
141
|
+
"ProviderEvents",
|
|
142
|
+
"ProviderSignatureVerification",
|
|
143
|
+
"ProviderSignatureVerificationSummary",
|
|
144
|
+
"ProviderVerifyTokenInfo",
|
|
122
145
|
# Common
|
|
123
146
|
"BulkDeleteResult",
|
|
124
147
|
"ImportResult",
|
|
@@ -211,9 +234,13 @@ __all__ = [
|
|
|
211
234
|
"SchemaValidationResult",
|
|
212
235
|
"UpdateSchemaParams",
|
|
213
236
|
# Sources
|
|
237
|
+
"CLEARABLE_SOURCE_FIELDS",
|
|
238
|
+
"KNOWN_SOURCE_PROVIDERS",
|
|
239
|
+
"ClearableSourceField",
|
|
214
240
|
"CreateSourceParams",
|
|
215
241
|
"DedupStrategy",
|
|
216
242
|
"IpFilterMode",
|
|
243
|
+
"PendingVerification",
|
|
217
244
|
"Source",
|
|
218
245
|
"SourceProvider",
|
|
219
246
|
"SourceWithSecret",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""The public provider catalog: GET /api/catalog/providers (no authentication needed).
|
|
2
|
+
|
|
3
|
+
Every enumerated value here (category, kind, secret_source, secret_format) is a plain string
|
|
4
|
+
rather than a Literal, for the reason ``SourceProvider`` is: the catalog grows server-side, and a
|
|
5
|
+
value this package has not heard of must not make the whole response fail to parse.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import field_validator
|
|
13
|
+
|
|
14
|
+
from ._base import HookbaseModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProviderEventType(HookbaseModel):
|
|
18
|
+
type: str
|
|
19
|
+
description: str = ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProviderVerifyTokenInfo(HookbaseModel):
|
|
23
|
+
"""Meta's second, Hookbase-generated credential (see SourceWithSecret.verify_token)."""
|
|
24
|
+
|
|
25
|
+
label: str
|
|
26
|
+
instruction: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProviderSignatureVerificationSummary(HookbaseModel):
|
|
30
|
+
"""The secret fields a create flow needs, as the catalog list carries them.
|
|
31
|
+
|
|
32
|
+
``secret_source`` says who produces the credential: "provider" (the vendor issues it; paste
|
|
33
|
+
it as signing_secret), "operator" (Hookbase generates it when omitted; paste it into the
|
|
34
|
+
vendor), "public_key" (paste the vendor's public key), "built_in" (nothing to enter; events
|
|
35
|
+
are still verified) or "none" (the vendor does not sign).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
supported: bool = False
|
|
39
|
+
#: The scheme kind: "hmac", "shared_secret", "bespoke" or "asymmetric". None when no scheme.
|
|
40
|
+
kind: str | None = None
|
|
41
|
+
secret_source: str | None = None
|
|
42
|
+
secret_label: str | None = None
|
|
43
|
+
secret_placeholder: str | None = None
|
|
44
|
+
#: The vendor issues the secret only after the source's URL is registered with it.
|
|
45
|
+
secret_after_registration: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ProviderSignatureVerification(ProviderSignatureVerificationSummary):
|
|
49
|
+
"""The full block from the provider detail call."""
|
|
50
|
+
|
|
51
|
+
algorithm: str | None = None
|
|
52
|
+
header_name: str | None = None
|
|
53
|
+
secret_location_hint: str | None = None
|
|
54
|
+
#: One sentence on where the value comes from or goes, in the vendor's own UI wording.
|
|
55
|
+
secret_instruction: str | None = None
|
|
56
|
+
#: "whsec" or "basic_auth" when the value has a required shape.
|
|
57
|
+
secret_format: str | None = None
|
|
58
|
+
#: Present for Meta providers, which also use a verify token.
|
|
59
|
+
verify_token: ProviderVerifyTokenInfo | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CatalogProvider(HookbaseModel):
|
|
63
|
+
"""A provider as the catalog list returns it.
|
|
64
|
+
|
|
65
|
+
``id`` is the catalog's id; ``hookbase_provider`` is the value to use as a source's
|
|
66
|
+
``provider``. They differ for some entries (catalog "mollie-nextgen" is provider "mollie",
|
|
67
|
+
and several catalog entries map to "custom").
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
id: str
|
|
71
|
+
name: str
|
|
72
|
+
category: str = ""
|
|
73
|
+
description: str = ""
|
|
74
|
+
icon: str | None = None
|
|
75
|
+
event_type_count: int = 0
|
|
76
|
+
hookbase_provider: str = ""
|
|
77
|
+
has_signature_verification: bool = False
|
|
78
|
+
has_ip_ranges: bool = False
|
|
79
|
+
#: None when the provider has no verification block, and also from API versions that
|
|
80
|
+
#: predate this summary on list items: fall back to client.providers.get(id) for it.
|
|
81
|
+
signature_verification: ProviderSignatureVerificationSummary | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ProviderCatalogList(HookbaseModel):
|
|
85
|
+
providers: list[CatalogProvider] = []
|
|
86
|
+
categories: list[str] = []
|
|
87
|
+
total: int = 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CatalogProviderDetail(HookbaseModel):
|
|
91
|
+
"""One provider in full, from GET /api/catalog/providers/:id."""
|
|
92
|
+
|
|
93
|
+
id: str
|
|
94
|
+
name: str
|
|
95
|
+
category: str = ""
|
|
96
|
+
description: str = ""
|
|
97
|
+
website: str | None = None
|
|
98
|
+
docs_url: str | None = None
|
|
99
|
+
icon: str | None = None
|
|
100
|
+
hookbase_provider: str = ""
|
|
101
|
+
signature_verification: ProviderSignatureVerification | None = None
|
|
102
|
+
event_types: list[ProviderEventType] = []
|
|
103
|
+
#: Example payloads keyed by event type.
|
|
104
|
+
sample_payloads: dict[str, Any] = {}
|
|
105
|
+
ip_ranges: list[str] | None = None
|
|
106
|
+
setup_guide: list[str] = []
|
|
107
|
+
|
|
108
|
+
@field_validator("event_types", "setup_guide", mode="before")
|
|
109
|
+
@classmethod
|
|
110
|
+
def _none_to_list(cls, v: Any) -> Any:
|
|
111
|
+
return [] if v is None else v
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ProviderEvents(HookbaseModel):
|
|
115
|
+
"""GET /api/catalog/providers/:id/events."""
|
|
116
|
+
|
|
117
|
+
provider_id: str
|
|
118
|
+
provider_name: str = ""
|
|
119
|
+
event_types: list[ProviderEventType] = []
|
|
120
|
+
sample_payloads: dict[str, Any] = {}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import field_validator
|
|
7
|
+
|
|
8
|
+
from . import _wire
|
|
9
|
+
from ._base import HookbaseModel
|
|
10
|
+
|
|
11
|
+
# Providers the API accepts for a source today.
|
|
12
|
+
#
|
|
13
|
+
# Mirrors SUPPORTED_SIGNATURE_PROVIDERS in the API (api/src/utils/signature-schemes.ts): 71
|
|
14
|
+
# signature schemes plus the aliases svix, clerk, resend, render, etsy and stytch (all of
|
|
15
|
+
# standard-webhooks) and generic (of custom). Regenerate with
|
|
16
|
+
# `npx tsx scripts/print-source-enums.ts` in the api package.
|
|
17
|
+
#
|
|
18
|
+
# This is a list of suggestions, not a constraint. `provider` is typed as a plain string on every
|
|
19
|
+
# model, request and response alike, because the API adds providers faster than this package
|
|
20
|
+
# ships: when it was a Literal, a single source created with a newer provider (wise, whatsapp...)
|
|
21
|
+
# made list(), get() and create() raise a ValidationError for the whole call. The API validates
|
|
22
|
+
# the value on create and update and answers an unknown one with a 400 naming it.
|
|
23
|
+
KNOWN_SOURCE_PROVIDERS: tuple[str, ...] = (
|
|
24
|
+
"adyen", "airtable", "asana", "auth0", "aws-sns", "bigcommerce", "bitbucket", "braintree",
|
|
25
|
+
"calendly", "chargebee", "circleci", "clerk", "clickup", "cloudflare", "coinbase-commerce",
|
|
26
|
+
"custom", "datadog", "discord", "docusign", "etsy", "freshdesk", "generic", "github",
|
|
27
|
+
"gitlab", "gitlab-signing-token", "gocardless", "grafana", "grafana-hmac",
|
|
28
|
+
"grafana-hmac-timestamped", "heroku", "hubspot", "intercom", "jira", "klaviyo",
|
|
29
|
+
"launchdarkly", "lemonsqueezy", "linear", "mailchimp", "mailgun", "mollie", "netlify",
|
|
30
|
+
"newrelic", "notion", "okta", "paddle", "pagerduty", "paypal", "pipedrive", "postmark",
|
|
31
|
+
"railway", "razorpay", "render", "resend", "segment", "sendgrid", "sentry", "shopify",
|
|
32
|
+
"slack", "square", "standard-webhooks", "stripe", "stytch", "supabase",
|
|
33
|
+
"supabase-auth-hooks", "svix", "telegram", "terraform-cloud", "trello", "twilio",
|
|
34
|
+
"typeform", "vercel", "whatsapp", "wise", "wise-sandbox", "woocommerce", "workos",
|
|
35
|
+
"zendesk", "zoom",
|
|
36
|
+
)
|
|
37
|
+
#: A source provider id. Any string: see KNOWN_SOURCE_PROVIDERS for the values accepted today.
|
|
38
|
+
SourceProvider = str
|
|
39
|
+
|
|
40
|
+
# Corrected in 2026-09: "header" and "event_id" were never accepted, and the three values the
|
|
41
|
+
# API actually defaults to and documents were missing. "auto" is the default.
|
|
42
|
+
DedupStrategy = Literal["auto", "provider_id", "payload_hash", "idempotency_key", "none"]
|
|
43
|
+
IpFilterMode = Literal["none", "allowlist", "denylist", "both"]
|
|
44
|
+
# HTTP verbs an ingest endpoint can be restricted to. OPTIONS is excluded: CORS preflight is
|
|
45
|
+
# answered before ingest runs, so it is never gateable.
|
|
46
|
+
IngestMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]
|
|
47
|
+
|
|
48
|
+
# The settings PATCH /api/sources/:id accepts an explicit null for (`.nullable()` in
|
|
49
|
+
# updateSourceSchema). Naming one in `UpdateSourceParams.clear` sends that null. Null signingSecret
|
|
50
|
+
# removes the secret; null verifyToken generates a fresh one. `clear` also takes snake_case.
|
|
51
|
+
CLEARABLE_SOURCE_FIELDS: tuple[str, ...] = (
|
|
52
|
+
"description",
|
|
53
|
+
"provider",
|
|
54
|
+
"signingSecret",
|
|
55
|
+
"verifyToken",
|
|
56
|
+
"rateLimitPerMinute",
|
|
57
|
+
"ipAllowlist",
|
|
58
|
+
"ipDenylist",
|
|
59
|
+
"encryptFields",
|
|
60
|
+
"maskFields",
|
|
61
|
+
"dedupStrategy",
|
|
62
|
+
"dedupWindowHours",
|
|
63
|
+
"dedupCustomHeader",
|
|
64
|
+
"allowedMethods",
|
|
65
|
+
)
|
|
66
|
+
ClearableSourceField = Literal[
|
|
67
|
+
"description", "provider", "signingSecret", "verifyToken", "rateLimitPerMinute",
|
|
68
|
+
"ipAllowlist", "ipDenylist", "encryptFields", "maskFields", "dedupStrategy",
|
|
69
|
+
"dedupWindowHours", "dedupCustomHeader", "allowedMethods",
|
|
70
|
+
"signing_secret", "verify_token", "rate_limit_per_minute", "ip_allowlist", "ip_denylist",
|
|
71
|
+
"encrypt_fields", "mask_fields", "dedup_strategy", "dedup_window_hours",
|
|
72
|
+
"dedup_custom_header", "allowed_methods",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Source(HookbaseModel):
|
|
77
|
+
"""A source as the API returns it.
|
|
78
|
+
|
|
79
|
+
The signing secret is not included. Responses carry ``has_signing_secret`` and the last four
|
|
80
|
+
characters; the full value comes back exactly twice, from :meth:`Sources.create` (which
|
|
81
|
+
returns a :class:`SourceWithSecret`) and from :meth:`Sources.reveal_secret`.
|
|
82
|
+
|
|
83
|
+
``has_signing_secret`` being False is not always a problem. Check the two flags beside it:
|
|
84
|
+
``verifies_without_secret`` (the provider signs with a key Hookbase already holds, as Wise
|
|
85
|
+
and AWS SNS do, so events are verified with nothing set) and ``awaiting_signing_secret``
|
|
86
|
+
(the vendor issues the secret only after the URL is registered with it, as Asana and Notion
|
|
87
|
+
do, and it has not been set yet).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
id: str
|
|
91
|
+
organization_id: str | None = None
|
|
92
|
+
name: str
|
|
93
|
+
slug: str
|
|
94
|
+
#: Any string; see KNOWN_SOURCE_PROVIDERS. Never validated client-side.
|
|
95
|
+
provider: SourceProvider | None = None
|
|
96
|
+
description: str | None = None
|
|
97
|
+
#: Whether a signing secret is set. The secret itself is not returned here.
|
|
98
|
+
has_signing_secret: bool = False
|
|
99
|
+
#: Last four characters of the signing secret, prefixed with "...", or None if unset.
|
|
100
|
+
signing_secret_last4: str | None = None
|
|
101
|
+
#: The provider verifies with a key Hookbase already holds (wise, wise-sandbox, aws-sns), so
|
|
102
|
+
#: there is no secret to set and has_signing_secret stays False on a fully verified source.
|
|
103
|
+
verifies_without_secret: bool = False
|
|
104
|
+
#: The vendor issues the secret only once this source's URL is registered with it (asana,
|
|
105
|
+
#: notion) and it has not been set yet. Events are not verified until it is. For Notion, see
|
|
106
|
+
#: Sources.get_pending_verification.
|
|
107
|
+
awaiting_signing_secret: bool = False
|
|
108
|
+
#: Reject events whose signature fails verification. Defaults to False, in which case a
|
|
109
|
+
#: failing event is flagged with signature_valid=False and delivered anyway.
|
|
110
|
+
reject_invalid_signatures: bool = False
|
|
111
|
+
rate_limit_per_minute: int | None = None
|
|
112
|
+
is_active: bool = True
|
|
113
|
+
custom_domain_id: str | None = None
|
|
114
|
+
ip_filter_mode: IpFilterMode = "none"
|
|
115
|
+
ip_allowlist: list[str] = []
|
|
116
|
+
ip_denylist: list[str] = []
|
|
117
|
+
#: JSONPath expressions whose values are encrypted at rest.
|
|
118
|
+
encrypt_fields: list[str] = []
|
|
119
|
+
#: JSONPath expressions whose values are masked in stored payloads.
|
|
120
|
+
mask_fields: list[str] = []
|
|
121
|
+
dedup_enabled: bool = False
|
|
122
|
+
dedup_strategy: DedupStrategy = "auto"
|
|
123
|
+
dedup_window_hours: int = 24
|
|
124
|
+
dedup_custom_header: str | None = None
|
|
125
|
+
transient_mode: bool = False
|
|
126
|
+
#: HTTP verbs the ingest endpoint accepts. Empty list means any method.
|
|
127
|
+
allowed_methods: list[str] = []
|
|
128
|
+
event_count: int = 0
|
|
129
|
+
route_count: int = 0
|
|
130
|
+
#: Returned by get() and create(). Not included in list() responses.
|
|
131
|
+
ingest_url: str | None = None
|
|
132
|
+
created_at: str = ""
|
|
133
|
+
updated_at: str = ""
|
|
134
|
+
|
|
135
|
+
@field_validator(
|
|
136
|
+
"is_active",
|
|
137
|
+
"reject_invalid_signatures",
|
|
138
|
+
"has_signing_secret",
|
|
139
|
+
"verifies_without_secret",
|
|
140
|
+
"awaiting_signing_secret",
|
|
141
|
+
"dedup_enabled",
|
|
142
|
+
"transient_mode",
|
|
143
|
+
mode="before",
|
|
144
|
+
)
|
|
145
|
+
@classmethod
|
|
146
|
+
def parse_bool_from_int(cls, v: Any) -> Any:
|
|
147
|
+
if isinstance(v, int):
|
|
148
|
+
return bool(v)
|
|
149
|
+
return v
|
|
150
|
+
|
|
151
|
+
@field_validator(
|
|
152
|
+
"ip_allowlist", "ip_denylist", "encrypt_fields", "mask_fields", mode="before"
|
|
153
|
+
)
|
|
154
|
+
@classmethod
|
|
155
|
+
def parse_json_list(cls, v: Any) -> Any:
|
|
156
|
+
if v is None:
|
|
157
|
+
return []
|
|
158
|
+
if isinstance(v, str):
|
|
159
|
+
try:
|
|
160
|
+
parsed = json.loads(v)
|
|
161
|
+
return parsed if isinstance(parsed, list) else []
|
|
162
|
+
except (json.JSONDecodeError, ValueError):
|
|
163
|
+
return []
|
|
164
|
+
return v
|
|
165
|
+
|
|
166
|
+
@field_validator("allowed_methods", mode="before")
|
|
167
|
+
@classmethod
|
|
168
|
+
def parse_allowed_methods(cls, v: Any) -> Any:
|
|
169
|
+
"""Coerce null/JSON-string forms to a list; both mean "any method" when empty."""
|
|
170
|
+
if v is None:
|
|
171
|
+
return []
|
|
172
|
+
if isinstance(v, str):
|
|
173
|
+
try:
|
|
174
|
+
parsed = json.loads(v)
|
|
175
|
+
return parsed if isinstance(parsed, list) else []
|
|
176
|
+
except (json.JSONDecodeError, ValueError):
|
|
177
|
+
return []
|
|
178
|
+
return v
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SourceWithSecret(Source):
|
|
182
|
+
"""What create() returns: a source plus the one look at its signing secret you get."""
|
|
183
|
+
|
|
184
|
+
#: The signing secret, in full, this once. None when the source has none to show: the
|
|
185
|
+
#: provider verifies without one (verifies_without_secret), the vendor issues it after
|
|
186
|
+
#: registration (awaiting_signing_secret), or the provider does not sign.
|
|
187
|
+
signing_secret: str | None = None
|
|
188
|
+
#: Meta providers (whatsapp) only: the verify token to paste into Meta's webhook setup, in
|
|
189
|
+
#: full, this once. Sources.verify_token reveals it later.
|
|
190
|
+
verify_token: str | None = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class PendingVerification(HookbaseModel):
|
|
194
|
+
"""A verification token a vendor delivered to the ingest URL during registration (Notion).
|
|
195
|
+
|
|
196
|
+
The request that carried it was unauthenticated, so it is held rather than adopted: paste
|
|
197
|
+
``token`` into the vendor's verify form, and once the vendor accepts it, call
|
|
198
|
+
:meth:`Sources.adopt_pending_verification` to make it the source's signing secret.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
provider: str
|
|
202
|
+
token: str
|
|
203
|
+
received_at: str
|
|
204
|
+
client_ip: str | None = None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class CreateSourceParams(HookbaseModel):
|
|
208
|
+
name: str
|
|
209
|
+
#: Required: it forms the ingest URL, /ingest/<org>/<slug>, and cannot be changed later.
|
|
210
|
+
slug: str
|
|
211
|
+
provider: SourceProvider | None = None
|
|
212
|
+
description: str | None = None
|
|
213
|
+
#: What to supply depends on the provider (the catalog's signatureVerification.secretSource,
|
|
214
|
+
#: see client.providers.get):
|
|
215
|
+
#: - "provider": the vendor issues it. Required for several (standard-webhooks and its
|
|
216
|
+
#: aliases, airtable, gitlab-signing-token, supabase-auth-hooks, jira, discord, sendgrid);
|
|
217
|
+
#: a missing or malformed one is a 400.
|
|
218
|
+
#: - "operator": omit it and the API generates one, returned once on the created source.
|
|
219
|
+
#: - "public_key": the vendor's public key (PEM or base64).
|
|
220
|
+
#: - "built_in" (wise, wise-sandbox, aws-sns): send nothing; any secret is a 400.
|
|
221
|
+
signing_secret: str | None = None
|
|
222
|
+
#: Meta providers (whatsapp) only; a 400 for any other. Omit it and the API generates one.
|
|
223
|
+
verify_token: str | None = None
|
|
224
|
+
#: Reject events whose signature fails verification. Defaults to False.
|
|
225
|
+
reject_invalid_signatures: bool | None = None
|
|
226
|
+
rate_limit_per_minute: int | None = None
|
|
227
|
+
ip_filter_mode: IpFilterMode | None = None
|
|
228
|
+
ip_allowlist: list[str] | None = None
|
|
229
|
+
ip_denylist: list[str] | None = None
|
|
230
|
+
encrypt_fields: list[str] | None = None
|
|
231
|
+
mask_fields: list[str] | None = None
|
|
232
|
+
dedup_enabled: bool | None = None
|
|
233
|
+
dedup_strategy: DedupStrategy | None = None
|
|
234
|
+
#: Deduplication window in hours, 1 to 168.
|
|
235
|
+
dedup_window_hours: int | None = None
|
|
236
|
+
#: Header to deduplicate on when dedup_strategy is "idempotency_key".
|
|
237
|
+
dedup_custom_header: str | None = None
|
|
238
|
+
transient_mode: bool | None = None
|
|
239
|
+
#: Restrict the ingest endpoint to these verbs. None/[] accepts any method.
|
|
240
|
+
allowed_methods: list[IngestMethod] | None = None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class UpdateSourceParams(HookbaseModel):
|
|
244
|
+
name: str | None = None
|
|
245
|
+
description: str | None = None
|
|
246
|
+
provider: SourceProvider | None = None
|
|
247
|
+
is_active: bool | None = None
|
|
248
|
+
signing_secret: str | None = None
|
|
249
|
+
#: Meta providers (whatsapp) only. "" regenerates it, as does clear=["verifyToken"].
|
|
250
|
+
verify_token: str | None = None
|
|
251
|
+
reject_invalid_signatures: bool | None = None
|
|
252
|
+
rate_limit_per_minute: int | None = None
|
|
253
|
+
ip_filter_mode: IpFilterMode | None = None
|
|
254
|
+
ip_allowlist: list[str] | None = None
|
|
255
|
+
ip_denylist: list[str] | None = None
|
|
256
|
+
encrypt_fields: list[str] | None = None
|
|
257
|
+
mask_fields: list[str] | None = None
|
|
258
|
+
dedup_enabled: bool | None = None
|
|
259
|
+
dedup_strategy: DedupStrategy | None = None
|
|
260
|
+
dedup_window_hours: int | None = None
|
|
261
|
+
dedup_custom_header: str | None = None
|
|
262
|
+
transient_mode: bool | None = None
|
|
263
|
+
#: Restrict the ingest endpoint to these verbs. Pass [] to accept any method again.
|
|
264
|
+
allowed_methods: list[IngestMethod] | None = None
|
|
265
|
+
|
|
266
|
+
clear: list[ClearableSourceField] | None = None
|
|
267
|
+
"""Fields to send as an explicit JSON null.
|
|
268
|
+
|
|
269
|
+
``None`` on any field above means "leave it alone", so it cannot also mean "remove it".
|
|
270
|
+
Naming a field here sends null for it, which PATCH /api/sources/:id accepts for the fields in
|
|
271
|
+
``CLEARABLE_SOURCE_FIELDS``::
|
|
272
|
+
|
|
273
|
+
client.sources.update("src_1", UpdateSourceParams(clear=["signingSecret"]))
|
|
274
|
+
client.sources.update("src_1", UpdateSourceParams(clear=["verifyToken"])) # regenerate
|
|
275
|
+
|
|
276
|
+
A field that is also set on this model raises rather than the SDK choosing one silently."""
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def update_source_body(params: UpdateSourceParams | dict[str, Any]) -> dict[str, Any]:
|
|
280
|
+
"""Build the PATCH /api/sources/:id body, turning ``clear`` into explicit nulls."""
|
|
281
|
+
body = _wire.dump(params)
|
|
282
|
+
_wire.apply_clear(body, CLEARABLE_SOURCE_FIELDS, "source")
|
|
283
|
+
return body
|
|
@@ -13,6 +13,7 @@ from .message_log import AsyncMessageLog, MessageLog
|
|
|
13
13
|
from .messages import AsyncMessages, Messages
|
|
14
14
|
from .organizations import AsyncOrganizations, Organizations
|
|
15
15
|
from .portal_tokens import AsyncPortalTokens, PortalTokens
|
|
16
|
+
from .providers import AsyncProviders, Providers
|
|
16
17
|
from .routes import AsyncRoutes, Routes
|
|
17
18
|
from .schemas import AsyncSchemas, Schemas
|
|
18
19
|
from .sources import AsyncSources, Sources
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"Messages", "AsyncMessages",
|
|
37
38
|
"Organizations", "AsyncOrganizations",
|
|
38
39
|
"PortalTokens", "AsyncPortalTokens",
|
|
40
|
+
"Providers", "AsyncProviders",
|
|
39
41
|
"Routes", "AsyncRoutes",
|
|
40
42
|
"Schemas", "AsyncSchemas",
|
|
41
43
|
"Sources", "AsyncSources",
|