hearth-sdk 1.0.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.
@@ -0,0 +1,75 @@
1
+ # Rust build artifacts
2
+ /target/
3
+ **/*.rs.bk
4
+ .env
5
+
6
+ # IDE files
7
+ .idea/
8
+ .vscode/
9
+ *.swp
10
+ *.swo
11
+ *~
12
+
13
+ # OS files
14
+ .DS_Store
15
+ Thumbs.db
16
+
17
+ # Coverage
18
+ lcov.info
19
+ *.profraw
20
+
21
+ # Fuzz artifacts
22
+ fuzz/artifacts/
23
+ fuzz/corpus/
24
+
25
+ # Proptest regressions
26
+ **/proptest-regressions/
27
+
28
+ node_modules/
29
+ .reflex/
30
+
31
+ # Tailwind standalone CLI (binary, not committed — install via `make tailwind-install`)
32
+ ui/tailwindcss
33
+
34
+ # Generated protobuf code (produced by build.rs on every build)
35
+ /src/protocol/generated/
36
+
37
+ # Binary symlink at repo root (not the helm chart subdirectory)
38
+ /hearth
39
+ *.wal
40
+
41
+ # Local runtime data (compose named volume + `cargo run` default path)
42
+ /data/
43
+
44
+ *.jpg
45
+ *.jpeg
46
+ *.png
47
+ *.log
48
+
49
+ .playwright-mcp/
50
+
51
+ /hearth.yaml
52
+ /hearth.yml
53
+
54
+ .codex
55
+
56
+ # Backup files
57
+ *.bak
58
+
59
+ # UI test artifacts (generated by make ui-test-smoke)
60
+ tests/ui/.auth/
61
+ tests/ui/reports/
62
+ tests/ui/node_modules/
63
+
64
+ *.tsbuildinfo
65
+ **/.phpunit.cache
66
+
67
+ **/.next/*
68
+ .next
69
+
70
+ go-gin
71
+
72
+ **/php/vendor
73
+ *.cache
74
+
75
+ .claude/scheduled_tasks.lock
@@ -0,0 +1,17 @@
1
+ {
2
+ "branches": [
3
+ "main",
4
+ { "name": "1.x", "range": "1.x.x", "channel": "1.x" },
5
+ { "name": "2.x", "range": "2.x.x", "channel": "2.x" }
6
+ ],
7
+ "tagFormat": "sdk-python-v${version}",
8
+ "plugins": [
9
+ "@semantic-release/commit-analyzer",
10
+ "@semantic-release/release-notes-generator",
11
+ ["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
12
+ ["@semantic-release/exec", {
13
+ "prepareCmd": "sed -i 's/^version = \"[^\"]*\"/version = \"${nextRelease.version}\"/' pyproject.toml"
14
+ }],
15
+ ["@semantic-release/github", { "successComment": false, "failComment": false }]
16
+ ]
17
+ }
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to `hearth-python` are documented here.
4
+
5
+ ## [Unreleased]
6
+
7
+ ### Added
8
+ - **`client.check_permission(access_token, permission, **opts)`** — calls `POST /oauth/authorize`
9
+ for a live per-request permission decision (decision mode). Fail-closed: any network or server
10
+ error returns `CheckPermissionResponse(allowed=False)` rather than raising (HEA-926).
11
+ - **`client.introspect(access_token, client_id, **opts)`** — calls RFC 7662
12
+ `POST /realms/{realm_id}/introspect`. Response includes a `mode` field that middleware
13
+ MUST validate against the configured expected mode (HEA-926).
14
+ - **`RequirePermissionMiddleware`** — ASGI middleware (Starlette, FastAPI) that enforces
15
+ a named permission using an explicit `mode`. Never auto-detects mode from JWT claim
16
+ presence (HEA-926 design constraint).
17
+ - **`WsgiPermissionMiddleware`** — WSGI middleware (Flask, Django) with identical
18
+ mode-awareness contract (HEA-926).
19
+ - **`AuthorizationModeMismatchError`** — raised when the introspection response echoes a
20
+ mode that differs from the configured expectation; middleware maps this to a 403 denial.
21
+ - **`AccessTokenAuthorizationMode`** type alias (`Literal["embedded", "introspection", "decision"]`).
22
+ - **`IntrospectRequest`**, **`IntrospectResponse`**, **`CheckPermissionRequest`**,
23
+ **`CheckPermissionResponse`** Pydantic models.
24
+
25
+ ### Changed
26
+ - SDK brought into conformance with the [Hearth SDK Common Specification](../../docs/specs/SDK.md).
27
+ - All 9 required error types from spec §5 are now exported.
28
+ - Full Claims API (spec §4) implemented on verified token objects.
29
+ - JWKS caching follows the 5-rule contract from spec §2.
30
+ - README updated with installation, quickstart, and troubleshooting sections (spec §10).
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: hearth-sdk
3
+ Version: 1.0.0
4
+ Summary: Hearth identity platform Python SDK
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: pydantic>=2
8
+ Requires-Dist: pyjwt[crypto]>=2.8
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
11
+ Requires-Dist: pytest>=8; extra == 'dev'
@@ -0,0 +1,182 @@
1
+ # Hearth Python SDK
2
+
3
+ Python client for the [Hearth](https://github.com/hearth-auth/hearth) identity API.
4
+
5
+ > **SDK Specification:** This SDK must conform to the [Hearth SDK Common Specification](../../docs/specs/SDK.md).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install hearth-sdk
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```python
16
+ from hearth import HearthClient
17
+
18
+ client = HearthClient(
19
+ issuer_url="https://hearth.example.com",
20
+ client_id="<your-client-id>",
21
+ )
22
+ ```
23
+
24
+ ## Permission delivery modes
25
+
26
+ Hearth supports three permission delivery modes controlled by the `access_token_authorization`
27
+ field on the OAuth client registration. The Python SDK exposes all three via explicit middleware
28
+ and client methods. **Mode is always configured explicitly — the SDK never auto-detects it from
29
+ JWT claim presence.**
30
+
31
+ ### embedded (default)
32
+
33
+ Permissions are embedded in the JWT at issuance. No network call on the hot path.
34
+
35
+ ```python
36
+ from hearth.middleware import WsgiPermissionMiddleware
37
+
38
+ # Flask example
39
+ app.wsgi_app = WsgiPermissionMiddleware(
40
+ app.wsgi_app,
41
+ client=client,
42
+ permission="docs.write",
43
+ mode="embedded",
44
+ )
45
+ ```
46
+
47
+ ### decision
48
+
49
+ The server makes a live per-request decision via `POST /oauth/authorize`. Fail-closed on errors.
50
+
51
+ ```python
52
+ # Starlette / FastAPI example
53
+ from hearth.middleware import RequirePermissionMiddleware
54
+
55
+ app = RequirePermissionMiddleware(
56
+ app,
57
+ client=client,
58
+ permission="docs.write",
59
+ mode="decision",
60
+ )
61
+ ```
62
+
63
+ Or call directly (returns `CheckPermissionResponse(allowed=False)` on any error):
64
+
65
+ ```python
66
+ result = client.check_permission(access_token, "docs.write")
67
+ if not result.allowed:
68
+ raise PermissionError("forbidden")
69
+ ```
70
+
71
+ ### introspection
72
+
73
+ The server introspects the token live via `POST /realms/{realm_id}/introspect` (RFC 7662).
74
+ The response echoes a `mode` field; middleware rejects tokens whose echoed mode does not
75
+ match the configured expectation.
76
+
77
+ ```python
78
+ from hearth.middleware import RequirePermissionMiddleware
79
+
80
+ app = RequirePermissionMiddleware(
81
+ app,
82
+ client=client,
83
+ permission="docs.write",
84
+ mode="introspection",
85
+ client_id="<resource-server-client-id>",
86
+ client_secret="<secret>", # optional for public clients
87
+ )
88
+ ```
89
+
90
+ Or call directly:
91
+
92
+ ```python
93
+ from hearth.errors import AuthorizationModeMismatchError
94
+
95
+ resp = client.introspect(access_token, client_id="<cid>", client_secret="<sec>")
96
+ if not resp.active:
97
+ raise PermissionError("inactive token")
98
+ if resp.mode != "introspection":
99
+ raise AuthorizationModeMismatchError("introspection", resp.mode or "embedded")
100
+ if "docs.write" not in (resp.permissions or []):
101
+ raise PermissionError("forbidden")
102
+ ```
103
+
104
+ ## Troubleshooting
105
+
106
+ **`DiscoveryError`** — verify `issuer_url` is reachable and returns a valid `/.well-known/openid-configuration`.
107
+
108
+ **`JWKSFetchError`** — check network connectivity to the JWKS endpoint. The SDK retries once on a cache miss before returning this error.
109
+
110
+ **`TokenExpiredError`** — the token's `exp` claim is in the past. Refresh the token or re-authenticate.
111
+
112
+ **`TokenInvalidError`** — JWT signature does not match any key in the JWKS. If the server recently rotated keys the SDK will re-fetch once automatically; persistent failures indicate a key mismatch.
113
+
114
+ **`TokenAudienceError`** — the token's `aud` claim does not contain the configured audience. Verify `client_id` matches the audience your authorization server issues.
115
+
116
+ See [docs/specs/SDK.md](../../docs/specs/SDK.md) Section 5 for the full error taxonomy.
117
+
118
+ ---
119
+
120
+ ## Agent Authentication (M5)
121
+
122
+ Enable `agent_auth.capabilities.identity = true` (plus `advanced = true` for AATs/transaction tokens) in `hearth.yaml`.
123
+
124
+ ```python
125
+ import httpx, hashlib, json, base64, time, uuid
126
+ from cryptography.hazmat.primitives.asymmetric import ec
127
+ from cryptography.hazmat.primitives import hashes, serialization
128
+ from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
129
+
130
+ # ── DPoP proof (RFC 9449) ──────────────────────────────────────────────────
131
+ priv = ec.generate_private_key(ec.SECP256R1())
132
+ pub = priv.public_key().public_bytes(
133
+ serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo
134
+ )
135
+ pub_numbers = priv.public_key().public_numbers()
136
+ x = base64.urlsafe_b64encode(pub_numbers.x.to_bytes(32, "big")).rstrip(b"=").decode()
137
+ y = base64.urlsafe_b64encode(pub_numbers.y.to_bytes(32, "big")).rstrip(b"=").decode()
138
+
139
+ canonical = json.dumps({"crv": "P-256", "kty": "EC", "x": x, "y": y}, separators=(",", ":"))
140
+ thumbprint = base64.urlsafe_b64encode(hashlib.sha256(canonical.encode()).digest()).rstrip(b"=").decode()
141
+
142
+ def b64u(obj):
143
+ return base64.urlsafe_b64encode(json.dumps(obj).encode()).rstrip(b"=").decode()
144
+
145
+ def make_dpop_proof(htm: str, htu: str, nonce: str | None = None) -> str:
146
+ header = {"alg": "ES256", "jwk": {"crv": "EC", "kty": "EC", "x": x, "y": y}, "typ": "dpop+jwt"}
147
+ claims = {"htm": htm, "htu": htu, "iat": int(time.time()), "jti": str(uuid.uuid4())}
148
+ if nonce:
149
+ claims["nonce"] = nonce
150
+ signing_input = f"{b64u(header)}.{b64u(claims)}"
151
+ der_sig = priv.sign(signing_input.encode(), ec.ECDSA(hashes.SHA256()))
152
+ r, s = decode_dss_signature(der_sig) # convert DER → raw r||s for JWT
153
+ raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big")
154
+ return f"{signing_input}.{base64.urlsafe_b64encode(raw_sig).rstrip(b'=').decode()}"
155
+
156
+ # ── client_credentials + DPoP ─────────────────────────────────────────────
157
+ token_url = f"{base_url}/realms/{realm_id}/token"
158
+ resp = httpx.post(token_url, data={"grant_type": "client_credentials"}, auth=(client_id, secret),
159
+ headers={"DPoP": make_dpop_proof("POST", token_url)})
160
+ nonce = resp.headers.get("dpop-nonce")
161
+ resp = httpx.post(token_url, data={"grant_type": "client_credentials"}, auth=(client_id, secret),
162
+ headers={"DPoP": make_dpop_proof("POST", token_url, nonce)})
163
+ access_token = resp.json()["access_token"]
164
+ # Decoded JWT claims will contain: cnf.jkt == thumbprint
165
+
166
+ # ── AAT (admin token required for /v1/aats) ────────────────────────────────
167
+ root_aat = httpx.post(f"{base_url}/v1/aats", json={
168
+ "realm_id": realm_id, "agent_id": agent_id,
169
+ "tools": [{"tool_name": "read_docs", "constraints": None}],
170
+ "expires_in_secs": 3600,
171
+ }, headers={"Authorization": f"Bearer {admin_token}"}).json()
172
+
173
+ # ── Transaction token ──────────────────────────────────────────────────────
174
+ txn = httpx.post(f"{base_url}/v1/transaction-tokens", json={
175
+ "realm_id": realm_id,
176
+ "requesting_agent_id": agent_a_id,
177
+ "target_agent_id": agent_b_id,
178
+ "txn_id": f"txn-{uuid.uuid4()}",
179
+ }, headers={"Authorization": f"Bearer {admin_token}"}).json()
180
+ ```
181
+
182
+ For the full surface (draft tracking, RFC 8693 exchange, Agent Card), see the [TypeScript SDK README](../typescript/README.md#agent-authentication-m5).
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@hearth-auth/sdk-python",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Hearth Python SDK — package.json shim for multi-semantic-release"
6
+ }
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "hearth-sdk"
3
+ version = "1.0.0"
4
+ description = "Hearth identity platform Python SDK"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "httpx>=0.27",
8
+ "pyjwt[crypto]>=2.8",
9
+ "pydantic>=2",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ dev = [
14
+ "pytest>=8",
15
+ "pytest-asyncio>=0.24",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/hearth"]
@@ -0,0 +1,113 @@
1
+ """Hearth identity platform Python SDK.
2
+
3
+ Provides HearthClient (auth flows, RBAC predicates), AdminClient
4
+ (user/realm CRUD), mode-aware middleware, and all request/response types.
5
+ """
6
+
7
+ from .client import HearthClient
8
+ from .admin import AdminClient
9
+ from .errors import (
10
+ HearthError,
11
+ HearthSdkError,
12
+ ConfigurationError,
13
+ DiscoveryError,
14
+ JWKSFetchError,
15
+ TokenExpiredError,
16
+ TokenNotYetValidError,
17
+ TokenInvalidError,
18
+ TokenIssuerError,
19
+ TokenAudienceError,
20
+ IntrospectionError,
21
+ RequiredActionError,
22
+ AuthorizationModeMismatchError,
23
+ )
24
+ from .claims import Claims
25
+ from .middleware import RequirePermissionMiddleware, WsgiPermissionMiddleware
26
+ from .types import (
27
+ AccessTokenAuthorizationMode,
28
+ BootstrapResponse,
29
+ User,
30
+ CreateUserRequest,
31
+ UpdateUserRequest,
32
+ Realm,
33
+ CreateRealmRequest,
34
+ UpdateRealmRequest,
35
+ PageResponse,
36
+ AuthorizeResponse,
37
+ TokenResponse,
38
+ UserInfoResponse,
39
+ MePermissionsResponse,
40
+ OAuthClient,
41
+ RegisterClientRequest,
42
+ CreateClientRequest,
43
+ UpdateClientRequest,
44
+ Role,
45
+ CreateRoleRequest,
46
+ UpdateRoleRequest,
47
+ Group,
48
+ CreateGroupRequest,
49
+ UpdateGroupRequest,
50
+ OrgMember,
51
+ AddOrgMemberRequest,
52
+ JwksDocument,
53
+ IntrospectRequest,
54
+ IntrospectResponse,
55
+ CheckPermissionRequest,
56
+ CheckPermissionResponse,
57
+ )
58
+
59
+ __all__ = [
60
+ # Clients
61
+ "HearthClient",
62
+ "AdminClient",
63
+ # Middleware
64
+ "RequirePermissionMiddleware",
65
+ "WsgiPermissionMiddleware",
66
+ # Errors
67
+ "HearthError",
68
+ "HearthSdkError",
69
+ "ConfigurationError",
70
+ "DiscoveryError",
71
+ "JWKSFetchError",
72
+ "TokenExpiredError",
73
+ "TokenNotYetValidError",
74
+ "TokenInvalidError",
75
+ "TokenIssuerError",
76
+ "TokenAudienceError",
77
+ "IntrospectionError",
78
+ "RequiredActionError",
79
+ "AuthorizationModeMismatchError",
80
+ # Claims
81
+ "Claims",
82
+ # Types
83
+ "AccessTokenAuthorizationMode",
84
+ "BootstrapResponse",
85
+ "User",
86
+ "CreateUserRequest",
87
+ "UpdateUserRequest",
88
+ "Realm",
89
+ "CreateRealmRequest",
90
+ "UpdateRealmRequest",
91
+ "PageResponse",
92
+ "AuthorizeResponse",
93
+ "TokenResponse",
94
+ "UserInfoResponse",
95
+ "MePermissionsResponse",
96
+ "OAuthClient",
97
+ "RegisterClientRequest",
98
+ "CreateClientRequest",
99
+ "UpdateClientRequest",
100
+ "Role",
101
+ "CreateRoleRequest",
102
+ "UpdateRoleRequest",
103
+ "Group",
104
+ "CreateGroupRequest",
105
+ "UpdateGroupRequest",
106
+ "OrgMember",
107
+ "AddOrgMemberRequest",
108
+ "JwksDocument",
109
+ "IntrospectRequest",
110
+ "IntrospectResponse",
111
+ "CheckPermissionRequest",
112
+ "CheckPermissionResponse",
113
+ ]