remdb 0.3.14__py3-none-any.whl → 0.3.157__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.
Files changed (112) hide show
  1. rem/agentic/README.md +76 -0
  2. rem/agentic/__init__.py +15 -0
  3. rem/agentic/agents/__init__.py +32 -2
  4. rem/agentic/agents/agent_manager.py +310 -0
  5. rem/agentic/agents/sse_simulator.py +502 -0
  6. rem/agentic/context.py +51 -27
  7. rem/agentic/context_builder.py +5 -3
  8. rem/agentic/llm_provider_models.py +301 -0
  9. rem/agentic/mcp/tool_wrapper.py +155 -18
  10. rem/agentic/otel/setup.py +93 -4
  11. rem/agentic/providers/phoenix.py +371 -108
  12. rem/agentic/providers/pydantic_ai.py +280 -57
  13. rem/agentic/schema.py +361 -21
  14. rem/agentic/tools/rem_tools.py +3 -3
  15. rem/api/README.md +215 -1
  16. rem/api/deps.py +255 -0
  17. rem/api/main.py +132 -40
  18. rem/api/mcp_router/resources.py +1 -1
  19. rem/api/mcp_router/server.py +28 -5
  20. rem/api/mcp_router/tools.py +555 -7
  21. rem/api/routers/admin.py +494 -0
  22. rem/api/routers/auth.py +278 -4
  23. rem/api/routers/chat/completions.py +402 -20
  24. rem/api/routers/chat/models.py +88 -10
  25. rem/api/routers/chat/otel_utils.py +33 -0
  26. rem/api/routers/chat/sse_events.py +542 -0
  27. rem/api/routers/chat/streaming.py +697 -45
  28. rem/api/routers/dev.py +81 -0
  29. rem/api/routers/feedback.py +268 -0
  30. rem/api/routers/messages.py +473 -0
  31. rem/api/routers/models.py +78 -0
  32. rem/api/routers/query.py +360 -0
  33. rem/api/routers/shared_sessions.py +406 -0
  34. rem/auth/__init__.py +13 -3
  35. rem/auth/middleware.py +186 -22
  36. rem/auth/providers/__init__.py +4 -1
  37. rem/auth/providers/email.py +215 -0
  38. rem/cli/commands/README.md +237 -64
  39. rem/cli/commands/cluster.py +1808 -0
  40. rem/cli/commands/configure.py +4 -7
  41. rem/cli/commands/db.py +386 -143
  42. rem/cli/commands/experiments.py +468 -76
  43. rem/cli/commands/process.py +14 -8
  44. rem/cli/commands/schema.py +97 -50
  45. rem/cli/commands/session.py +336 -0
  46. rem/cli/dreaming.py +2 -2
  47. rem/cli/main.py +29 -6
  48. rem/config.py +10 -3
  49. rem/models/core/core_model.py +7 -1
  50. rem/models/core/experiment.py +58 -14
  51. rem/models/core/rem_query.py +5 -2
  52. rem/models/entities/__init__.py +25 -0
  53. rem/models/entities/domain_resource.py +38 -0
  54. rem/models/entities/feedback.py +123 -0
  55. rem/models/entities/message.py +30 -1
  56. rem/models/entities/ontology.py +1 -1
  57. rem/models/entities/ontology_config.py +1 -1
  58. rem/models/entities/session.py +83 -0
  59. rem/models/entities/shared_session.py +180 -0
  60. rem/models/entities/subscriber.py +175 -0
  61. rem/models/entities/user.py +1 -0
  62. rem/registry.py +10 -4
  63. rem/schemas/agents/core/agent-builder.yaml +134 -0
  64. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  65. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  66. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  67. rem/schemas/agents/rem.yaml +7 -3
  68. rem/services/__init__.py +3 -1
  69. rem/services/content/service.py +92 -19
  70. rem/services/email/__init__.py +10 -0
  71. rem/services/email/service.py +459 -0
  72. rem/services/email/templates.py +360 -0
  73. rem/services/embeddings/api.py +4 -4
  74. rem/services/embeddings/worker.py +16 -16
  75. rem/services/phoenix/client.py +154 -14
  76. rem/services/postgres/README.md +197 -15
  77. rem/services/postgres/__init__.py +2 -1
  78. rem/services/postgres/diff_service.py +547 -0
  79. rem/services/postgres/pydantic_to_sqlalchemy.py +470 -140
  80. rem/services/postgres/repository.py +132 -0
  81. rem/services/postgres/schema_generator.py +205 -4
  82. rem/services/postgres/service.py +6 -6
  83. rem/services/rem/parser.py +44 -9
  84. rem/services/rem/service.py +36 -2
  85. rem/services/session/compression.py +137 -51
  86. rem/services/session/reload.py +15 -8
  87. rem/settings.py +515 -27
  88. rem/sql/background_indexes.sql +21 -16
  89. rem/sql/migrations/001_install.sql +387 -54
  90. rem/sql/migrations/002_install_models.sql +2304 -377
  91. rem/sql/migrations/003_optional_extensions.sql +326 -0
  92. rem/sql/migrations/004_cache_system.sql +548 -0
  93. rem/sql/migrations/005_schema_update.sql +145 -0
  94. rem/utils/README.md +45 -0
  95. rem/utils/__init__.py +18 -0
  96. rem/utils/date_utils.py +2 -2
  97. rem/utils/files.py +157 -1
  98. rem/utils/model_helpers.py +156 -1
  99. rem/utils/schema_loader.py +220 -22
  100. rem/utils/sql_paths.py +146 -0
  101. rem/utils/sql_types.py +3 -1
  102. rem/utils/vision.py +1 -1
  103. rem/workers/__init__.py +3 -1
  104. rem/workers/db_listener.py +579 -0
  105. rem/workers/unlogged_maintainer.py +463 -0
  106. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/METADATA +340 -229
  107. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/RECORD +109 -80
  108. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/WHEEL +1 -1
  109. rem/sql/002_install_models.sql +0 -1068
  110. rem/sql/install_models.sql +0 -1051
  111. rem/sql/migrations/003_seed_default_user.sql +0 -48
  112. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/entry_points.txt +0 -0
