remdb 0.3.242__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.

Potentially problematic release.


This version of remdb might be problematic. Click here for more details.

Files changed (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,163 @@
1
+ """
2
+ Google OAuth Provider.
3
+
4
+ Implements OAuth 2.1 / OIDC for Google Sign-In.
5
+
6
+ Configuration:
7
+ 1. Create OAuth 2.0 credentials at https://console.cloud.google.com/apis/credentials
8
+ 2. Set authorized redirect URI: http://localhost:8000/api/auth/callback (dev)
9
+ 3. Enable Google+ API for userinfo access
10
+ 4. Set environment variables:
11
+ - AUTH__GOOGLE__CLIENT_ID
12
+ - AUTH__GOOGLE__CLIENT_SECRET
13
+ - AUTH__GOOGLE__REDIRECT_URI
14
+
15
+ Google-specific features:
16
+ - Hosted domain restriction (hd parameter for Google Workspace)
17
+ - Incremental authorization
18
+ - Offline access for refresh tokens
19
+
20
+ References:
21
+ - Google OAuth 2.0: https://developers.google.com/identity/protocols/oauth2
22
+ - Google OIDC: https://developers.google.com/identity/openid-connect/openid-connect
23
+ - Scopes: https://developers.google.com/identity/protocols/oauth2/scopes
24
+ """
25
+
26
+ from typing import Any
27
+
28
+ from .base import OAuthProvider, OAuthUserInfo
29
+
30
+
31
+ class GoogleOAuthProvider(OAuthProvider):
32
+ """
33
+ Google OAuth 2.1 / OIDC provider.
34
+
35
+ Uses Google's OIDC endpoints for authentication.
36
+ Supports both online (access token only) and offline (refresh token) access.
37
+ """
38
+
39
+ # Google OIDC discovery endpoint:
40
+ # https://accounts.google.com/.well-known/openid-configuration
41
+ AUTHORIZATION_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"
42
+ TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
43
+ USERINFO_ENDPOINT = "https://openidconnect.googleapis.com/v1/userinfo"
44
+ JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs"
45
+
46
+ # Google OAuth scopes
47
+ # openid: Required for OIDC
48
+ # email: User email address
49
+ # profile: User profile information (name, picture)
50
+ DEFAULT_SCOPES = [
51
+ "openid",
52
+ "email",
53
+ "profile",
54
+ ]
55
+
56
+ @property
57
+ def authorization_endpoint(self) -> str:
58
+ """Google authorization endpoint."""
59
+ return self.AUTHORIZATION_ENDPOINT
60
+
61
+ @property
62
+ def token_endpoint(self) -> str:
63
+ """Google token endpoint."""
64
+ return self.TOKEN_ENDPOINT
65
+
66
+ @property
67
+ def userinfo_endpoint(self) -> str:
68
+ """Google userinfo endpoint."""
69
+ return self.USERINFO_ENDPOINT
70
+
71
+ @property
72
+ def jwks_uri(self) -> str:
73
+ """Google JWKS URI for token validation."""
74
+ return self.JWKS_URI
75
+
76
+ @property
77
+ def default_scopes(self) -> list[str]:
78
+ """Default scopes for Google OAuth."""
79
+ return self.DEFAULT_SCOPES.copy()
80
+
81
+ def normalize_user_info(self, claims: dict[str, Any]) -> OAuthUserInfo:
82
+ """
83
+ Normalize Google OIDC claims to OAuthUserInfo.
84
+
85
+ Google OIDC claims:
86
+ - sub: Unique user ID (stable identifier)
87
+ - email: User email address
88
+ - email_verified: Email verification status
89
+ - name: Full name
90
+ - given_name: First name
91
+ - family_name: Last name
92
+ - picture: Profile picture URL
93
+ - locale: User locale (e.g., "en")
94
+ - hd: Hosted domain (for Google Workspace accounts)
95
+
96
+ Args:
97
+ claims: Raw claims from ID token or userinfo endpoint
98
+
99
+ Returns:
100
+ Normalized user information
101
+ """
102
+ return OAuthUserInfo(
103
+ sub=claims["sub"],
104
+ email=claims.get("email"),
105
+ email_verified=claims.get("email_verified", False),
106
+ name=claims.get("name"),
107
+ given_name=claims.get("given_name"),
108
+ family_name=claims.get("family_name"),
109
+ picture=claims.get("picture"),
110
+ locale=claims.get("locale"),
111
+ provider="google",
112
+ raw_claims=claims,
113
+ )
114
+
115
+ def generate_auth_url_with_hosted_domain(
116
+ self,
117
+ state: str,
118
+ code_challenge: str,
119
+ hosted_domain: str | None = None,
120
+ access_type: str = "online",
121
+ scopes: list[str] | None = None,
122
+ nonce: str | None = None,
123
+ ) -> str:
124
+ """
125
+ Generate authorization URL with Google-specific parameters.
126
+
127
+ Args:
128
+ state: CSRF protection state
129
+ code_challenge: PKCE code challenge
130
+ hosted_domain: Restrict to Google Workspace domain (e.g., "example.com")
131
+ access_type: "online" (access token only) or "offline" (refresh token)
132
+ scopes: OAuth scopes (uses default_scopes if None)
133
+ nonce: OIDC nonce for ID token replay protection
134
+
135
+ Returns:
136
+ Authorization URL
137
+
138
+ Google-specific parameters:
139
+ - hd: Hosted domain restriction (Google Workspace only)
140
+ - access_type: online (default) or offline (for refresh tokens)
141
+ - prompt: consent (force consent screen), select_account (account picker)
142
+ - include_granted_scopes: true (incremental authorization)
143
+ """
144
+ extra_params: dict[str, str] = {
145
+ "access_type": access_type,
146
+ "include_granted_scopes": "true", # Incremental authorization
147
+ }
148
+
149
+ # Hosted domain restriction (Google Workspace)
150
+ if hosted_domain:
151
+ extra_params["hd"] = hosted_domain
152
+
153
+ # Force consent screen to get refresh token
154
+ if access_type == "offline":
155
+ extra_params["prompt"] = "consent"
156
+
157
+ return self.generate_auth_url(
158
+ state=state,
159
+ code_challenge=code_challenge,
160
+ scopes=scopes,
161
+ nonce=nonce,
162
+ extra_params=extra_params,
163
+ )
@@ -0,0 +1,237 @@
1
+ """
2
+ Microsoft Entra ID (Azure AD) OAuth Provider.
3
+
4
+ Implements OAuth 2.1 / OIDC for Microsoft authentication.
5
+
6
+ Configuration:
7
+ 1. Register application at https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps
8
+ 2. Create client secret under "Certificates & secrets"
9
+ 3. Add redirect URI: http://localhost:8000/api/auth/callback (dev)
10
+ 4. Set API permissions:
11
+ - Microsoft Graph: User.Read (delegated)
12
+ - Optional: email, profile, openid (automatically included)
13
+ 5. Set environment variables:
14
+ - AUTH__MICROSOFT__CLIENT_ID (Application ID)
15
+ - AUTH__MICROSOFT__CLIENT_SECRET
16
+ - AUTH__MICROSOFT__TENANT_ID (or "common" for multi-tenant)
17
+ - AUTH__MICROSOFT__REDIRECT_URI
18
+
19
+ Microsoft-specific features:
20
+ - Multi-tenant support (common, organizations, consumers)
21
+ - Azure AD B2C support
22
+ - Conditional access policies
23
+ - Token caching with MSAL
24
+
25
+ Tenant options:
26
+ - common: Multi-tenant + personal Microsoft accounts
27
+ - organizations: Multi-tenant (work/school only)
28
+ - consumers: Personal Microsoft accounts only
29
+ - {tenant-id}: Single tenant (specific organization)
30
+
31
+ References:
32
+ - Microsoft identity platform: https://learn.microsoft.com/en-us/entra/identity-platform/
33
+ - OAuth 2.0 flow: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
34
+ - OIDC: https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc
35
+ - Scopes: https://learn.microsoft.com/en-us/graph/permissions-reference
36
+ """
37
+
38
+ from typing import Any
39
+
40
+ from .base import OAuthProvider, OAuthUserInfo
41
+
42
+
43
+ class MicrosoftOAuthProvider(OAuthProvider):
44
+ """
45
+ Microsoft Entra ID (Azure AD) OAuth 2.1 / OIDC provider.
46
+
47
+ Supports multi-tenant authentication and Microsoft Graph API access.
48
+ Uses Microsoft identity platform v2.0 endpoints.
49
+ """
50
+
51
+ # Microsoft identity platform v2.0 endpoints
52
+ # Replace {tenant} with:
53
+ # - "common" for multi-tenant + personal accounts
54
+ # - "organizations" for work/school accounts only
55
+ # - "consumers" for personal Microsoft accounts only
56
+ # - Tenant ID/domain for single-tenant
57
+ AUTHORIZATION_ENDPOINT_TEMPLATE = (
58
+ "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize"
59
+ )
60
+ TOKEN_ENDPOINT_TEMPLATE = "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
61
+ USERINFO_ENDPOINT = "https://graph.microsoft.com/v1.0/me"
62
+ JWKS_URI_TEMPLATE = "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
63
+
64
+ # Microsoft Graph scopes
65
+ # openid: Required for OIDC
66
+ # email: User email address
67
+ # profile: User profile information
68
+ # User.Read: Read user profile via Microsoft Graph
69
+ # offline_access: Request refresh token
70
+ DEFAULT_SCOPES = [
71
+ "openid",
72
+ "email",
73
+ "profile",
74
+ "User.Read", # Microsoft Graph: read user profile
75
+ ]
76
+
77
+ def __init__(
78
+ self,
79
+ client_id: str,
80
+ client_secret: str,
81
+ redirect_uri: str,
82
+ tenant: str = "common",
83
+ ):
84
+ """
85
+ Initialize Microsoft OAuth provider.
86
+
87
+ Args:
88
+ client_id: Application (client) ID from Azure portal
89
+ client_secret: Client secret from Azure portal
90
+ redirect_uri: Redirect URI registered in Azure portal
91
+ tenant: Tenant ID or "common"/"organizations"/"consumers"
92
+ """
93
+ super().__init__(client_id, client_secret, redirect_uri)
94
+ self.tenant = tenant
95
+
96
+ @property
97
+ def authorization_endpoint(self) -> str:
98
+ """Microsoft authorization endpoint."""
99
+ return self.AUTHORIZATION_ENDPOINT_TEMPLATE.format(tenant=self.tenant)
100
+
101
+ @property
102
+ def token_endpoint(self) -> str:
103
+ """Microsoft token endpoint."""
104
+ return self.TOKEN_ENDPOINT_TEMPLATE.format(tenant=self.tenant)
105
+
106
+ @property
107
+ def userinfo_endpoint(self) -> str:
108
+ """Microsoft Graph /me endpoint for user info."""
109
+ return self.USERINFO_ENDPOINT
110
+
111
+ @property
112
+ def jwks_uri(self) -> str:
113
+ """Microsoft JWKS URI for token validation."""
114
+ return self.JWKS_URI_TEMPLATE.format(tenant=self.tenant)
115
+
116
+ @property
117
+ def default_scopes(self) -> list[str]:
118
+ """Default scopes for Microsoft OAuth."""
119
+ return self.DEFAULT_SCOPES.copy()
120
+
121
+ def normalize_user_info(self, claims: dict[str, Any]) -> OAuthUserInfo:
122
+ """
123
+ Normalize Microsoft claims to OAuthUserInfo.
124
+
125
+ Microsoft Graph /me response:
126
+ - id: Unique user ID (stable identifier)
127
+ - userPrincipalName: User principal name (UPN)
128
+ - mail: Primary email (may be null)
129
+ - displayName: Display name
130
+ - givenName: First name
131
+ - surname: Last name
132
+ - preferredLanguage: User locale
133
+
134
+ Microsoft ID token claims:
135
+ - sub: Subject (unique user ID, different from Graph ID)
136
+ - email: User email
137
+ - name: Full name
138
+ - given_name: First name
139
+ - family_name: Last name
140
+ - preferred_username: UPN or email
141
+
142
+ Args:
143
+ claims: Raw claims from ID token or Microsoft Graph /me
144
+
145
+ Returns:
146
+ Normalized user information
147
+ """
148
+ # Handle both ID token claims and Graph API response
149
+ # Graph API uses different field names than OIDC claims
150
+ if "id" in claims:
151
+ # Microsoft Graph /me response
152
+ sub = claims["id"]
153
+ email = claims.get("mail") or claims.get("userPrincipalName")
154
+ name = claims.get("displayName")
155
+ given_name = claims.get("givenName")
156
+ family_name = claims.get("surname")
157
+ locale = claims.get("preferredLanguage")
158
+ else:
159
+ # OIDC ID token claims
160
+ sub = claims["sub"]
161
+ email = claims.get("email") or claims.get("preferred_username")
162
+ name = claims.get("name")
163
+ given_name = claims.get("given_name")
164
+ family_name = claims.get("family_name")
165
+ locale = claims.get("locale")
166
+
167
+ return OAuthUserInfo(
168
+ sub=sub,
169
+ email=email,
170
+ email_verified=True, # Microsoft verifies emails during account creation
171
+ name=name,
172
+ given_name=given_name,
173
+ family_name=family_name,
174
+ picture=None, # Microsoft Graph requires separate photo endpoint
175
+ locale=locale,
176
+ provider="microsoft",
177
+ raw_claims=claims,
178
+ )
179
+
180
+ def generate_auth_url_with_prompt(
181
+ self,
182
+ state: str,
183
+ code_challenge: str,
184
+ prompt: str | None = None,
185
+ domain_hint: str | None = None,
186
+ login_hint: str | None = None,
187
+ scopes: list[str] | None = None,
188
+ nonce: str | None = None,
189
+ ) -> str:
190
+ """
191
+ Generate authorization URL with Microsoft-specific parameters.
192
+
193
+ Args:
194
+ state: CSRF protection state
195
+ code_challenge: PKCE code challenge
196
+ prompt: Authentication behavior (none, login, consent, select_account)
197
+ domain_hint: Domain hint for faster login (e.g., "contoso.com")
198
+ login_hint: Login hint (email) to pre-fill sign-in form
199
+ scopes: OAuth scopes (uses default_scopes if None)
200
+ nonce: OIDC nonce for ID token replay protection
201
+
202
+ Returns:
203
+ Authorization URL
204
+
205
+ Microsoft-specific parameters:
206
+ - prompt: Authentication behavior
207
+ - none: Silent authentication (fails if interaction required)
208
+ - login: Force user to re-authenticate
209
+ - consent: Force consent screen
210
+ - select_account: Show account picker
211
+ - domain_hint: Domain hint for faster login (skip domain discovery)
212
+ - login_hint: Email to pre-fill sign-in form
213
+ - response_mode: query (default), form_post, fragment
214
+ """
215
+ extra_params: dict[str, str] = {}
216
+
217
+ if prompt:
218
+ extra_params["prompt"] = prompt
219
+
220
+ if domain_hint:
221
+ extra_params["domain_hint"] = domain_hint
222
+
223
+ if login_hint:
224
+ extra_params["login_hint"] = login_hint
225
+
226
+ # Add offline_access scope for refresh token
227
+ scopes_with_offline = scopes or self.default_scopes.copy()
228
+ if "offline_access" not in scopes_with_offline:
229
+ scopes_with_offline.append("offline_access")
230
+
231
+ return self.generate_auth_url(
232
+ state=state,
233
+ code_challenge=code_challenge,
234
+ scopes=scopes_with_offline,
235
+ nonce=nonce,
236
+ extra_params=extra_params,
237
+ )