chitmark 0.6.0__tar.gz → 0.6.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.
@@ -94,3 +94,8 @@ lerna-debug.log*
94
94
  # Tinybird CLI local auth (contains tokens)
95
95
  .tinyb
96
96
  tinybird/.tinyb
97
+ .repo-staging
98
+ _publish
99
+ .sync-push
100
+ .labs/
101
+ .routing-lab/
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: chitmark
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Official Python SDK for Chitmark by Open Agent Ledger: trust decisions on agent-mediated actions (verify, feedback, challenge)
5
5
  Project-URL: Homepage, https://chitmark.com
6
6
  Project-URL: Documentation, https://chitmark.com/docs
7
7
  Author-email: Open Agent Ledger <dev@chitmark.com>
8
8
  License: Proprietary: no open-source grant. Copyright (c) 2026 Open Agent Ledger. All rights reserved.
9
9
  License-File: LICENSE
10
- Keywords: agent-actions,anti-abuse,chitmark,open-agent-ledger,trust-decisions
10
+ Keywords: agent-actions,allow-challenge-deny,anti-abuse,chitmark,credit-abuse,free-tier-abuse,free-trial-abuse,open-agent-ledger,signup-abuse,trial-farming,trust-decisions
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "chitmark"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "Official Python SDK for Chitmark by Open Agent Ledger: trust decisions on agent-mediated actions (verify, feedback, challenge)"
9
9
  readme = "README.md"
10
10
  license = { text = "Proprietary: no open-source grant. Copyright (c) 2026 Open Agent Ledger. All rights reserved." }
@@ -18,6 +18,12 @@ keywords = [
18
18
  "agent-actions",
19
19
  "trust-decisions",
20
20
  "anti-abuse",
21
+ "free-trial-abuse",
22
+ "trial-farming",
23
+ "signup-abuse",
24
+ "credit-abuse",
25
+ "free-tier-abuse",
26
+ "allow-challenge-deny",
21
27
  ]