rem/auth/middleware.py CHANGED
@@ -2,14 +2,36 @@
2
2
  OAuth Authentication Middleware for FastAPI.
3
3
 
4
4
  Protects API endpoints by requiring valid session.
5
- Redirects unauthenticated requests to login page.
5
+ Supports anonymous access with rate limiting when allow_anonymous=True.
6
+ MCP endpoints are always protected unless explicitly disabled.
6
7
 
7
8
  Design Pattern:
9
+ - Check X-API-Key header first (if API key auth enabled)
8
10
  - Check session for user on protected paths
9
- - Return 401 for API calls (JSON)
10
- - Redirect to login for browser requests (HTML)
11
+ - Check Bearer token for dev token (non-production only)
12
+ - MCP paths always require authentication (protected service)
13
+ - If allow_anonymous=True: Allow unauthenticated requests (marked as ANONYMOUS tier)
14
+ - If allow_anonymous=False: Return 401 for API calls, redirect browsers to login
11
15
  - Exclude auth endpoints and public paths
12
16
 
17
+ Access Modes (configured in settings.auth):
18
+ - enabled=true, allow_anonymous=true: Auth available, anonymous gets rate-limited access
19
+ - enabled=true, allow_anonymous=false: Auth required for all requests
20
+ - enabled=false: Middleware not loaded, all requests pass through
21
+ - mcp_requires_auth=true (default): MCP always requires login regardless of allow_anonymous
22
+ - mcp_requires_auth=false: MCP follows normal allow_anonymous rules (dev only)
23
+
24
+ API Key Authentication (configured in settings.api):
25
+ - api_key_enabled=true: Require X-API-Key header for protected endpoints
26
+ - api_key: The secret key to validate against
27
+ - Provides simple programmatic access without OAuth flow
28
+ - X-API-Key header takes precedence over session auth
29
+
30
+ Dev Token Support (non-production only):
31
+ - GET /api/auth/dev/token returns a Bearer token for test-user
32
+ - Include as: Authorization: Bearer dev_<signature>
33
+ - Only works when ENVIRONMENT != "production"
34
+
13
35
  Usage:
14
36
  from rem.auth.middleware import AuthMiddleware
15
37
 
@@ -17,6 +39,8 @@ Usage:
17
39
  AuthMiddleware,
18
40
  protected_paths=["/api/v1"],
