ohauthcat 0.0.2__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 (39) hide show
  1. ohauthcat-0.0.2/.gitignore +43 -0
  2. ohauthcat-0.0.2/CHANGELOG.md +27 -0
  3. ohauthcat-0.0.2/LICENSE +35 -0
  4. ohauthcat-0.0.2/PKG-INFO +181 -0
  5. ohauthcat-0.0.2/README.md +153 -0
  6. ohauthcat-0.0.2/THIRD_PARTY_LICENSES +852 -0
  7. ohauthcat-0.0.2/examples/python/LICENSE +35 -0
  8. ohauthcat-0.0.2/examples/python/THIRD_PARTY_LICENSES +5413 -0
  9. ohauthcat-0.0.2/examples/python/pyproject.toml +28 -0
  10. ohauthcat-0.0.2/pyproject.toml +52 -0
  11. ohauthcat-0.0.2/spec/README.md +23 -0
  12. ohauthcat-0.0.2/src/ohauthcat/__init__.py +95 -0
  13. ohauthcat-0.0.2/src/ohauthcat/_version.py +6 -0
  14. ohauthcat-0.0.2/src/ohauthcat/access_token.py +400 -0
  15. ohauthcat-0.0.2/src/ohauthcat/audit.py +56 -0
  16. ohauthcat-0.0.2/src/ohauthcat/client.py +178 -0
  17. ohauthcat-0.0.2/src/ohauthcat/contract.json +1 -0
  18. ohauthcat-0.0.2/src/ohauthcat/custom_attributes.py +130 -0
  19. ohauthcat-0.0.2/src/ohauthcat/directory_csv.py +72 -0
  20. ohauthcat-0.0.2/src/ohauthcat/end_user.py +232 -0
  21. ohauthcat-0.0.2/src/ohauthcat/errors.py +135 -0
  22. ohauthcat-0.0.2/src/ohauthcat/existing_auth.py +1222 -0
  23. ohauthcat-0.0.2/src/ohauthcat/flags.py +200 -0
  24. ohauthcat-0.0.2/src/ohauthcat/framework.py +448 -0
  25. ohauthcat-0.0.2/src/ohauthcat/jwt_verifier.py +225 -0
  26. ohauthcat-0.0.2/src/ohauthcat/logo.py +123 -0
  27. ohauthcat-0.0.2/src/ohauthcat/migration.py +351 -0
  28. ohauthcat-0.0.2/src/ohauthcat/models.py +9246 -0
  29. ohauthcat-0.0.2/src/ohauthcat/passkeys.py +211 -0
  30. ohauthcat-0.0.2/src/ohauthcat/py.typed +0 -0
  31. ohauthcat-0.0.2/src/ohauthcat/resource_server.py +148 -0
  32. ohauthcat-0.0.2/src/ohauthcat/resources.py +10311 -0
  33. ohauthcat-0.0.2/src/ohauthcat/response_models.py +8758 -0
  34. ohauthcat-0.0.2/src/ohauthcat/ssf.py +218 -0
  35. ohauthcat-0.0.2/src/ohauthcat/sso.py +391 -0
  36. ohauthcat-0.0.2/src/ohauthcat/standby.py +137 -0
  37. ohauthcat-0.0.2/src/ohauthcat/transport.py +929 -0
  38. ohauthcat-0.0.2/src/ohauthcat/webhooks.py +178 -0
  39. ohauthcat-0.0.2/src/ohauthcat/wire.py +199 -0
