memydev-auth-sdk 0.1.0__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.
memyauth/__init__.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ @fileoverview Official MemyAuth Python SDK — async-first with a mechanically generated sync facade.
3
+ @module memyauth
4
+ @description Public entrypoint. Async: `memyauth.MemyAuth`. Sync: `memyauth.sync.MemyAuth`. The
5
+ async modules (`_client.py`, `_discovery.py`, `_oidc.py`, `_iam.py`, `_idtoken.py`,
6
+ `_transport.py`) are the ONLY hand-written client surface; `_sync/*.py` is mechanically derived
7
+ by `scripts/gen_sync.py` — never hand-edit it. `_roles.py`, `_session.py`, `_state.py`,
8
+ `_config.py`, `_errors.py`, `_types.py`, `_pkce.py` are pure/synchronous and shared verbatim by
9
+ both surfaces (no twin exists or is needed).
10
+ @created 2026-09-06
11
+ """
12
+ from ._client import MemyAuth, SDK_VERSION
13
+ from ._config import ResolvedConfig, RetryConfig, resolve_config
14
+ from ._discovery import DiscoveryService
15
+ from ._errors import (
16
+ BadRequestError,
17
+ ConfigError,
18
+ DiscoveryError,
19
+ DiscoveryEscapeError,
20
+ ForbiddenError,
21
+ GoneError,
22
+ IdTokenError,
23
+ IssuerMismatchError,
24
+ MemyAuthError,
25
+ NetworkError,
26
+ NotFoundError,
27
+ ProtocolError,
28
+ RefreshRejectedError,
29
+ ServiceUnavailableError,
30
+ SessionTokenError,
31
+ StateCookieError,
32
+ StateMismatchError,
33
+ TimeoutError,
34
+ TokenExchangeError,
35
+ UnauthorizedError,
36
+ UnsupportedAlgorithmError,
37
+ UnsupportedAlgorithmPinError,
38
+ UserInfoError,
39
+ decode_error,
40
+ )
41
+ from ._iam import IamService
42
+ from ._idtoken import IdTokenVerifier
43
+ from ._oidc import OidcService
44
+ from ._pkce import SystemRandomSource, generate_code_challenge, generate_code_verifier
45
+ from ._roles import CONSUMER_ROLES, DEFAULT_CONSUMER_ROLE, DEFAULT_ROLE_MAP, GLOBAL_ADMIN_IDP_ROLES, RoleMapper
46
+ from ._session import SessionTokenService
47
+ from ._state import StateCookieService
48
+ from ._types import (
49
+ AuthorizationRequest,
50
+ Clock,
51
+ DiscoveryHealth,
52
+ IamOrgEntry,
53
+ IamSession,
54
+ IamTenant,
55
+ IamUserContext,
56
+ IdpTokenClaims,
57
+ OidcDiscoveryDocument,
58
+ OidcTokenResponse,
59
+ RandomSource,
60
+ SessionAccessClaims,
61
+ SessionAccessInput,
62
+ SessionRefreshClaims,
63
+ SessionRefreshInput,
64
+ StateCookieInput,
65
+ StateCookiePayload,
66
+ SystemClock,
67
+ UserContext,
68
+ UserContextOrgEntry,
69
+ UserContextTenant,
70
+ UserInfo,
71
+ )
72
+
73
+ __version__ = SDK_VERSION
74
+
75
+ __all__ = [
76
+ "MemyAuth",
77
+ "SDK_VERSION",
78
+ "__version__",
79
+ "resolve_config",
80
+ "ResolvedConfig",
81
+ "RetryConfig",
82
+ "DiscoveryService",
83
+ "OidcService",
84
+ "IamService",
85
+ "IdTokenVerifier",
86
+ "RoleMapper",
87
+ "DEFAULT_ROLE_MAP",
88
+ "CONSUMER_ROLES",
89
+ "DEFAULT_CONSUMER_ROLE",
90
+ "GLOBAL_ADMIN_IDP_ROLES",
91
+ "SessionTokenService",
92
+ "StateCookieService",
93
+ "generate_code_verifier",
94
+ "generate_code_challenge",
95
+ "SystemRandomSource",
96
+ # errors
97
+ "MemyAuthError",
98
+ "ConfigError",
99
+ "UnsupportedAlgorithmPinError",
100
+ "NetworkError",
101
+ "TimeoutError",
102
+ "ProtocolError",
103
+ "BadRequestError",
104
+ "UnauthorizedError",
105
+ "ForbiddenError",
106
+ "NotFoundError",
107
+ "GoneError",
108
+ "ServiceUnavailableError",
109
+ "DiscoveryError",
110
+ "IssuerMismatchError",
111
+ "DiscoveryEscapeError",
112
+ "TokenExchangeError",
113
+ "RefreshRejectedError",
114
+ "UserInfoError",
115
+ "IdTokenError",
116
+ "UnsupportedAlgorithmError",
117
+ "SessionTokenError",
118
+ "StateCookieError",
119
+ "StateMismatchError",
120
+ "decode_error",
121
+ # types
122
+ "Clock",
123
+ "RandomSource",
124
+ "SystemClock",
125
+ "OidcDiscoveryDocument",
126
+ "DiscoveryHealth",
127
+ "AuthorizationRequest",
128
+ "OidcTokenResponse",
129
+ "UserInfo",
130
+ "UserContext",
131
+ "UserContextTenant",
132
+ "UserContextOrgEntry",
133
+ "IamUserContext",
134
+ "IamSession",
135
+ "IamOrgEntry",
136
+ "IamTenant",
137
+ "SessionAccessInput",
138
+ "SessionAccessClaims",
139
+ "SessionRefreshInput",
140
+ "SessionRefreshClaims",
141
+ "StateCookieInput",
142
+ "StateCookiePayload",
143
+ "IdpTokenClaims",
144
+ ]
memyauth/_client.py ADDED
@@ -0,0 +1,169 @@
1
+ """
2
+ @fileoverview Top-level `MemyAuth` async client — composes the seven capability services.
3
+ @module memyauth._client
4
+ @description [async -> _sync/_client.py twin via scripts/gen_sync.py]. `SDK_VERSION` lives HERE
5
+ (design §G.5's six-file version-parity check reads it via
6
+ `/SDK_VERSION\\s*=\\s*["\\']([^"\\']+)["\\']/`). `.discovery`, `.oidc`, `.iam` and `.roles` are
7
+ EAGER (built in `__init__`) because their prerequisites are always present once `resolve_config`
8
+ succeeds; `.session`, `.state` and `.id_token` are LAZY properties — `.session`/`.state` raise
9
+ `ConfigError` at first access when their signing secret was never configured (an IAM-only
10
+ consumer should never need one), while `.id_token` never raises (`id_token_validation` is a
11
+ required, already-validated field, so its prerequisite is always satisfiable) but is still built
12
+ lazily and cached, purely to avoid touching `discovery` before it is needed.
13
+ @dependencies ._config, ._discovery, ._oidc, ._iam, ._idtoken, ._roles, ._session, ._state
14
+ @relatedFiles __init__.py (re-exports MemyAuth + SDK_VERSION), sync/__init__.py (the generated
15
+ sync facade), _config.py (resolve_config), tests/test_client.py
16
+ @created 2026-09-06
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Optional
21
+
22
+ # ABSOLUTE imports for the shared/pure modules; RELATIVE for the sibling async modules that DO
23
+ # get _sync/ twins, so the generated _sync/_client.py binds to ITS siblings in _sync/ (see
24
+ # _transport.py's header for why this split is required).
25
+ from memyauth._config import ResolvedConfig, resolve_config
26
+ from memyauth._errors import ConfigError
27
+ from memyauth._roles import RoleMapper
28
+ from memyauth._session import SessionTokenService
29
+ from memyauth._state import StateCookieService
30
+
31
+ from ._discovery import DiscoveryService
32
+ from ._iam import IamService
33
+ from ._idtoken import IdTokenVerifier
34
+ from ._oidc import OidcService
35
+
36
+ __all__ = ["MemyAuth", "SDK_VERSION"]
37
+
38
+ SDK_VERSION = "0.1.0"
39
+
40
+ _SECRET_FIELDS = (
41
+ "client_secret",
42
+ "iam_app_secret",
43
+ "iam_m2m_token",
44
+ "session_signing_secret",
45
+ "state_signing_secret",
46
+ )
47
+
48
+
49
+ class MemyAuth:
50
+ """Async MemyAuth client — the composition root for all nine customer capabilities.
51
+
52
+ Usage::
53
+
54
+ async with MemyAuth(
55
+ issuer_url="https://auth.memy.dev/oidc",
56
+ client_id="my-app",
57
+ client_secret="...",
58
+ public_base_url="https://my-app.example.com",
59
+ id_token_validation="local-jwks",
60
+ ) as auth:
61
+ req = await auth.oidc.build_authorization_url()
62
+ """
63
+
64
+ def __init__(self, **kwargs: Any) -> None:
65
+ self._config: ResolvedConfig = resolve_config(**kwargs)
66
+
67
+ self.discovery = DiscoveryService(
68
+ issuer_url=self._config["issuer_url"],
69
+ internal_issuer_url=self._config["internal_issuer_url"],
70
+ timeout_ms=self._config["discovery_timeout_ms"],
71
+ transport=self._config["transport"],
72
+ clock=self._config["clock"],
73
+ )
74
+ self.oidc = OidcService(
75
+ config=self._config,
76
+ discovery=self.discovery,
77
+ transport=self._config["transport"],
78
+ random=self._config["random"],
79
+ )
80
+ self.iam = IamService(config=self._config, transport=self._config["transport"])
81
+ self.roles = RoleMapper(
82
+ role_map=self._config["role_map"],
83
+ role_overrides=self._config["role_overrides"],
84
+ default_role=self._config["default_role"],
85
+ global_admin_roles=self._config["global_admin_roles"],
86
+ )
87
+
88
+ self._session: Optional[SessionTokenService] = None
89
+ self._state: Optional[StateCookieService] = None
90
+ self._id_token: Optional[IdTokenVerifier] = None
91
+
92
+ @property
93
+ def session(self) -> SessionTokenService:
94
+ if self._session is None:
95
+ secret = self._config.get("session_signing_secret")
96
+ if not secret:
97
+ raise ConfigError("sessionSigningSecret is required to issue or verify session tokens")
98
+ self._session = SessionTokenService(
99
+ signing_secret=secret,
100
+ access_ttl_seconds=self._config["access_ttl_seconds"],
101
+ refresh_ttl_seconds=self._config["refresh_ttl_seconds"],
102
+ issuer=self._config["session_issuer"],
103
+ clock=self._config["clock"],
104
+ )
105
+ return self._session
106
+
107
+ @property
108
+ def state(self) -> StateCookieService:
109
+ if self._state is None:
110
+ secret = self._config.get("state_signing_secret") or self._config.get("session_signing_secret")
111
+ if not secret:
112
+ raise ConfigError("stateSigningSecret (or sessionSigningSecret) is required to sign state cookies")
113
+ self._state = StateCookieService(
114
+ signing_secret=secret,
115
+ ttl_seconds=self._config["state_ttl_seconds"],
116
+ cookie_name=self._config["state_cookie_name"],
117
+ clock=self._config["clock"],
118
+ )
119
+ return self._state
120
+
121
+ @property
122
+ def id_token(self) -> IdTokenVerifier:
123
+ """Always available — `id_token_validation` is a required, already-validated field, so
124
+ this never raises. Still lazy + cached, purely to defer construction cost."""
125
+ if self._id_token is None:
126
+ self._id_token = IdTokenVerifier(
127
+ mode=self._config["id_token_validation"],
128
+ allowed_algorithms=self._config["allowed_algorithms"],
129
+ discovery=self.discovery,
130
+ transport=self._config["transport"],
131
+ clock=self._config["clock"],
132
+ )
133
+ return self._id_token
134
+
135
+ def resolved_config(self) -> dict[str, Any]:
136
+ """The resolved config, with every secret-bearing field replaced by `"<set>"` /
137
+ `"<unset>"` — safe to log or hand to a conformance driver."""
138
+ redacted: dict[str, Any] = dict(self._config)
139
+ redacted.pop("transport", None)
140
+ redacted.pop("clock", None)
141
+ redacted.pop("random", None)
142
+ for field in _SECRET_FIELDS:
143
+ redacted[field] = "<set>" if self._config.get(field) else "<unset>"
144
+ return redacted
145
+
146
+ def capabilities(self) -> dict[str, bool]:
147
+ """Which lazily-gated capabilities are currently usable, without constructing them."""
148
+ return {
149
+ "discovery": True,
150
+ "oidc": True,
151
+ "iam": True,
152
+ "roles": True,
153
+ "session": bool(self._config.get("session_signing_secret")),
154
+ "state": bool(self._config.get("state_signing_secret") or self._config.get("session_signing_secret")),
155
+ "id_token": True,
156
+ }
157
+
158
+ async def close(self) -> None:
159
+ await self.discovery.close() # type: ignore[attr-defined]
160
+ await self.oidc.close() # type: ignore[attr-defined]
161
+ await self.iam.close() # type: ignore[attr-defined]
162
+ if self._id_token is not None:
163
+ await self._id_token.close() # type: ignore[attr-defined]
164
+
165
+ async def __aenter__(self) -> "MemyAuth":
166
+ return self
167
+
168
+ async def __aexit__(self, *exc: Any) -> None:
169
+ await self.close()