22
28
  classifiers = [
23
29
  "Development Status :: 3 - Alpha",
@@ -272,7 +272,12 @@ class Chitmark:
272
272
  body["idempotencyKey"] = idempotency_key
273
273
  return cast(
274
274
  FeedbackResponse,
275
- self._request("POST", "/v1/feedback", body=body, idempotency_key=idempotency_key),
275
+ self._request(
276
+ "POST",
277
+ "/v1/feedback",
278
+ body=body,
279
+ idempotency_key=idempotency_key,
280
+ ),
276
281
  )
277
282
 
278
283
  def challenge(
@@ -76,6 +76,7 @@ class VerdictTokenClaims(TypedDict, total=False):
76
76
  confidence: float
77
77
  aud: str
78
78
  sub: str
79
+ tid: str
79
80
  jti: str
80
81
  iat: int
81
82
  exp: int
@@ -1,11 +1,13 @@
1
1
  """Client-side verdictToken verification (MVP-016).
2
2
 
3
3
  A verdictToken is an ES256 JWT decision receipt minted by the edge and bound
4
- to session (``sub``), origin (``aud``), and ``jti`` with a short ``exp``. This
5
- module validates shape, signature (against the JWKS served from
6
- ``GET /.well-known/jwks.json``), expiry, session binding, and, when an
7
- expected audience is supplied, origin binding. Rejection reasons are surfaced
8
- as ``VerdictTokenError.code``.
4
+ to session (``sub``), origin hint (``aud``), issuing tenant (``tid``), and
5
+ ``jti`` with a short ``exp``. This module validates shape, signature (against
6
+ the JWKS served from ``GET /.well-known/jwks.json``), expiry, session
7
+ binding, and, when expected values are supplied, tenant and origin binding.
8
+ ``aud`` reflects request headers and is attacker influenced: always verify
9
+ ``session`` and ``tenant_id``, never ``aud`` alone. Rejection reasons are
10
+ surfaced as ``VerdictTokenError.code``.
9
11
 
10
12
  Requires the optional extras ``PyJWT`` and ``cryptography``
11
13
  (``uv sync --extra verdict``).
@@ -46,6 +48,7 @@ def verify_verdict_token(
46
48
  token: str,
47
49
  *,
48
50
  session: str | None = None,
51
+ tenant_id: str | None = None,
49
52
  aud: str | None = None,
50
53
  jwks: Mapping[str, Any] | None = None,
51
54
  jwks_url: str = DEFAULT_JWKS_URL,
@@ -57,7 +60,8 @@ def verify_verdict_token(
57
60
  Args:
58
61
  token: The ES256 JWT from a verify verdict.
59
62
  session: Expected session; token ``sub`` must equal this.
60
- aud: Expected origin; token ``aud`` must equal this when provided.
63
+ tenant_id: Expected issuing tenant; token ``tid`` must equal this.
64
+ aud: Expected origin hint; token ``aud`` must equal this when provided.
61
65
  jwks: Pre-fetched JWKS mapping; skips the network fetch.
62
66
  jwks_url: JWKS endpoint; defaults to the production API.
63
67
  http_client: Reusable ``httpx.Client`` for the JWKS fetch.
@@ -65,9 +69,10 @@ def verify_verdict_token(
65
69
 
66
70
  Raises:
67
71
  VerdictTokenError: with a machine-readable ``code``:
68
- ``malformed_token``, ``wrong_algorithm``, ``jwks_unavailable``,
69
- ``unknown_kid``, ``invalid_signature``, ``expired``,
70
- ``not_yet_valid``, ``wrong_session``, or ``wrong_aud``.
72
+ ``malformed_token``, ``wrong_algorithm``, ``wrong_type``,
73
+ ``jwks_unavailable``, ``unknown_kid``, ``invalid_signature``,
74
+ ``expired``, ``not_yet_valid``, ``wrong_session``,
75
+ ``wrong_tenant``, or ``wrong_aud``.
71
76
  """
72
77
  if pyjwt is None: # pragma: no cover
73
78
  raise VerdictTokenError(
@@ -84,6 +89,14 @@ def verify_verdict_token(
84
89
  f"verdictToken must use ES256, got {alg!r}",
85
90
  code="wrong_algorithm",
86
91
  )
92
+ # RFC 8725 section 3.11: explicit typing keeps this receipt distinct
93
+ # from any other JWT a tenant might accept. The edge always mints
94
+ # typ "JWT".
95
+ if header.get("typ") != "JWT":
96
+ raise VerdictTokenError(
97
+ f"verdictToken must be a JWT, got {header.get('typ')!r}",
98
+ code="wrong_type",
99
+ )
87
100
  kid = header.get("kid")
88
101
  if not isinstance(kid, str) or not kid:
89
102
  raise VerdictTokenError(
@@ -132,6 +145,11 @@ def verify_verdict_token(
132
145
  "verdictToken was minted for a different session",
133
146
  code="wrong_session",
134
147
  )
148
+ if tenant_id is not None and claims.get("tid") != tenant_id:
149
+ raise VerdictTokenError(
150
+ "verdictToken was minted for a different tenant",
151
+ code="wrong_tenant",
152
+ )
135
153
  if aud is not None and claims.get("aud") != aud:
136
154
  raise VerdictTokenError(
137
155
  "verdictToken was minted for a different origin",
@@ -94,6 +94,7 @@ def _mint(keys: dict, **overrides: object) -> str:
94
94
  "confidence": 0.56,
95
95
  "aud": "https://app.acme.com",
96
96
  "sub": "sess_token_a",
97
+ "tid": "org_unit",
97
98
  "jti": "jti_1",
98
99
  "iat": now,
99
100
  "exp": now + 300,
@@ -144,6 +145,23 @@ def test_rejects_wrong_audience(keys: dict) -> None:
144
145
  assert exc.value.code == "wrong_aud"
145
146
 
146
147
 
148
+ def test_rejects_wrong_tenant(keys: dict) -> None:
149
+ token = _mint(keys, tid="org_other")
150
+
151
+ with pytest.raises(VerdictTokenError) as exc:
152
+ verify_verdict_token(token, tenant_id="org_unit", jwks=_jwks(keys))
153
+ assert exc.value.code == "wrong_tenant"
154
+
155
+
156
+ def test_accepts_expected_tenant(keys: dict) -> None:
157
+ token = _mint(keys, tid="org_unit", sub="sess_token_a")
158
+
159
+ claims = verify_verdict_token(
160
+ token, session="sess_token_a", tenant_id="org_unit", jwks=_jwks(keys)
161
+ )
162
+ assert claims["tid"] == "org_unit"
163
+
164
+
147
165
  def test_rejects_expired_token(keys: dict) -> None:
148
166
  token = _mint(keys, exp=int(time.time()) - 120)
149
167
 
@@ -184,6 +202,17 @@ def test_rejects_wrong_algorithm(keys: dict) -> None:
184
202
  assert exc.value.code == "wrong_algorithm"
185
203
 
186
204
 
205
+ def test_rejects_missing_jwt_typ(keys: dict) -> None:
206
+ token = _mint(keys)
207
+ header, payload, signature = token.split(".")
208
+ wrong_header = _b64url_json({"alg": "ES256", "typ": "at+jwt", "kid": keys["kid"]})
209
+ wrong = f"{wrong_header}.{payload}.{signature}"
210
+
211
+ with pytest.raises(VerdictTokenError) as exc:
212
+ verify_verdict_token(wrong, jwks=_jwks(keys))
213
+ assert exc.value.code == "wrong_type"
214
+
215
+
187
216
  def test_rejects_malformed_token() -> None:
188
217
  with pytest.raises(VerdictTokenError) as exc:
189
218
  verify_verdict_token("not-a-jwt", jwks={"keys": []})
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes