idside 0.1.0.dev0__py3-none-any.whl

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.
idside/__about__.py ADDED
@@ -0,0 +1,5 @@
1
+ # Patents: https://sdk.idside.eu/patents
2
+ # This software may be covered by the patents and applications listed there.
3
+
4
+ # Single source of truth for the version (read by hatchling and by __init__).
5
+ __version__ = "0.1.0.dev0"
idside/__init__.py ADDED
@@ -0,0 +1,158 @@
1
+ # Patents: https://sdk.idside.eu/patents
2
+ # This software may be covered by the patents and applications listed there.
3
+
4
+ """ID Side — AI Consent Infrastructure SDK.
5
+
6
+ Intended usage::
7
+
8
+ from openai import OpenAI
9
+ from idside import consent
10
+
11
+ client = consent.wrap(OpenAI())
12
+ client.chat.completions.create(..., user_id="usr_marie_456")
13
+
14
+ Anyone holding a proof can check it offline, with no call to ID Side::
15
+
16
+ from idside import verify_act
17
+
18
+ proof = verify_act(token, jwks=jwks, request_hash=request_hash)
19
+
20
+ Invariants (see docs/versioning.md §3): opaque tunnel (no preference setter),
21
+ fail-closed, one ACT token = one request. No private key in this package: it
22
+ verifies proofs, it never issues them.
23
+
24
+ Licensed under the ID Side Source-Available License, version 1.0: see LICENSE
25
+ and NOTICE in this distribution.
26
+
27
+ Patents: https://sdk.idside.eu/patents
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from idside import consent, verify
33
+ from idside.__about__ import __version__
34
+ from idside._transport import (
35
+ DEFAULT_BASE_URL,
36
+ AsyncConsentClient,
37
+ AsyncHttpConsentClient,
38
+ ConsentClient,
39
+ ConsentDecision,
40
+ HttpConsentClient,
41
+ )
42
+ from idside.adapters import ProviderAdapter
43
+ from idside.exceptions import (
44
+ ActAlgorithmError,
45
+ ActBindingMismatchError,
46
+ ActExpiredError,
47
+ ActIssuerError,
48
+ ActKeyNotFoundError,
49
+ ActMalformedError,
50
+ ActNotYetValidError,
51
+ ActPrivateKeyInKeySetError,
52
+ ActSignatureError,
53
+ ActTypeMismatchError,
54
+ ActVerificationError,
55
+ ActVerificationUnavailableError,
56
+ AuthenticationError,
57
+ BindingError,
58
+ BindingRequiredError,
59
+ ConflictingIdentityError,
60
+ ConsentDeniedError,
61
+ ConsentRefreshRequiredError,
62
+ ConsentTokenError,
63
+ ConsentUnreachableError,
64
+ ContractError,
65
+ CredentialRoutingError,
66
+ IDSideError,
67
+ InvalidBindingError,
68
+ MissingCredentialError,
69
+ MissingIdentityError,
70
+ QuotaExceededError,
71
+ RateLimitedError,
72
+ ToolUseRefusedError,
73
+ UnenforceableOptionError,
74
+ UnenforcedCallError,
75
+ UnsupportedProviderError,
76
+ )
77
+ from idside.options import (
78
+ ConsentOption,
79
+ Disclosure,
80
+ catalogue,
81
+ decidable_options,
82
+ disclosures,
83
+ )
84
+ from idside.verify import (
85
+ ACT_ALGORITHM,
86
+ ACT_TYPE,
87
+ DEFAULT_LEEWAY_SECONDS,
88
+ PRODUCTION_ISSUER,
89
+ TEST_ISSUER,
90
+ VerifiedAct,
91
+ verify_act,
92
+ )
93
+
94
+ __all__ = [
95
+ # Constants a caller needs to state what it is verifying against, exported
96
+ # from the package root so both SDKs present the same surface.
97
+ "ACT_ALGORITHM",
98
+ "ACT_TYPE",
99
+ # `DEFAULT_BASE_URL`, `ConsentClient`, `AsyncConsentClient`,
100
+ # `ConsentDecision`, `HttpConsentClient` and `AsyncHttpConsentClient` are the
101
+ # consent-client surface, for the supported `consent_client=` injection: the
102
+ # protocols to implement, the decision to return, and the default transport
103
+ # with its `timeout=`. Annotating or building any of them used to mean
104
+ # importing `idside._transport`, a private module under no compatibility
105
+ # promise, for something the documentation invites callers to do.
106
+ "DEFAULT_BASE_URL",
107
+ "DEFAULT_LEEWAY_SECONDS",
108
+ "PRODUCTION_ISSUER",
109
+ "TEST_ISSUER",
110
+ "ActAlgorithmError",
111
+ "ActBindingMismatchError",
112
+ "ActExpiredError",
113
+ "ActIssuerError",
114
+ "ActKeyNotFoundError",
115
+ "ActMalformedError",
116
+ "ActNotYetValidError",
117
+ "ActPrivateKeyInKeySetError",
118
+ "ActSignatureError",
119
+ "ActTypeMismatchError",
120
+ "ActVerificationError",
121
+ "ActVerificationUnavailableError",
122
+ "AsyncConsentClient",
123
+ "AsyncHttpConsentClient",
124
+ "AuthenticationError",
125
+ "BindingError",
126
+ "BindingRequiredError",
127
+ "ConflictingIdentityError",
128
+ "ConsentClient",
129
+ "ConsentDecision",
130
+ "ConsentDeniedError",
131
+ "ConsentOption",
132
+ "ConsentRefreshRequiredError",
133
+ "ConsentTokenError",
134
+ "ConsentUnreachableError",
135
+ "ContractError",
136
+ "CredentialRoutingError",
137
+ "Disclosure",
138
+ "HttpConsentClient",
139
+ "IDSideError",
140
+ "InvalidBindingError",
141
+ "MissingCredentialError",
142
+ "MissingIdentityError",
143
+ "ProviderAdapter",
144
+ "QuotaExceededError",
145
+ "RateLimitedError",
146
+ "ToolUseRefusedError",
147
+ "UnenforceableOptionError",
148
+ "UnenforcedCallError",
149
+ "UnsupportedProviderError",
150
+ "VerifiedAct",
151
+ "__version__",
152
+ "catalogue",
153
+ "consent",
154
+ "decidable_options",
155
+ "disclosures",
156
+ "verify",
157
+ "verify_act",
158
+ ]
idside/_destination.py ADDED
@@ -0,0 +1,165 @@
1
+ # Patents: https://sdk.idside.eu/patents
2
+ # This software may be covered by the patents and applications listed there.
3
+
4
+ """Where a wrapped client sends its requests, reduced to what can be compared.
5
+
6
+ The net used to bind an authorized request to its method and path, and never to
7
+ its host. A per-call ``server_url``, a derived client pointed elsewhere, a
8
+ ``client.base_url = …`` after ``wrap()``: each sent the prompt, the ACT token
9
+ and the provider key to an address nobody had wrapped, carrying a token that
10
+ matched. So ``wrap()`` freezes the client's origin — scheme, host and effective
11
+ port, and nothing else of the URL — and the net refuses a body-bearing request
12
+ bound anywhere else.
13
+
14
+ One module for both halves on purpose: the origin frozen from a client's base
15
+ URL and the origin read off an outgoing request must be normalised the same
16
+ way, or the comparison refuses the client's own traffic.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any, NamedTuple
22
+
23
+ import httpx
24
+
25
+ __all__ = ["Origin", "client_origin", "origin_of", "unguarded_route"]
26
+
27
+ _DEFAULT_PORTS = {"http": 80, "https": 443}
28
+
29
+
30
+ class Origin(NamedTuple):
31
+ """Scheme, host and effective port: what a URL says about who receives it."""
32
+
33
+ scheme: str
34
+ host: str
35
+ port: int | None
36
+
37
+ def __str__(self) -> str:
38
+ """The origin as a URL writes it, for a message a developer reads.
39
+
40
+ No port when there is none or it is the scheme's default: ``host:None``
41
+ names no address, and ``:443`` only adds noise to one. An IPv6 host is
42
+ bracketed, or its colons would read as the port.
43
+ """
44
+ host = f"[{self.host}]" if ":" in self.host else self.host
45
+ port = (
46
+ ""
47
+ if self.port is None or self.port == _DEFAULT_PORTS.get(self.scheme)
48
+ else f":{self.port}"
49
+ )
50
+ return f"{self.scheme}://{host}{port}"
51
+
52
+
53
+ def origin_of(url: Any) -> Origin | None:
54
+ """The origin of a URL object or string, or None if it names no host.
55
+
56
+ ``httpx.URL`` and ``httpx2.URL`` are read through their attributes, which
57
+ already hold the parsed host — never through ``str()``, so a request's query
58
+ string is not so much as formatted. A string goes through ``httpx.URL``: the
59
+ same parser as the requests it is compared with, since two parsers disagree
60
+ on an internationalised host — ``urlsplit`` keeps ``xn--bcher-kva.example``
61
+ as written where ``httpx`` decodes it to ``bücher.example``, and every call
62
+ of a client configured in punycode was refused.
63
+ """
64
+ if url is None:
65
+ return None
66
+ scheme = getattr(url, "scheme", None)
67
+ host = getattr(url, "host", None)
68
+ port: Any
69
+ if isinstance(scheme, str) and isinstance(host, str):
70
+ port = getattr(url, "port", None)
71
+ else:
72
+ try:
73
+ parsed = httpx.URL(str(url))
74
+ except httpx.InvalidURL: # a port that is not a number is no origin at all
75
+ return None
76
+ scheme, host, port = parsed.scheme, parsed.host, parsed.port
77
+ if not scheme or not host:
78
+ return None
79
+ scheme = scheme.lower()
80
+ return Origin(
81
+ scheme,
82
+ host.lower(),
83
+ port if isinstance(port, int) else _DEFAULT_PORTS.get(scheme),
84
+ )
85
+
86
+
87
+ def client_origin(client: Any) -> Origin | None:
88
+ """The origin a provider client sends to, or None if it does not say.
89
+
90
+ Read from the attribute each library itself builds URLs from — never from
91
+ the transport, whose own ``base_url`` the libraries do not use and which a
92
+ derived client shares with its parent. ``google-genai`` keeps it in
93
+ ``_api_client._http_options``, ``mistralai`` in ``sdk_configuration``, the
94
+ Stainless clients (``openai``, ``anthropic``) in ``base_url``.
95
+ """
96
+ state = getattr(client, "__dict__", None)
97
+ if not isinstance(state, dict):
98
+ return None
99
+ api_client = state.get("_api_client")
100
+ if api_client is not None:
101
+ options = getattr(api_client, "_http_options", None)
102
+ return origin_of(getattr(options, "base_url", None))
103
+ configuration = state.get("sdk_configuration")
104
+ if configuration is not None:
105
+ try:
106
+ server_url, _variables = configuration.get_server_details()
107
+ except Exception:
108
+ return None
109
+ return origin_of(server_url)
110
+ try:
111
+ return origin_of(getattr(client, "base_url", None))
112
+ except Exception:
113
+ # A `base_url` property is code; one that raises names no origin.
114
+ return None
115
+
116
+
117
+ #: Modes in which ``google-genai`` sends through something that is not an HTTP
118
+ #: client the net can recognise, with what to do instead.
119
+ _GENAI_UNGUARDED = (
120
+ (
121
+ "_use_google_auth_sync",
122
+ "sends on google-auth's `requests` session (Vertex AI with mTLS)",
123
+ "configure mTLS on an httpx.Client and pass it as "
124
+ "http_options=HttpOptions(httpx_client=…)",
125
+ ),
126
+ (
127
+ "_use_aiohttp",
128
+ "sends its async requests on aiohttp",
129
+ "pass http_options=HttpOptions(httpx_async_client=httpx.AsyncClient())",
130
+ ),
131
+ (
132
+ # Not implied by `_use_aiohttp`: an `async_client_args` transport turns
133
+ # that one off, and leaves this one on.
134
+ "_use_google_auth_async",
135
+ "sends its async requests on google-auth's aiohttp session "
136
+ "(Vertex AI with mTLS)",
137
+ "configure mTLS on an httpx.AsyncClient and pass it as "
138
+ "http_options=HttpOptions(httpx_async_client=…)",
139
+ ),
140
+ )
141
+
142
+
143
+ def unguarded_route(client: Any) -> str | None:
144
+ """Describe a route out of ``client`` the net cannot guard, if it has one.
145
+
146
+ The net recognises HTTP clients by shape, and patches the ones a client
147
+ holds. A client that sends through something else — a ``requests`` session,
148
+ an ``aiohttp`` session created on first use — has a route no patch reaches,
149
+ and wrapping it would claim a coverage that is not there.
150
+ """
151
+ state = getattr(client, "__dict__", None)
152
+ api_client = state.get("_api_client") if isinstance(state, dict) else None
153
+ if api_client is None:
154
+ return None
155
+ for probe, route, remedy in _GENAI_UNGUARDED:
156
+ check = getattr(api_client, probe, None)
157
+ if not callable(check):
158
+ continue
159
+ try:
160
+ unguarded = bool(check())
161
+ except Exception:
162
+ unguarded = True # a mode that cannot be asked cannot be ruled out
163
+ if unguarded:
164
+ return f"it {route}. To wrap it, {remedy}"
165
+ return None