@@ -0,0 +1,43 @@
1
+ # OS
2
+ .DS_Store
3
+
4
+ # Environment variables and secrets
5
+ .env
6
+ .env.*
7
+ !.env.template
8
+ *.pem
9
+ *.key
10
+ secrets/
11
+
12
+ # Build outputs
13
+ dist/
14
+ build/
15
+ coverage/
16
+ *.tsbuildinfo
17
+
18
+ # Node
19
+ node_modules/
20
+
21
+ # Python
22
+ .venv/
23
+ __pycache__/
24
+ *.py[cod]
25
+ .mypy_cache/
26
+ .pytest_cache/
27
+ .ruff_cache/
28
+ .coverage
29
+
30
+ # Repository baseline (handbook doc 31 §2b)
31
+ Thumbs.db
32
+ .idea/
33
+ .vscode/*
34
+ !.vscode/extensions.json
35
+ *.swp
36
+ *.p12
37
+ *.log
38
+ tmp/
39
+ .cache/
40
+ *.egg-info/
41
+
42
+ # graphify output is generated locally on demand and never committed.
43
+ graphify-out/
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `ohauthcat` Python SDK are recorded here. The SDK
4
+ follows [Semantic Versioning](https://semver.org/).
5
+
6
+ ## Unreleased
7
+
8
+ ### Changed
9
+
10
+ - Request models now enforce the size limits defined by the OhAuthCat API, so
11
+ an oversized request fails locally with a validation error instead of being
12
+ sent and rejected by the API:
13
+ - `AttributePreviewRequest.attributes`: at most 256 keys, each with at most
14
+ 256 values; `AttributePreviewRequest.claims`: at most 256 keys.
15
+ - `ConnectionForwardingUpdate.acs_urls` and `.redirect_uris`,
16
+ `Saml.legacy_acs_urls` and `ImportBundleDirectory.legacy_scim_urls`: at
17
+ most 5 entries each.
18
+ - `Metadata` and `ImportBundleOrganization.metadata`: at most 50 keys, each
19
+ value at most 500 characters.
20
+
21
+ ### Fixed
22
+
23
+ - The metadata check was reversed: it capped metadata at 500 keys and left
24
+ values unbounded. It now matches the API (50 keys, values of at most 500
25
+ characters).
26
+
27
+ No public class, field or import path was renamed or removed.
@@ -0,0 +1,35 @@
1
+ OhAuthCat SDK and CLI Licence
2
+
3
+ Copyright (c) 2026 Generic Technologies Private Limited. All rights reserved.
4
+ OhAuthCat is a product of Generic Technologies Private Limited.
5
+
6
+ This software development kit (the "Software") is licensed, not sold, under
7
+ the OhAuthCat SDK and CLI Licence, version 1.0, published at
8
+ https://ohauthcat.com/legal/sdk-licence (the "Licence"). By downloading,
9
+ installing or using the Software you accept the Licence. The summary below is
10
+ for convenience; if it differs from the Licence, the Licence prevails.
11
+
12
+ While you have an active agreement with OhAuthCat for its hosted service, you
13
+ may use, copy, read, modify and make derivative works of the Software, and
14
+ distribute it, modified or not, only as an integrated part of your own
15
+ application, solely to access and use the OhAuthCat service.
16
+
17
+ You must not:
18
+
19
+ 1. use the Software, or any modified version or part of it, with any service
20
+ other than the OhAuthCat service;
21
+ 2. distribute, publish or make the Software, or any modified version or part
22
+ of it, available on its own, as a standalone library, package, SDK, fork or
23
+ tool;
24
+ 3. remove or alter any copyright notice, this file or THIRD_PARTY_LICENSES;
25
+ 4. use the Software or its source code to build or help build a product or
26
+ service that competes with the OhAuthCat service; or
27
+ 5. license or distribute the Software under terms that would require it to be
28
+ disclosed or licensed as open source or distributed free of charge.
29
+
30
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
+ IMPLIED, TO THE EXTENT THE LAW ALLOWS. LIABILITY IS LIMITED AS SET OUT IN THE
32
+ LICENCE.
33
+
34
+ Third-party components distributed with the Software are listed in
35
+ THIRD_PARTY_LICENSES and are governed by their own licences.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.5
2
+ Name: ohauthcat
3
+ Version: 0.0.2
4
+ Summary: Official OhAuthCat SDK for Python
5
+ Project-URL: Documentation, https://docs.ohauthcat.com
6
+ License-File: LICENSE
7
+ License-File: THIRD_PARTY_LICENSES
8
+ Classifier: License :: Other/Proprietary License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: cryptography<51,>=50.0.2
13
+ Requires-Dist: httpx<1,>=0.27
14
+ Requires-Dist: pydantic<3,>=2.7
15
+ Requires-Dist: typing-extensions<5,>=4.12
16
+ Provides-Extra: dev
17
+ Requires-Dist: black==26.10.1; extra == 'dev'
18
+ Requires-Dist: build; extra == 'dev'
19
+ Requires-Dist: datamodel-code-generator==0.83.0; extra == 'dev'
20
+ Requires-Dist: hatch; extra == 'dev'
21
+ Requires-Dist: isort==6.1.0; extra == 'dev'
22
+ Requires-Dist: mypy; extra == 'dev'
23
+ Requires-Dist: pytest; extra == 'dev'
24
+ Requires-Dist: ruff==0.16.10; extra == 'dev'
25
+ Requires-Dist: setuptools>=84.1.0; extra == 'dev'
26
+ Requires-Dist: types-pyyaml==6.0.12.20260906; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # ohauthcat (Python)
30
+
31
+ Typed sync and async clients for the pinned OhAuthCat versioned operations.
32
+
33
+ ```sh
34
+ pip install ohauthcat
35
+ ```
36
+
37
+ ### Before the first release
38
+
39
+ `ohauthcat` is not published to PyPI yet, so the install command above fails until the first release. Until then, install it from the source repository. The repository is private: you need read access to `bitbucket.org/the-genie-team/ohauthcat-sdk-python` over SSH. Run these commands from your project directory; the `v0.0.2` tag matches the version above (until that tag exists, replace `--branch v0.0.2` with `--branch dev` in `git clone`, and `@v0.0.2` with `@dev` in the `pip install` URL).
40
+
41
+ ```sh
42
+ git clone --depth 1 --branch v0.0.2 git@bitbucket.org:the-genie-team/ohauthcat-sdk-python.git
43
+ pip install ./ohauthcat-sdk-python
44
+ ```
45
+
46
+ Without a clone:
47
+
48
+ ```sh
49
+ pip install "git+ssh://git@bitbucket.org/the-genie-team/ohauthcat-sdk-python.git@v0.0.2"
50
+ ```
51
+
52
+ ```python
53
+ from ohauthcat import OhAuthCat, models
54
+
55
+ with OhAuthCat(api_key="sk_example") as client:
56
+ organization = client.organizations.create_organization(
57
+ body=models.OrganizationCreate(name="Example")
58
+ )
59
+ ```
60
+
61
+ ## Errors
62
+
63
+ API failures raise `OhAuthCatError` (or a subclass) with `code`, `status`, `param`, `doc_url` and `request_id`. Arguments the SDK refuses before sending anything raise `OhAuthCatValidationError`: its `code` is `request_invalid`, `param` names the argument when known and `doc_url` links the catalogue page. It is also a `ValueError`, so existing `except ValueError` handlers keep working.
64
+
65
+ ## Webhooks
66
+
67
+ Verify the exact raw request body before parsing it. Pass every secret you hold during a rotation. Verification raises `WebhookVerificationError` whose `code` is, for example, `signature_invalid` or `timestamp_outside` (older than 300 seconds).
68
+
69
+ ```python
70
+ import os
71
+
72
+ from ohauthcat import webhooks
73
+ from ohauthcat.webhooks import WebhookVerificationError
74
+
75
+
76
+ def handle_webhook(raw_body: bytes, header: str) -> int:
77
+ """raw_body: the exact request bytes; header: the OhAuthCat-Signature header."""
78
+ try:
79
+ event = webhooks.verify(raw_body, header, [os.environ.get("OHAUTHCAT_WEBHOOK_SECRET", "")])
80
+ except WebhookVerificationError as err:
81
+ print("rejected:", err.code)
82
+ return 400
83
+ print(event.type, event.id)
84
+ return 204
85
+ ```
86
+
87
+ `AsyncOhAuthCat` provides the same resource methods with `await` and async context management. `client.events.list()` returns a forward iterator, or async iterator. `client.portal.create_link(...)` aliases the generated portal operation. Original `models` validate requests; `response_models` tolerate additive enum strings while retaining singleton discriminators and field constraints.
88
+
89
+ ## Sign-in
90
+
91
+ Configure `client_id="pk_..."` (and `environment_id="env_..."` to verify access tokens). Every sign-in needs a random `state` that you keep in the user's session and compare on the callback, or an attacker can sign your user into the attacker's account (login CSRF). `get_authorization_url` generates one when you pass none and returns it as `.state`, together with the PKCE `.code_verifier`. Refuse a missing or different state before exchanging the code. The exchange uses the secret API key, is never retried automatically (reusing a code revokes the first grant), and raises `RestartSignIn` when the user must start again.
92
+
93
+ ```python
94
+ import hmac
95
+
96
+ from ohauthcat import OhAuthCat, RestartSignIn
97
+
98
+ REDIRECT_URI = "https://app.example.com/callback"
99
+
100
+
101
+ def start_sign_in(client: OhAuthCat, session: dict[str, str]) -> str:
102
+ """Returns the URL to redirect the browser to. `session` is your server-side session."""
103
+ auth = client.sso.get_authorization_url(redirect_uri=REDIRECT_URI, domain="customer.com")
104
+ session["sso_state"] = auth.state
105
+ session["sso_verifier"] = auth.code_verifier
106
+ return auth.url
107
+
108
+
109
+ def finish_sign_in(client: OhAuthCat, session: dict[str, str], code: str, state: str) -> str:
110
+ expected = session.pop("sso_state", "")
111
+ verifier = session.pop("sso_verifier", "")
112
+ if not expected or not verifier or not hmac.compare_digest(expected.encode(), state.encode()):
113
+ raise PermissionError("sign-in expired, start again") # never exchange the code
114
+ try:
115
+ token = client.sso.get_profile_and_token(
116
+ code=code, code_verifier=verifier, redirect_uri=REDIRECT_URI
117
+ )
118
+ except RestartSignIn:
119
+ raise PermissionError("sign-in expired, start again") from None
120
+ return token.profile.email # create your session for this user
121
+ ```
122
+
123
+ Guests: a sign-in admitted under the organization's guest policy (an email domain outside its verified domains) is identified by `token.profile.access_policy == "guest"` (`AccessPolicy.guest.value` in `ohauthcat.response_models`; response enums decode to plain strings so a value newer than this SDK never fails); for every other sign-in the field is absent. `email_domain_verified` stays true for guests, so check this field when guests must get less access.
124
+
125
+ `client.sso.verify_access_token(token)` verifies ES256 with the configured issuer/client/environment and a bounded JWKS cache fetched from the active trusted broker origin. The configured issuer remains exact when retrieval fails over. `get_sso_profile(access_token=token)` additionally checks the authenticated broker profile. Await both on the async client. Issuer defaults to `https://auth.ohauthcat.com`; explicitly configured HTTP loopback origins support local fixtures. No token-supplied issuer or key URL is trusted.
126
+
127
+ Clients own their default HTTPX clients. Injected `http_client` instances remain caller-owned. Closing the SDK disables requests and token verification. Async requests have an overall deadline and cancellation cleanup. Sync HTTPX uses per-phase timeouts and checks the total budget between chunks/retries, including JWKS lock waits; a blocked network read or custom transport may exceed that total budget before control returns.
128
+
129
+ Upload a logo from bytes or a file path:
130
+
131
+ ```python
132
+ with OhAuthCat(api_key="sk_example") as client:
133
+ logo = client.logos.upload_file("logo.svg", idempotency_key="logo-2026-10")
134
+ # or: client.logos.upload(Path("logo.png").read_bytes())
135
+ client.branding.update_branding(body=models.BrandingUpdate(logo_url=str(logo.logo_url)))
136
+ ```
137
+
138
+ `client.logos.upload(data)` and `upload_file(path)` (awaitable on the async client) refuse, before any request, a file that is empty, larger than 256 KiB, or neither a PNG (by its signature) nor an SVG (by its root element), raising `OhAuthCatError` with code `logo_invalid`, status `0` and param `data`. `check_logo(data)` runs the same check alone. Accepted bytes are base64-encoded and sent to `upload_branding_logo`; the API still validates dimensions and SVG content, and its `logo_invalid` refusal raises the same error type with the HTTP status and message.
139
+
140
+ Install tooling in a repository-local virtual environment:
141
+
142
+ ```sh
143
+ python3 -m venv .venv
144
+ .venv/bin/pip install -e '.[dev]'
145
+ ```
146
+
147
+ Run named test files, for example `.venv/bin/python -m pytest tests/test_transport.py`. The independent contract tests additionally need Node 24 and the development spec mock: `(cd spec-mock && pnpm install --frozen-lockfile)`.
148
+
149
+ Migration helpers are explicitly import-bound: `helper = client.migration(import_id=..., legacy_provider="workos", resolve_legacy_profile=resolver)`. Read `helper.routing.for_organization(org)` and call `helper.migrate_session(legacy_access_token=...)`; await both with the async client. Your resolver authenticates UserInfo and returns the legacy profile ID, while OhAuthCat receives only the import and that ID. Routing cache defaults to 30 seconds (maximum 60); call `helper.routing.invalidate()` after a switch or rollback. Session migration reads a fresh import. Sync resolvers must cooperate with `LegacySessionContext.remaining_seconds()` and `is_cancelled()`; async resolvers must honor cancellation, whose cleanup is awaited. See the OhAuthCat docs for the full contract and direct generated API distinction.
150
+
151
+ Automatic trusted edge failover, configurable fallback origins and request-driven dual health probes use the shared policy in handbook docs/12 §8.5. `standby=False` disables it; custom primary origins require both explicit trusted fallback origins to opt in.
152
+
153
+ ## Portal links
154
+
155
+ A setup link lasts `expires_in` seconds, 300 (5 minutes) to 2,592,000 (30 days); leave it out for the default of 604,800 (7 days). A link created with a secret key opens once; revoke it to end it early, and `expires_at` on the result says when it ends. `ttl` (60 to 300 seconds) is only for sealed single-use embed links; never send both.
156
+
157
+ ```python
158
+ link = client.portal.create_link(
159
+ body=models.PortalLinkCreate(
160
+ organization="org_...", intents=[models.Intent.sso], expires_in=24 * 60 * 60
161
+ )
162
+ )
163
+ print(link.url, link.expires_at)
164
+ ```
165
+
166
+ ## Development
167
+
168
+ Python 3.11 (CI also runs 3.14). From the repository root:
169
+
170
+ ```sh
171
+ python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
172
+ (cd spec-mock && pnpm install --frozen-lockfile) # Node 24; contract tests only
173
+ .venv/bin/ruff format --check . && .venv/bin/ruff check . && .venv/bin/mypy --strict src
174
+ timeout 300 .venv/bin/python -m pytest tests/test_transport.py # targeted
175
+ .venv/bin/python scripts/check_licenses.py
176
+ sh scripts/check-pin.sh && sh scripts/check-drift.sh
177
+ sh scripts/repin.sh <core-ref> # local only, needs ../ohauthcat-core
178
+ .venv/bin/python -m build
179
+ ```
180
+
181
+ Framework examples (FastAPI, Django, Flask) live in `examples/python`. Releases: [RELEASING.md](RELEASING.md).
@@ -0,0 +1,153 @@
1
+ # ohauthcat (Python)
2
+
3
+ Typed sync and async clients for the pinned OhAuthCat versioned operations.
4
+
5
+ ```sh
6
+ pip install ohauthcat
7
+ ```
8
+
9
+ ### Before the first release
10
+
11
+ `ohauthcat` is not published to PyPI yet, so the install command above fails until the first release. Until then, install it from the source repository. The repository is private: you need read access to `bitbucket.org/the-genie-team/ohauthcat-sdk-python` over SSH. Run these commands from your project directory; the `v0.0.2` tag matches the version above (until that tag exists, replace `--branch v0.0.2` with `--branch dev` in `git clone`, and `@v0.0.2` with `@dev` in the `pip install` URL).
12
+
13
+ ```sh
14
+ git clone --depth 1 --branch v0.0.2 git@bitbucket.org:the-genie-team/ohauthcat-sdk-python.git
15
+ pip install ./ohauthcat-sdk-python
16
+ ```
17
+
18
+ Without a clone:
19
+
20
+ ```sh
21
+ pip install "git+ssh://git@bitbucket.org/the-genie-team/ohauthcat-sdk-python.git@v0.0.2"
22
+ ```
23
+
24
+ ```python
25
+ from ohauthcat import OhAuthCat, models
26
+
27
+ with OhAuthCat(api_key="sk_example") as client:
28
+ organization = client.organizations.create_organization(
29
+ body=models.OrganizationCreate(name="Example")
30
+ )
31
+ ```
32
+
33
+ ## Errors
34
+
35
+ API failures raise `OhAuthCatError` (or a subclass) with `code`, `status`, `param`, `doc_url` and `request_id`. Arguments the SDK refuses before sending anything raise `OhAuthCatValidationError`: its `code` is `request_invalid`, `param` names the argument when known and `doc_url` links the catalogue page. It is also a `ValueError`, so existing `except ValueError` handlers keep working.
36
+
37
+ ## Webhooks
38
+
39
+ Verify the exact raw request body before parsing it. Pass every secret you hold during a rotation. Verification raises `WebhookVerificationError` whose `code` is, for example, `signature_invalid` or `timestamp_outside` (older than 300 seconds).
40
+
41
+ ```python
42
+ import os
43
+
44
+ from ohauthcat import webhooks
45
+ from ohauthcat.webhooks import WebhookVerificationError
46
+
47
+
48
+ def handle_webhook(raw_body: bytes, header: str) -> int:
49
+ """raw_body: the exact request bytes; header: the OhAuthCat-Signature header."""
50
+ try:
51
+ event = webhooks.verify(raw_body, header, [os.environ.get("OHAUTHCAT_WEBHOOK_SECRET", "")])
52
+ except WebhookVerificationError as err:
53
+ print("rejected:", err.code)
54
+ return 400
55
+ print(event.type, event.id)
56
+ return 204
57
+ ```
58
+
59
+ `AsyncOhAuthCat` provides the same resource methods with `await` and async context management. `client.events.list()` returns a forward iterator, or async iterator. `client.portal.create_link(...)` aliases the generated portal operation. Original `models` validate requests; `response_models` tolerate additive enum strings while retaining singleton discriminators and field constraints.
60
+
61
+ ## Sign-in
62
+
63
+ Configure `client_id="pk_..."` (and `environment_id="env_..."` to verify access tokens). Every sign-in needs a random `state` that you keep in the user's session and compare on the callback, or an attacker can sign your user into the attacker's account (login CSRF). `get_authorization_url` generates one when you pass none and returns it as `.state`, together with the PKCE `.code_verifier`. Refuse a missing or different state before exchanging the code. The exchange uses the secret API key, is never retried automatically (reusing a code revokes the first grant), and raises `RestartSignIn` when the user must start again.
64
+
65
+ ```python
66
+ import hmac
67
+
68
+ from ohauthcat import OhAuthCat, RestartSignIn
69
+
70
+ REDIRECT_URI = "https://app.example.com/callback"
71
+
72
+
73
+ def start_sign_in(client: OhAuthCat, session: dict[str, str]) -> str:
74
+ """Returns the URL to redirect the browser to. `session` is your server-side session."""
75
+ auth = client.sso.get_authorization_url(redirect_uri=REDIRECT_URI, domain="customer.com")
76
+ session["sso_state"] = auth.state
77
+ session["sso_verifier"] = auth.code_verifier
78
+ return auth.url
79
+
80
+
81
+ def finish_sign_in(client: OhAuthCat, session: dict[str, str], code: str, state: str) -> str:
82
+ expected = session.pop("sso_state", "")
83
+ verifier = session.pop("sso_verifier", "")
84
+ if not expected or not verifier or not hmac.compare_digest(expected.encode(), state.encode()):
85
+ raise PermissionError("sign-in expired, start again") # never exchange the code
86
+ try:
87
+ token = client.sso.get_profile_and_token(
88
+ code=code, code_verifier=verifier, redirect_uri=REDIRECT_URI
89
+ )
90
+ except RestartSignIn:
91
+ raise PermissionError("sign-in expired, start again") from None
92
+ return token.profile.email # create your session for this user
93
+ ```
94
+
95
+ Guests: a sign-in admitted under the organization's guest policy (an email domain outside its verified domains) is identified by `token.profile.access_policy == "guest"` (`AccessPolicy.guest.value` in `ohauthcat.response_models`; response enums decode to plain strings so a value newer than this SDK never fails); for every other sign-in the field is absent. `email_domain_verified` stays true for guests, so check this field when guests must get less access.
96
+
97
+ `client.sso.verify_access_token(token)` verifies ES256 with the configured issuer/client/environment and a bounded JWKS cache fetched from the active trusted broker origin. The configured issuer remains exact when retrieval fails over. `get_sso_profile(access_token=token)` additionally checks the authenticated broker profile. Await both on the async client. Issuer defaults to `https://auth.ohauthcat.com`; explicitly configured HTTP loopback origins support local fixtures. No token-supplied issuer or key URL is trusted.
98
+
99
+ Clients own their default HTTPX clients. Injected `http_client` instances remain caller-owned. Closing the SDK disables requests and token verification. Async requests have an overall deadline and cancellation cleanup. Sync HTTPX uses per-phase timeouts and checks the total budget between chunks/retries, including JWKS lock waits; a blocked network read or custom transport may exceed that total budget before control returns.
100
+
101
+ Upload a logo from bytes or a file path:
102
+
103
+ ```python
104
+ with OhAuthCat(api_key="sk_example") as client:
105
+ logo = client.logos.upload_file("logo.svg", idempotency_key="logo-2026-10")
106
+ # or: client.logos.upload(Path("logo.png").read_bytes())
107
+ client.branding.update_branding(body=models.BrandingUpdate(logo_url=str(logo.logo_url)))
108
+ ```
109
+
110
+ `client.logos.upload(data)` and `upload_file(path)` (awaitable on the async client) refuse, before any request, a file that is empty, larger than 256 KiB, or neither a PNG (by its signature) nor an SVG (by its root element), raising `OhAuthCatError` with code `logo_invalid`, status `0` and param `data`. `check_logo(data)` runs the same check alone. Accepted bytes are base64-encoded and sent to `upload_branding_logo`; the API still validates dimensions and SVG content, and its `logo_invalid` refusal raises the same error type with the HTTP status and message.
111
+
112
+ Install tooling in a repository-local virtual environment:
113
+
114
+ ```sh
115
+ python3 -m venv .venv
116
+ .venv/bin/pip install -e '.[dev]'
117
+ ```
118
+
119
+ Run named test files, for example `.venv/bin/python -m pytest tests/test_transport.py`. The independent contract tests additionally need Node 24 and the development spec mock: `(cd spec-mock && pnpm install --frozen-lockfile)`.
120
+
121
+ Migration helpers are explicitly import-bound: `helper = client.migration(import_id=..., legacy_provider="workos", resolve_legacy_profile=resolver)`. Read `helper.routing.for_organization(org)` and call `helper.migrate_session(legacy_access_token=...)`; await both with the async client. Your resolver authenticates UserInfo and returns the legacy profile ID, while OhAuthCat receives only the import and that ID. Routing cache defaults to 30 seconds (maximum 60); call `helper.routing.invalidate()` after a switch or rollback. Session migration reads a fresh import. Sync resolvers must cooperate with `LegacySessionContext.remaining_seconds()` and `is_cancelled()`; async resolvers must honor cancellation, whose cleanup is awaited. See the OhAuthCat docs for the full contract and direct generated API distinction.
122
+
123
+ Automatic trusted edge failover, configurable fallback origins and request-driven dual health probes use the shared policy in handbook docs/12 §8.5. `standby=False` disables it; custom primary origins require both explicit trusted fallback origins to opt in.
124
+
125
+ ## Portal links
126
+
127
+ A setup link lasts `expires_in` seconds, 300 (5 minutes) to 2,592,000 (30 days); leave it out for the default of 604,800 (7 days). A link created with a secret key opens once; revoke it to end it early, and `expires_at` on the result says when it ends. `ttl` (60 to 300 seconds) is only for sealed single-use embed links; never send both.
128
+
129
+ ```python
130
+ link = client.portal.create_link(
131
+ body=models.PortalLinkCreate(
132
+ organization="org_...", intents=[models.Intent.sso], expires_in=24 * 60 * 60
133
+ )
134
+ )
135
+ print(link.url, link.expires_at)
136
+ ```
137
+
138
+ ## Development
139
+
140
+ Python 3.11 (CI also runs 3.14). From the repository root:
141
+
142
+ ```sh
143
+ python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
144
+ (cd spec-mock && pnpm install --frozen-lockfile) # Node 24; contract tests only
145
+ .venv/bin/ruff format --check . && .venv/bin/ruff check . && .venv/bin/mypy --strict src
146
+ timeout 300 .venv/bin/python -m pytest tests/test_transport.py # targeted
147
+ .venv/bin/python scripts/check_licenses.py
148
+ sh scripts/check-pin.sh && sh scripts/check-drift.sh
149
+ sh scripts/repin.sh <core-ref> # local only, needs ../ohauthcat-core
150
+ .venv/bin/python -m build
151
+ ```
152
+
153
+ Framework examples (FastAPI, Django, Flask) live in `examples/python`. Releases: [RELEASING.md](RELEASING.md).