19
41
  excluded_paths=["/api/auth", "/health"],
42
+ allow_anonymous=settings.auth.allow_anonymous,
43
+ mcp_requires_auth=settings.auth.mcp_requires_auth,
20
44
  )
21
45
  """
22
46
 
@@ -25,6 +49,8 @@ from starlette.requests import Request
25
49
  from starlette.responses import JSONResponse, RedirectResponse
26
50
  from loguru import logger
27
51
 
52
+ from ..settings import settings
53
+
28
54
 
29
55
  class AuthMiddleware(BaseHTTPMiddleware):
30
56
  """
@@ -32,6 +58,8 @@ class AuthMiddleware(BaseHTTPMiddleware):
32
58
 
33
59
  Checks for valid user session on protected paths.
34
60
  Compatible with OAuth flows from auth router.
61
+ Supports anonymous access with rate limiting.
62
+ MCP endpoints are always protected unless explicitly disabled.
35
63
  """
36
64
 
37
65
  def __init__(
@@ -39,6 +67,9 @@ class AuthMiddleware(BaseHTTPMiddleware):
39
67
  app,
40
68
  protected_paths: list[str] | None = None,
41
69
  excluded_paths: list[str] | None = None,
70
+ allow_anonymous: bool = True,
71
+ mcp_requires_auth: bool = True,
72
+ mcp_path: str = "/api/v1/mcp",
42
73
  ):
43
74
  """
44
75
  Initialize auth middleware.
@@ -47,10 +78,85 @@ class AuthMiddleware(BaseHTTPMiddleware):
47
78
  app: ASGI application
48
79
  protected_paths: Paths that require authentication
49
80
  excluded_paths: Paths to exclude from auth check
81
+ allow_anonymous: Allow unauthenticated requests (rate-limited)
82
+ mcp_requires_auth: Always require auth for MCP (protected service)
83
+ mcp_path: Path prefix for MCP endpoints
50
84
  """
51
85
  super().__init__(app)
52
86
  self.protected_paths = protected_paths or ["/api/v1"]
53
87
  self.excluded_paths = excluded_paths or ["/api/auth", "/health", "/docs", "/openapi.json"]
88
+ self.allow_anonymous = allow_anonymous
89
+ self.mcp_requires_auth = mcp_requires_auth
90
+ self.mcp_path = mcp_path
91
+
92
+ def _check_api_key(self, request: Request) -> dict | None:
93
+ """
94
+ Check for valid X-API-Key header.
95
+
96
+ Returns:
97
+ API key user dict if valid, None otherwise
98
+ """
99
+ # Only check if API key auth is enabled
100
+ if not settings.api.api_key_enabled:
101
+ return None
102
+
103
+ # Check for X-API-Key header
104
+ api_key = request.headers.get("x-api-key")
105
+ if not api_key:
106
+ return None
107
+
108
+ # Validate against configured API key
109
+ if settings.api.api_key and api_key == settings.api.api_key:
110
+ logger.debug("X-API-Key authenticated")
111
+ return {
112
+ "id": "api-key-user",
113
+ "email": "api@rem.local",
114
+ "name": "API Key User",
115
+ "provider": "api-key",
116
+ "tenant_id": "default",
117
+ "tier": "pro", # API key users get full access
118
+ "roles": ["user"],
119
+ }
120
+
121
+ # Invalid API key
122
+ logger.warning("Invalid X-API-Key provided")
123
+ return None
124
+
125
+ def _check_dev_token(self, request: Request) -> dict | None:
126
+ """
127
+ Check for valid dev token in Authorization header (non-production only).
128
+
129
+ Returns:
130
+ Test user dict if valid dev token, None otherwise
131
+ """
132
+ if settings.environment == "production":
133
+ return None
134
+
135
+ auth_header = request.headers.get("authorization", "")
136
+ if not auth_header.startswith("Bearer "):
137
+ return None
138
+
139
+ token = auth_header[7:] # Strip "Bearer "
140
+
141
+ # Only check dev tokens (start with "dev_")
142
+ if not token.startswith("dev_"):
143
+ return None
144
+
145
+ # Verify dev token
146
+ from ..api.routers.dev import verify_dev_token
147
+ if verify_dev_token(token):
148
+ logger.debug("Dev token authenticated as test-user")
149
+ return {
150
+ "id": "test-user",
151
+ "email": "test@rem.local",
152
+ "name": "Test User",
153
+ "provider": "dev",
154
+ "tenant_id": "default",
155
+ "tier": "pro", # Give test user pro tier for full access
156
+ "roles": ["admin"],
157
+ }
158
+
159
+ return None
54
160
 
55
161
  async def dispatch(self, request: Request, call_next):
56
162
  """
