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
rem/auth/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ REM Authentication Module.
3
+
4
+ Authentication with support for:
5
+ - Email passwordless login (verification codes)
6
+ - Google OAuth
7
+ - Microsoft Entra ID (Azure AD) OIDC
8
+ - Custom OIDC providers
9
+
10
+ Design Pattern:
11
+ - Provider-agnostic base classes
12
+ - PKCE (Proof Key for Code Exchange) for OAuth flows
13
+ - State parameter for CSRF protection
14
+ - Nonce for ID token replay protection
15
+ - Token validation with JWKS
16
+ - Clean separation: providers/ for auth logic, middleware.py for FastAPI integration
17
+
18
+ Email Auth Flow:
19
+ 1. POST /api/auth/email/send-code with {email}
20
+ 2. User receives code via email
21
+ 3. POST /api/auth/email/verify with {email, code}
22
+ 4. Session created, user authenticated
23
+ """
24
+
25
+ from .providers.base import OAuthProvider
26
+ from .providers.email import EmailAuthProvider, EmailAuthResult
27
+ from .providers.google import GoogleOAuthProvider
28
+ from .providers.microsoft import MicrosoftOAuthProvider
29
+
30
+ __all__ = [
31
+ "OAuthProvider",
32
+ "EmailAuthProvider",
33
+ "EmailAuthResult",
34
+ "GoogleOAuthProvider",
35
+ "MicrosoftOAuthProvider",
36
+ ]
rem/auth/jwt.py ADDED
@@ -0,0 +1,367 @@
1
+ """
2
+ JWT Token Service for REM Authentication.
3
+
4
+ Provides JWT token generation and validation for stateless authentication.
5
+ Uses HS256 algorithm with the session secret for signing.
6
+
7
+ Token Types:
8
+ - Access Token: Short-lived (default 1 hour), used for API authentication
9
+ - Refresh Token: Long-lived (default 7 days), used to obtain new access tokens
10
+
11
+ Token Claims:
12
+ - sub: User ID (UUID string)
13
+ - email: User email
14
+ - name: User display name
15
+ - role: User role (user, admin)
16
+ - tier: User subscription tier
17
+ - roles: List of roles for authorization
18
+ - provider: Auth provider (email, google, microsoft)
19
+ - tenant_id: Tenant identifier for multi-tenancy
20
+ - exp: Expiration timestamp
21
+ - iat: Issued at timestamp
22
+ - type: Token type (access, refresh)
23
+
24
+ Usage:
25
+ from rem.auth.jwt import JWTService
26
+
27
+ jwt_service = JWTService()
28
+
29
+ # Generate tokens after successful authentication
30
+ tokens = jwt_service.create_tokens(user_dict)
31
+ # Returns: {"access_token": "...", "refresh_token": "...", "token_type": "bearer", "expires_in": 3600}
32
+
33
+ # Validate token from Authorization header
34
+ user = jwt_service.verify_token(token)
35
+ # Returns user dict or None if invalid
36
+
37
+ # Refresh access token
38
+ new_tokens = jwt_service.refresh_access_token(refresh_token)
39
+ """
40
+
41
+ import time
42
+ import hmac
43
+ import hashlib
44
+ import base64
45
+ import json
46
+ from datetime import datetime, timezone
47
+ from typing import Optional
48
+
49
+ from loguru import logger
50
+
51
+
52
+ class JWTService:
53
+ """
54
+ JWT token service for authentication.
55
+
56
+ Uses HMAC-SHA256 for signing - simple and secure for single-service deployment.
57
+ For multi-service deployments, consider switching to RS256 with public/private keys.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ secret: str | None = None,
63
+ access_token_expiry_seconds: int = 3600, # 1 hour
64
+ refresh_token_expiry_seconds: int = 604800, # 7 days
65
+ issuer: str = "rem",
66
+ ):
67
+ """
68
+ Initialize JWT service.
69
+
70
+ Args:
71
+ secret: Secret key for signing (uses settings.auth.session_secret if not provided)
72
+ access_token_expiry_seconds: Access token lifetime in seconds
73
+ refresh_token_expiry_seconds: Refresh token lifetime in seconds
74
+ issuer: Token issuer identifier
75
+ """
76
+ if secret:
77
+ self._secret = secret
78
+ else:
79
+ from ..settings import settings
80
+ self._secret = settings.auth.session_secret
81
+
82
+ self._access_expiry = access_token_expiry_seconds
83
+ self._refresh_expiry = refresh_token_expiry_seconds
84
+ self._issuer = issuer
85
+
86
+ def _base64url_encode(self, data: bytes) -> str:
87
+ """Base64url encode without padding."""
88
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("utf-8")
89
+
90
+ def _base64url_decode(self, data: str) -> bytes:
91
+ """Base64url decode with padding restoration."""
92
+ padding = 4 - len(data) % 4
93
+ if padding != 4:
94
+ data += "=" * padding
95
+ return base64.urlsafe_b64decode(data)
96
+
97
+ def _sign(self, message: str) -> str:
98
+ """Create HMAC-SHA256 signature."""
99
+ signature = hmac.new(
100
+ self._secret.encode("utf-8"),
101
+ message.encode("utf-8"),
102
+ hashlib.sha256
103
+ ).digest()
104
+ return self._base64url_encode(signature)
105
+
106
+ def _create_token(self, payload: dict) -> str:
107
+ """
108
+ Create a JWT token.
109
+
110
+ Args:
111
+ payload: Token claims
112
+
113
+ Returns:
114
+ Encoded JWT string
115
+ """
116
+ header = {"alg": "HS256", "typ": "JWT"}
117
+
118
+ header_encoded = self._base64url_encode(json.dumps(header, separators=(",", ":")).encode())
119
+ payload_encoded = self._base64url_encode(json.dumps(payload, separators=(",", ":")).encode())
120
+
121
+ message = f"{header_encoded}.{payload_encoded}"
122
+ signature = self._sign(message)
123
+
124
+ return f"{message}.{signature}"
125
+
126
+ def _verify_signature(self, token: str) -> dict | None:
127
+ """
128
+ Verify token signature and decode payload.
129
+
130
+ Args:
131
+ token: JWT token string
132
+
133
+ Returns:
134
+ Decoded payload dict or None if invalid
135
+ """
136
+ try:
137
+ parts = token.split(".")
138
+ if len(parts) != 3:
139
+ return None
140
+
141
+ header_encoded, payload_encoded, signature = parts
142
+
143
+ # Verify signature
144
+ message = f"{header_encoded}.{payload_encoded}"
145
+ expected_signature = self._sign(message)
146
+
147
+ if not hmac.compare_digest(signature, expected_signature):
148
+ logger.debug("JWT signature verification failed")
149
+ return None
150
+
151
+ # Decode payload
152
+ payload = json.loads(self._base64url_decode(payload_encoded))
153
+ return payload
154
+
155
+ except Exception as e:
156
+ logger.debug(f"JWT decode error: {e}")
157
+ return None
158
+
159
+ def create_tokens(
160
+ self,
161
+ user: dict,
162
+ access_expiry: int | None = None,
163
+ refresh_expiry: int | None = None,
164
+ ) -> dict:
165
+ """
166
+ Create access and refresh tokens for a user.
167
+
168
+ Args:
169
+ user: User dict with id, email, name, role, tier, roles, provider, tenant_id
170
+ access_expiry: Override access token expiry (seconds)
171
+ refresh_expiry: Override refresh token expiry (seconds)
172
+
173
+ Returns:
174
+ Dict with access_token, refresh_token, token_type, expires_in
175
+ """
176
+ now = int(time.time())
177
+ access_exp = access_expiry or self._access_expiry
178
+ refresh_exp = refresh_expiry or self._refresh_expiry
179
+
180
+ # Common claims
181
+ base_claims = {
182
+ "sub": user.get("id", ""),
183
+ "email": user.get("email", ""),
184
+ "name": user.get("name", ""),
185
+ "role": user.get("role"),
186
+ "tier": user.get("tier", "free"),
187
+ "roles": user.get("roles", ["user"]),
188
+ "provider": user.get("provider", "email"),
189
+ "tenant_id": user.get("tenant_id", "default"),
190
+ "iss": self._issuer,
191
+ "iat": now,
192
+ }
193
+
194
+ # Access token
195
+ access_payload = {
196
+ **base_claims,
197
+ "type": "access",
198
+ "exp": now + access_exp,
199
+ }
200
+ access_token = self._create_token(access_payload)
201
+
202
+ # Refresh token (minimal claims for security)
203
+ refresh_payload = {
204
+ "sub": user.get("id", ""),
205
+ "email": user.get("email", ""),
206
+ "type": "refresh",
207
+ "iss": self._issuer,
208
+ "iat": now,
209
+ "exp": now + refresh_exp,
210
+ }
211
+ refresh_token = self._create_token(refresh_payload)
212
+
213
+ return {
214
+ "access_token": access_token,
215
+ "refresh_token": refresh_token,
216
+ "token_type": "bearer",
217
+ "expires_in": access_exp,
218
+ }
219
+
220
+ def verify_token(self, token: str, token_type: str = "access") -> dict | None:
221
+ """
222
+ Verify a token and return user claims.
223
+
224
+ Args:
225
+ token: JWT token string
226
+ token_type: Expected token type ("access" or "refresh")
227
+
228
+ Returns:
229
+ User dict with claims or None if invalid/expired
230
+ """
231
+ payload = self._verify_signature(token)
232
+ if not payload:
233
+ return None
234
+
235
+ # Check token type
236
+ if payload.get("type") != token_type:
237
+ logger.debug(f"Token type mismatch: expected {token_type}, got {payload.get('type')}")
238
+ return None
239
+
240
+ # Check expiration
241
+ exp = payload.get("exp", 0)
242
+ if exp < time.time():
243
+ logger.debug("Token expired")
244
+ return None
245
+
246
+ # Check issuer
247
+ if payload.get("iss") != self._issuer:
248
+ logger.debug(f"Token issuer mismatch: expected {self._issuer}, got {payload.get('iss')}")
249
+ return None
250
+
251
+ # Return user dict (compatible with session user format)
252
+ return {
253
+ "id": payload.get("sub"),
254
+ "email": payload.get("email"),
255
+ "name": payload.get("name"),
256
+ "role": payload.get("role"),
257
+ "tier": payload.get("tier", "free"),
258
+ "roles": payload.get("roles", ["user"]),
259
+ "provider": payload.get("provider", "email"),
260
+ "tenant_id": payload.get("tenant_id", "default"),
261
+ }
262
+
263
+ def refresh_access_token(
264
+ self, refresh_token: str, user_override: dict | None = None
265
+ ) -> dict | None:
266
+ """
267
+ Create new access token using refresh token.
268
+
269
+ Args:
270
+ refresh_token: Valid refresh token
271
+ user_override: Optional dict with user fields to override defaults
272
+ (e.g., role, roles, tier, name from database lookup)
273
+
274
+ Returns:
275
+ New token dict or None if refresh token is invalid
276
+ """
277
+ # Verify refresh token
278
+ payload = self._verify_signature(refresh_token)
279
+ if not payload:
280
+ return None
281
+
282
+ if payload.get("type") != "refresh":
283
+ logger.debug("Not a refresh token")
284
+ return None
285
+
286
+ # Check expiration
287
+ exp = payload.get("exp", 0)
288
+ if exp < time.time():
289
+ logger.debug("Refresh token expired")
290
+ return None
291
+
292
+ # Build user dict with defaults
293
+ user = {
294
+ "id": payload.get("sub"),
295
+ "email": payload.get("email"),
296
+ "name": payload.get("email", "").split("@")[0],
297
+ "provider": "email",
298
+ "tenant_id": "default",
299
+ "tier": "free",
300
+ "role": "user",
301
+ "roles": ["user"],
302
+ }
303
+
304
+ # Apply overrides from database lookup if provided
305
+ if user_override:
306
+ if user_override.get("role"):
307
+ user["role"] = user_override["role"]
308
+ if user_override.get("roles"):
309
+ user["roles"] = user_override["roles"]
310
+ if user_override.get("tier"):
311
+ user["tier"] = user_override["tier"]
312
+ if user_override.get("name"):
313
+ user["name"] = user_override["name"]
314
+
315
+ # Only return new access token, keep same refresh token
316
+ now = int(time.time())
317
+ access_payload = {
318
+ "sub": user["id"],
319
+ "email": user["email"],
320
+ "name": user["name"],
321
+ "role": user["role"],
322
+ "tier": user["tier"],
323
+ "roles": user["roles"],
324
+ "provider": user["provider"],
325
+ "tenant_id": user["tenant_id"],
326
+ "type": "access",
327
+ "iss": self._issuer,
328
+ "iat": now,
329
+ "exp": now + self._access_expiry,
330
+ }
331
+
332
+ return {
333
+ "access_token": self._create_token(access_payload),
334
+ "token_type": "bearer",
335
+ "expires_in": self._access_expiry,
336
+ }
337
+
338
+ def decode_without_verification(self, token: str) -> dict | None:
339
+ """
340
+ Decode token without verification (for debugging only).
341
+
342
+ Args:
343
+ token: JWT token string
344
+
345
+ Returns:
346
+ Decoded payload or None
347
+ """
348
+ try:
349
+ parts = token.split(".")
350
+ if len(parts) != 3:
351
+ return None
352
+ payload = json.loads(self._base64url_decode(parts[1]))
353
+ return payload
354
+ except Exception:
355
+ return None
356
+
357
+
358
+ # Singleton instance for convenience
359
+ _jwt_service: Optional[JWTService] = None
360
+
361
+
362
+ def get_jwt_service() -> JWTService:
363
+ """Get or create the JWT service singleton."""
364
+ global _jwt_service
365
+ if _jwt_service is None:
366
+ _jwt_service = JWTService()
367
+ return _jwt_service