@@ -61,7 +167,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
61
167
  call_next: Next middleware in chain
62
168
 
63
169
  Returns:
64
- Response (401/redirect if unauthorized, normal response if authorized)
170
+ Response (401/redirect if unauthorized, normal response if authorized/anonymous)
65
171
  """
66
172
  path = request.url.path
67
173
 
@@ -69,32 +175,90 @@ class AuthMiddleware(BaseHTTPMiddleware):
69
175
  is_protected = any(path.startswith(p) for p in self.protected_paths)
70
176
  is_excluded = any(path.startswith(p) for p in self.excluded_paths)
71
177
 
178
+ # Check if this is an MCP path (paid service, always requires auth)
179
+ is_mcp_path = path.startswith(self.mcp_path)
180
+
72
181
  # Skip auth check for excluded paths
73
182
  if not is_protected or is_excluded:
74
183
  return await call_next(request)
75
184
 
76
- # Check for valid session
77
- user = request.session.get("user")
78
- if not user:
79
- logger.warning(f"Unauthorized access attempt: {path}")
185
+ # Check for X-API-Key header first (if enabled)
186
+ api_key_user = self._check_api_key(request)
187
+ if api_key_user:
188
+ request.state.user = api_key_user
189
+ request.state.is_anonymous = False
190
+ return await call_next(request)
80
191
 
81
- # Return 401 for API requests (JSON)
82
- # Check Accept header to determine if client expects JSON
83
- accept = request.headers.get("accept", "")
84
- if "application/json" in accept or path.startswith("/api/"):
192
+ # If API key auth is enabled but no valid key provided, reject immediately
193
+ if settings.api.api_key_enabled:
194
+ # Check if X-API-Key header was provided but invalid
195
+ if request.headers.get("x-api-key"):
196
+ logger.warning(f"Invalid X-API-Key for: {path}")
85
197
  return JSONResponse(
86
198
  status_code=401,
87
- content={"detail": "Authentication required"},
88
- headers={
89
- "WWW-Authenticate": 'Bearer realm="REM API"',
90
- },
199
+ content={"detail": "Invalid API key"},
200
+ headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
91
201
  )
202
+ # No API key provided when required
203
+ logger.debug(f"Missing X-API-Key for: {path}")
204
+ return JSONResponse(
205
+ status_code=401,
206
+ content={"detail": "API key required. Include X-API-Key header."},
207
+ headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
208
+ )
209
+
210
+ # Check for dev token (non-production only)
211
+ dev_user = self._check_dev_token(request)
212
+ if dev_user:
213
+ request.state.user = dev_user
214
+ request.state.is_anonymous = False
215
+ return await call_next(request)
216
+
217
+ # Check for valid session
218
+ user = request.session.get("user")
219
+
220
+ if user:
221
+ # Authenticated user - add to request state
222
+ request.state.user = user
223
+ request.state.is_anonymous = False
224
+ return await call_next(request)
225
+
226
+ # No user session - check if MCP path requires auth
227
+ if is_mcp_path and self.mcp_requires_auth:
228
+ # MCP is a protected service - always require authentication
229
+ logger.warning(f"Unauthorized MCP access attempt: {path}")
230
+ return JSONResponse(
231
+ status_code=401,
232
+ content={
233
+ "detail": "Authentication required for MCP. Please login to use this service.",
234
+ "code": "MCP_AUTH_REQUIRED",
235
+ },
236
+ headers={
237
+ "WWW-Authenticate": 'Bearer realm="REM MCP"',
238
+ },
239
+ )
240
+
241
+ # No user session - handle anonymous access for non-MCP paths
242
+ if self.allow_anonymous:
243
+ # Allow anonymous access - rate limiting handled downstream
244
+ request.state.user = None
245
+ request.state.is_anonymous = True
246
+ logger.debug(f"Anonymous access: {path}")
247
+ return await call_next(request)
92
248
 
93
- # Redirect to login for browser requests
94
- # TODO: Store original URL for post-login redirect
95
- return RedirectResponse(url="/api/auth/google/login", status_code=302)
249
+ # Anonymous not allowed - require authentication
250
+ logger.warning(f"Unauthorized access attempt: {path}")
96
251
 
97
- # Add user to request state for downstream handlers
98
- request.state.user = user
252
+ # Return 401 for API requests (JSON)
253
+ accept = request.headers.get("accept", "")
254
+ if "application/json" in accept or path.startswith("/api/"):
255
+ return JSONResponse(
256
+ status_code=401,
257
+ content={"detail": "Authentication required"},
258
+ headers={
259
+ "WWW-Authenticate": 'Bearer realm="REM API"',
260
+ },
261
+ )
99
262
 
100
- return await call_next(request)
263
+ # Redirect to login for browser requests
264
+ return RedirectResponse(url="/api/auth/google/login", status_code=302)
@@ -1,6 +1,7 @@
1
- """OAuth provider implementations."""
1
+ """Authentication provider implementations."""
2
2
 
3
3
  from .base import OAuthProvider, OAuthTokens, OAuthUserInfo
4
+ from .email import EmailAuthProvider, EmailAuthResult
4
5
  from .google import GoogleOAuthProvider
5
6
  from .microsoft import MicrosoftOAuthProvider
6
7
 
@@ -8,6 +9,8 @@ __all__ = [
8
9
  "OAuthProvider",
9
10
  "OAuthTokens",
10
11
  "OAuthUserInfo",
12
+ "EmailAuthProvider",
13
+ "EmailAuthResult",
11
14
  "GoogleOAuthProvider",
12
15
  "MicrosoftOAuthProvider",
13
16
  ]
@@ -0,0 +1,215 @@
1
+ """
2
+ Email Authentication Provider.
3
+
4
+ Passwordless authentication using email verification codes.
5
+ Unlike OAuth providers, this handles the full flow internally.
6
+
7
+ Flow:
8
+ 1. User requests login with email address
9
+ 2. System generates code, upserts user, sends email
10
+ 3. User enters code
11
+ 4. System verifies code and creates session
12
+
13
+ Design:
14
+ - Uses EmailService for sending codes
15
+ - Creates users with deterministic UUID from email hash
16
+ - Stores challenge in user metadata
17
+ - No external OAuth dependencies
18
+ """
19
+
20
+ from typing import TYPE_CHECKING
21
+ from pydantic import BaseModel, Field
22
+ from loguru import logger
23
+
24
+ from ...services.email import EmailService
25
+
26
+ if TYPE_CHECKING:
27
+ from ...services.postgres import PostgresService
28
+
29
+
30
+ class EmailAuthResult(BaseModel):
31
+ """Result of email authentication operations."""
32
+
33
+ success: bool = Field(description="Whether operation succeeded")
34
+ email: str = Field(description="Email address")
35
+ user_id: str | None = Field(default=None, description="User ID if authenticated")
36
+ error: str | None = Field(default=None, description="Error message if failed")
37
+ message: str | None = Field(default=None, description="User-friendly message")
38
+
39
+
40
+ class EmailAuthProvider:
41
+ """
42
+ Email-based passwordless authentication provider.
43
+
44
+ Handles the complete email login flow:
45
+ 1. send_code() - Generate and send verification code
46
+ 2. verify_code() - Verify code and return user info
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ email_service: EmailService | None = None,
52
+ template_kwargs: dict | None = None,
53
+ ):
54
+ """
55
+ Initialize EmailAuthProvider.
56
+
57
+ Args:
58
+ email_service: EmailService instance (creates new one if not provided)
59
+ template_kwargs: Customization for email templates (colors, branding, etc.)
60
+ """
61
+ self._email_service = email_service or EmailService()
62
+ self._template_kwargs = template_kwargs or {}
63
+
64
+ @property
65
+ def is_configured(self) -> bool:
66
+ """Check if email auth is properly configured."""
67
+ return self._email_service.is_configured
68
+
69
+ async def send_code(
70
+ self,
71
+ email: str,
72
+ db: "PostgresService",
73
+ tenant_id: str = "default",
74
+ ) -> EmailAuthResult:
75
+ """
76
+ Send a verification code to an email address.
77
+
78
+ Creates user if not exists (using deterministic UUID from email).
79
+ Stores code in user metadata.
80
+
81
+ Args:
82
+ email: Email address to send code to
83
+ db: PostgresService instance
84
+ tenant_id: Tenant identifier
85
+
86
+ Returns:
87
+ EmailAuthResult with success status
88
+ """
89
+ if not self.is_configured:
90
+ return EmailAuthResult(
91
+ success=False,
92
+ email=email,
93
+ error="Email service not configured",
94
+ message="Email login is not available. Please try another method.",
95
+ )
96
+
97
+ try:
98
+ result = await self._email_service.send_login_code(
99
+ email=email,
100
+ db=db,
101
+ tenant_id=tenant_id,
102
+ template_kwargs=self._template_kwargs,
103
+ )
104
+
105
+ if result["success"]:
106
+ return EmailAuthResult(
107
+ success=True,
108
+ email=email,
109
+ user_id=result["user_id"],
110
+ message=f"Verification code sent to {email}. Check your inbox.",
111
+ )
112
+ else:
113
+ return EmailAuthResult(
114
+ success=False,
115
+ email=email,
116
+ error=result.get("error", "Failed to send code"),
117
+ message="Failed to send verification code. Please try again.",
118
+ )
119
+
120
+ except Exception as e:
121
+ logger.error(f"Error sending login code: {e}")
122
+ return EmailAuthResult(
123
+ success=False,
124
+ email=email,
125
+ error=str(e),
126
+ message="An error occurred. Please try again.",
127
+ )
128
+
129
+ async def verify_code(
130
+ self,
131
+ email: str,
132
+ code: str,
133
+ db: "PostgresService",
134
+ tenant_id: str = "default",
135
+ ) -> EmailAuthResult:
136
+ """
137
+ Verify a login code and authenticate user.
138
+
139
+ Args:
140
+ email: Email address
141
+ code: 6-digit verification code
142
+ db: PostgresService instance
143
+ tenant_id: Tenant identifier
144
+
145
+ Returns:
146
+ EmailAuthResult with user_id if successful
147
+ """
148
+ try:
149
+ result = await self._email_service.verify_login_code(
150
+ email=email,
151
+ code=code,
152
+ db=db,
153
+ tenant_id=tenant_id,
154
+ )
155
+
156
+ if result["valid"]:
157
+ return EmailAuthResult(
158
+ success=True,
159
+ email=email,
160
+ user_id=result["user_id"],
161
+ message="Successfully authenticated!",
162
+ )
163
+ else:
164
+ error = result.get("error", "Invalid code")
165
+ # User-friendly error messages
166
+ if error == "Login code expired":
167
+ message = "Your code has expired. Please request a new one."
168
+ elif error == "Invalid login code":
169
+ message = "Invalid code. Please check and try again."
170
+ elif error == "No login code requested":
171
+ message = "No code was requested for this email. Please request a new code."
172
+ elif error == "User not found":
173
+ message = "Email not found. Please request a login code first."
174
+ else:
175
+ message = "Verification failed. Please try again."
176
+
177
+ return EmailAuthResult(
178
+ success=False,
179
+ email=email,
180
+ error=error,
181
+ message=message,
182
+ )
183
+
184
+ except Exception as e:
185
+ logger.error(f"Error verifying login code: {e}")
186
+ return EmailAuthResult(
187
+ success=False,
188
+ email=email,
189
+ error=str(e),
190
+ message="An error occurred. Please try again.",
191
+ )
192
+
193
+ def get_user_dict(self, email: str, user_id: str) -> dict:
194
+ """
195
+ Create a user dict for session storage.
196
+
197
+ Compatible with OAuth user format for consistent session handling.
198
+
199
+ Args:
200
+ email: User's email
201
+ user_id: User's UUID
202
+
203
+ Returns:
204
+ User dict for session
205
+ """
206
+ return {
207
+ "id": user_id,
208
+ "email": email,
209
+ "email_verified": True, # Email is verified through code
210
+ "name": email.split("@")[0], # Use email prefix as name
211
+ "provider": "email",
212
+ "tenant_id": "default",
213
+ "tier": "free", # Email users start at free tier
214
+ "roles": ["user"],
215
+ }