sql-dialect-master 1.1.6__tar.gz → 1.1.7__tar.gz

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 (67) hide show
  1. {sql_dialect_master-1.1.6/sql_dialect_master.egg-info → sql_dialect_master-1.1.7}/PKG-INFO +15 -2
  2. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/README.md +14 -1
  3. sql_dialect_master-1.1.7/backend/api/client_identity.py +57 -0
  4. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/main.py +8 -1
  5. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/middleware.py +43 -6
  6. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/rate_limit_store.py +2 -2
  7. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/config.py +36 -0
  8. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/metadata.py +2 -2
  9. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/pyproject.toml +1 -1
  10. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7/sql_dialect_master.egg-info}/PKG-INFO +15 -2
  11. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/SOURCES.txt +1 -0
  12. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/LICENSE +0 -0
  13. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/__init__.py +0 -0
  14. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/__init__.py +0 -0
  15. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/auth.py +0 -0
  16. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/readiness.py +0 -0
  17. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/api/workers.py +0 -0
  18. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/__init__.py +0 -0
  19. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/cache.py +0 -0
  20. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/column_hint_validation.py +0 -0
  21. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/constants.py +0 -0
  22. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/exceptions.py +0 -0
  23. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/function_call_scanner.py +0 -0
  24. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/function_semantics.py +0 -0
  25. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/functions_db.json +0 -0
  26. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/functions_lookup.py +0 -0
  27. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql.py +0 -0
  28. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/__init__.py +0 -0
  29. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/aggregations.py +0 -0
  30. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
  31. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/core_extract.py +0 -0
  32. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/evidence.py +0 -0
  33. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/mappings.py +0 -0
  34. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/relations.py +0 -0
  35. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/templates.py +0 -0
  36. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_components/tokenizer.py +0 -0
  37. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_legacy.py +0 -0
  38. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/__init__.py +0 -0
  39. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
  40. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
  41. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/dialect.py +0 -0
  42. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/intent.py +0 -0
  43. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/ir.py +0 -0
  44. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/models.py +0 -0
  45. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/orchestrator.py +0 -0
  46. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/predicates.py +0 -0
  47. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/renderer.py +0 -0
  48. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/resolution.py +0 -0
  49. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
  50. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/templates.py +0 -0
  51. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/nl2sql_pipeline/validation.py +0 -0
  52. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/p1_sql_scanner.py +0 -0
  53. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/p2_data_validation.py +0 -0
  54. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/parser.py +0 -0
  55. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/post_processor.py +0 -0
  56. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/rules.py +0 -0
  57. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/semantic_diff.py +0 -0
  58. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/sql_statements.py +0 -0
  59. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/transpiler.py +0 -0
  60. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/type_mapping.json +0 -0
  61. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/core/type_mapping.py +0 -0
  62. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/utils/__init__.py +0 -0
  63. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/backend/utils/validators.py +0 -0
  64. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/setup.cfg +0 -0
  65. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
  66. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/requires.txt +0 -0
  67. {sql_dialect_master-1.1.6 → sql_dialect_master-1.1.7}/sql_dialect_master.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql-dialect-master
3
- Version: 1.1.6
3
+ Version: 1.1.7
4
4
  Summary: SQL Dialect Master - Multi-database SQL conversion engine
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -35,7 +35,7 @@ Dynamic: license-file
35
35
 
36
36
  Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
37
37
 
38
- Current release: 1.1.6
38
+ Current release: 1.1.7
39
39
 
40
40
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
41
41
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -359,3 +359,16 @@ Rate limiting is on by default. The two backends are:
359
359
  - `SDM_RATE_LIMIT_BACKEND=redis` — shared, atomic limits across all workers. Set `SDM_REDIS_URL` when using this backend. The store connects and pings Redis at startup and fails fast if it is unreachable; the `redis` package is required (install with `pip install redis`).
360
360
 
361
361
  `SDM_RATE_LIMIT_ENABLED=false` disables enforcement entirely. Use the in-memory backend for local development and the Redis backend for any deployment with more than one worker or instance that must enforce a single global limit.
362
+
363
+ By default, the API does not trust `X-Forwarded-For`; rate limiting and logs
364
+ use the direct peer address. Set `SDM_TRUSTED_PROXIES` to the proxy IPs/CIDRs
365
+ only when the direct peer is a trusted proxy, after which the rightmost
366
+ non-trusted address in the XFF chain is used. Deployments behind a proxy must
367
+ also configure `SDM_HEALTH_PROBE_TOKEN` for readiness probes; readiness never
368
+ uses XFF for authorization.
369
+
370
+ SQL-bearing POST responses and health probes use `Cache-Control: no-store`;
371
+ public catalog responses use short `private` caching. Full deployment details
372
+ will be documented in `docs/deployment.md`.
373
+
374
+ See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
4
4
 
5
- Current release: 1.1.6
5
+ Current release: 1.1.7
6
6
 
7
7
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -326,3 +326,16 @@ Rate limiting is on by default. The two backends are:
326
326
  - `SDM_RATE_LIMIT_BACKEND=redis` — shared, atomic limits across all workers. Set `SDM_REDIS_URL` when using this backend. The store connects and pings Redis at startup and fails fast if it is unreachable; the `redis` package is required (install with `pip install redis`).
327
327
 
328
328
  `SDM_RATE_LIMIT_ENABLED=false` disables enforcement entirely. Use the in-memory backend for local development and the Redis backend for any deployment with more than one worker or instance that must enforce a single global limit.
329
+
330
+ By default, the API does not trust `X-Forwarded-For`; rate limiting and logs
331
+ use the direct peer address. Set `SDM_TRUSTED_PROXIES` to the proxy IPs/CIDRs
332
+ only when the direct peer is a trusted proxy, after which the rightmost
333
+ non-trusted address in the XFF chain is used. Deployments behind a proxy must
334
+ also configure `SDM_HEALTH_PROBE_TOKEN` for readiness probes; readiness never
335
+ uses XFF for authorization.
336
+
337
+ SQL-bearing POST responses and health probes use `Cache-Control: no-store`;
338
+ public catalog responses use short `private` caching. Full deployment details
339
+ will be documented in `docs/deployment.md`.
340
+
341
+ See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
@@ -0,0 +1,57 @@
1
+ """Resolve client IPs without trusting spoofable forwarding headers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import re
7
+ from collections.abc import Iterable
8
+
9
+ from fastapi import Request
10
+
11
+ _BRACKETED_IPV6 = re.compile(r"^\[([^\]]+)\](?::\d+)?$")
12
+
13
+
14
+ def _parse_ip(value: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
15
+ candidate = value.strip()
16
+ if not candidate or "%" in candidate:
17
+ return None
18
+ bracketed = _BRACKETED_IPV6.fullmatch(candidate)
19
+ if bracketed:
20
+ candidate = bracketed.group(1)
21
+ elif candidate.count(":") == 1:
22
+ host, port = candidate.rsplit(":", 1)
23
+ if port.isdigit():
24
+ candidate = host
25
+ try:
26
+ return ipaddress.ip_address(candidate)
27
+ except ValueError:
28
+ return None
29
+
30
+
31
+ def _is_trusted(
32
+ address: ipaddress.IPv4Address | ipaddress.IPv6Address,
33
+ trusted_networks: Iterable,
34
+ ) -> bool:
35
+ return any(
36
+ address.version == network.version and address in network
37
+ for network in trusted_networks
38
+ )
39
+
40
+
41
+ def resolve_client_ip(request: Request, trusted_networks: tuple = ()) -> str:
42
+ """Resolve the client address, trusting XFF only behind trusted peers."""
43
+ peer = request.client.host if request.client else "unknown"
44
+ if not trusted_networks or peer == "unknown":
45
+ return peer
46
+
47
+ peer_address = _parse_ip(peer)
48
+ if peer_address is None or not _is_trusted(peer_address, trusted_networks):
49
+ return peer
50
+
51
+ forwarded_for = request.headers.get("X-Forwarded-For", "")
52
+ for value in reversed(forwarded_for.split(",")):
53
+ address = _parse_ip(value)
54
+ if address is None or _is_trusted(address, trusted_networks):
55
+ continue
56
+ return str(address)
57
+ return peer
@@ -235,6 +235,7 @@ app = FastAPI(
235
235
 
236
236
  # Import middleware
237
237
  from backend.api.middleware import (
238
+ CacheControlMiddleware,
238
239
  RateLimiter,
239
240
  RateLimitMiddleware,
240
241
  SecurityHeadersMiddleware,
@@ -244,6 +245,7 @@ from backend.core.exceptions import SDMException, UnsupportedDialectError, Error
244
245
 
245
246
  # Configure CORS from the centralized Pydantic settings source.
246
247
  ALLOWED_ORIGINS = [origin.strip() for origin in settings.allowed_origins.split(",") if origin.strip()]
248
+ TRUSTED_PROXY_NETWORKS = settings.trusted_proxy_networks
247
249
 
248
250
  app.add_middleware(
249
251
  CORSMiddleware,
@@ -285,10 +287,15 @@ app.add_middleware(
285
287
  RateLimitMiddleware,
286
288
  limiter=rate_limiter,
287
289
  enabled=settings.rate_limit_enabled,
290
+ trusted_proxies=TRUSTED_PROXY_NETWORKS,
288
291
  )
289
292
 
290
293
  # Structured request logging must wrap rate limiting so rejected requests also get IDs.
291
- app.add_middleware(StructuredLoggingMiddleware)
294
+ app.add_middleware(
295
+ StructuredLoggingMiddleware,
296
+ trusted_proxies=TRUSTED_PROXY_NETWORKS,
297
+ )
298
+ app.add_middleware(CacheControlMiddleware)
292
299
 
293
300
  # Initialize services
294
301
  transpiler = SQLTranspiler()
@@ -19,6 +19,7 @@ from starlette.concurrency import run_in_threadpool
19
19
 
20
20
  from backend.core.exceptions import ErrorCode
21
21
 
22
+ from .client_identity import resolve_client_ip
22
23
  from .rate_limit_store import InMemoryRateLimitStore, RateLimitStore, create_rate_limit_store
23
24
  from .readiness import health_probe_response
24
25
 
@@ -35,7 +36,7 @@ def _sanitize_log_value(value: str, max_length: int = 500) -> str:
35
36
  class RateLimitEntry:
36
37
  """Legacy compatibility entry."""
37
38
  requests: int = 0
38
- window_start: float = field(default_factory=time.time)
39
+ window_start: float = field(default_factory=time.monotonic)
39
40
 
40
41
 
41
42
  class RateLimiter:
@@ -67,13 +68,14 @@ class RateLimiter:
67
68
  class RateLimitMiddleware(BaseHTTPMiddleware):
68
69
  """Rate limiting middleware with a bounded request-body guard."""
69
70
 
70
- def __init__(self, app, limiter: RateLimiter = None, enabled: bool = True, max_body_bytes: int = DEFAULT_MAX_REQUEST_BODY_BYTES):
71
+ def __init__(self, app, limiter: RateLimiter = None, enabled: bool = True, max_body_bytes: int = DEFAULT_MAX_REQUEST_BODY_BYTES, trusted_proxies: tuple = ()):
71
72
  super().__init__(app)
72
73
  if max_body_bytes <= 0:
73
74
  raise ValueError("max_body_bytes must be positive")
74
75
  self.limiter = limiter or RateLimiter(store=None if enabled else InMemoryRateLimitStore())
75
76
  self.enabled = enabled
76
77
  self.max_body_bytes = max_body_bytes
78
+ self.trusted_proxies = trusted_proxies
77
79
 
78
80
  async def _read_bounded_body(self, request: Request) -> Response | None:
79
81
  """Consume and cache request bodies without trusting Content-Length."""
@@ -171,15 +173,16 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
171
173
  return response
172
174
 
173
175
  def _get_client_id(self, request: Request) -> str:
174
- return request.client.host if request.client else "unknown"
176
+ return resolve_client_ip(request, self.trusted_proxies)
175
177
 
176
178
 
177
179
  class StructuredLoggingMiddleware(BaseHTTPMiddleware):
178
180
  """Structured request logging and request-id propagation."""
179
181
 
180
- def __init__(self, app, log_body: bool = False):
182
+ def __init__(self, app, log_body: bool = False, trusted_proxies: tuple = ()):
181
183
  super().__init__(app)
182
184
  self.log_body = log_body
185
+ self.trusted_proxies = trusted_proxies
183
186
 
184
187
  @staticmethod
185
188
  def _get_request_id(request: Request) -> str:
@@ -346,7 +349,7 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
346
349
  request.state.request_id = request_id
347
350
  status_code = 500
348
351
  error = None
349
- log_data = {"event": "request_start", "request_id": request_id, "method": _sanitize_log_value(request.method), "path": _sanitize_log_value(request.url.path), "query": _sanitize_log_value(str(request.query_params)), "client": _sanitize_log_value(request.client.host if request.client else "unknown"), "timestamp": datetime.now().isoformat()}
352
+ log_data = {"event": "request_start", "request_id": request_id, "method": _sanitize_log_value(request.method), "path": _sanitize_log_value(request.url.path), "query": _sanitize_log_value(str(request.query_params)), "client": _sanitize_log_value(resolve_client_ip(request, self.trusted_proxies)), "timestamp": datetime.now().isoformat()}
350
353
  logger.info(json.dumps(log_data))
351
354
  try:
352
355
  response = await call_next(request)
@@ -370,6 +373,40 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
370
373
  response = await call_next(request)
371
374
  response.headers["X-Content-Type-Options"] = "nosniff"
372
375
  response.headers["X-Frame-Options"] = "DENY"
373
- response.headers["X-XSS-Protection"] = "1; mode=block"
376
+ response.headers["X-XSS-Protection"] = "0"
374
377
  response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
375
378
  return response
379
+
380
+
381
+ class CacheControlMiddleware(BaseHTTPMiddleware):
382
+ """Apply explicit browser/proxy caching policy to API responses."""
383
+
384
+ @staticmethod
385
+ def _policy(request: Request) -> str | None:
386
+ if request.method == "OPTIONS":
387
+ return None
388
+ path = request.url.path
389
+ if path in {"/docs", "/redoc", "/openapi.json"}:
390
+ return None
391
+ if request.method == "POST" and path.startswith("/api/"):
392
+ return "no-store"
393
+ if request.method in {"GET", "HEAD"}:
394
+ if path in {"/health", "/ready", "/health/deep"}:
395
+ return "no-store"
396
+ if path == "/api/dialects":
397
+ return "private, max-age=60"
398
+ if path == "/api/functions" or path.startswith("/api/functions/"):
399
+ return "private, max-age=60"
400
+ if path == "/api/types" or path.startswith("/api/types/"):
401
+ return "private, max-age=60"
402
+ if path in {"/", "/api/stats"}:
403
+ return "private, max-age=15"
404
+ return None
405
+
406
+ async def dispatch(self, request: Request, call_next) -> Response:
407
+ response = await call_next(request)
408
+ if not response.headers.get("Cache-Control"):
409
+ policy = self._policy(request)
410
+ if policy:
411
+ response.headers["Cache-Control"] = policy
412
+ return response
@@ -44,7 +44,7 @@ class InMemoryRateLimitStore:
44
44
 
45
45
  def check(self, key: str, limit: int, window_seconds: int) -> tuple[bool, int, int]:
46
46
  with self._lock:
47
- now = time.time()
47
+ now = time.monotonic()
48
48
  self._prune_expired(now)
49
49
  count, window_start = self._entries.get(key, (0, now))
50
50
  if now - window_start >= window_seconds:
@@ -58,7 +58,7 @@ class InMemoryRateLimitStore:
58
58
 
59
59
  def stats(self) -> dict:
60
60
  with self._lock:
61
- self._prune_expired(time.time())
61
+ self._prune_expired(time.monotonic())
62
62
  return {"backend": "memory", "active_clients": len(self._entries)}
63
63
 
64
64
 
@@ -10,6 +10,7 @@ Supports configuration via:
10
10
  - Default values
11
11
  """
12
12
  import logging
13
+ import ipaddress
13
14
  import sys
14
15
  from enum import Enum
15
16
  from typing import Any, Dict, List, Literal, Optional, TypedDict
@@ -225,6 +226,7 @@ class AppSettings(BaseSettings):
225
226
  "http://localhost:5174,http://127.0.0.1:5174,"
226
227
  "http://localhost:4174,http://127.0.0.1:4174"
227
228
  )
229
+ trusted_proxies: str = ""
228
230
  max_batch_size: int = Field(100, gt=0)
229
231
  request_timeout: int = Field(30, gt=0)
230
232
 
@@ -299,6 +301,40 @@ class AppSettings(BaseSettings):
299
301
  "log_level must be one of DEBUG, INFO, WARNING, ERROR, CRITICAL"
300
302
  )
301
303
  return normalized
304
+
305
+ @field_validator("trusted_proxies", mode="before")
306
+ @classmethod
307
+ def _normalize_trusted_proxies(cls, value: str) -> str:
308
+ if value is None:
309
+ return ""
310
+ if not isinstance(value, str):
311
+ raise ValueError("trusted_proxies must be a comma-separated string")
312
+ normalized = []
313
+ for raw_value in value.split(","):
314
+ candidate = raw_value.strip()
315
+ if not candidate:
316
+ continue
317
+ try:
318
+ network = ipaddress.ip_network(candidate, strict=False)
319
+ except ValueError as exc:
320
+ raise ValueError(
321
+ f"trusted_proxies contains an invalid IP/CIDR: {candidate!r}"
322
+ ) from exc
323
+ if network.prefixlen == 0:
324
+ raise ValueError("trusted_proxies must not contain a catch-all network")
325
+ normalized.append(str(network))
326
+ return ",".join(normalized)
327
+
328
+ @property
329
+ def trusted_proxy_networks(self) -> tuple:
330
+ """Return parsed trusted proxy networks for request identity checks."""
331
+ if not self.trusted_proxies:
332
+ return ()
333
+ return tuple(
334
+ ipaddress.ip_network(value, strict=False)
335
+ for value in self.trusted_proxies.split(",")
336
+ if value
337
+ )
302
338
 
303
339
  def to_dict(self) -> Dict[str, Any]:
304
340
  """Convert settings to dictionary."""
@@ -17,8 +17,8 @@ Design rules:
17
17
  from __future__ import annotations
18
18
 
19
19
  # ── Release version facts (sole source of truth) ────────────────────────
20
- PROJECT_VERSION = "1.1.6"
21
- API_VERSION = "1.1.6"
20
+ PROJECT_VERSION = "1.1.7"
21
+ API_VERSION = "1.1.7"
22
22
  # Frontend v2 architecture — independent of the product release version.
23
23
  FRONTEND_ARCHITECTURE_VERSION = 2
24
24
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql-dialect-master"
3
- version = "1.1.6"
3
+ version = "1.1.7"
4
4
  description = "SQL Dialect Master - Multi-database SQL conversion engine"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql-dialect-master
3
- Version: 1.1.6
3
+ Version: 1.1.7
4
4
  Summary: SQL Dialect Master - Multi-database SQL conversion engine
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -35,7 +35,7 @@ Dynamic: license-file
35
35
 
36
36
  Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
37
37
 
38
- Current release: 1.1.6
38
+ Current release: 1.1.7
39
39
 
40
40
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
41
41
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -359,3 +359,16 @@ Rate limiting is on by default. The two backends are:
359
359
  - `SDM_RATE_LIMIT_BACKEND=redis` — shared, atomic limits across all workers. Set `SDM_REDIS_URL` when using this backend. The store connects and pings Redis at startup and fails fast if it is unreachable; the `redis` package is required (install with `pip install redis`).
360
360
 
361
361
  `SDM_RATE_LIMIT_ENABLED=false` disables enforcement entirely. Use the in-memory backend for local development and the Redis backend for any deployment with more than one worker or instance that must enforce a single global limit.
362
+
363
+ By default, the API does not trust `X-Forwarded-For`; rate limiting and logs
364
+ use the direct peer address. Set `SDM_TRUSTED_PROXIES` to the proxy IPs/CIDRs
365
+ only when the direct peer is a trusted proxy, after which the rightmost
366
+ non-trusted address in the XFF chain is used. Deployments behind a proxy must
367
+ also configure `SDM_HEALTH_PROBE_TOKEN` for readiness probes; readiness never
368
+ uses XFF for authorization.
369
+
370
+ SQL-bearing POST responses and health probes use `Cache-Control: no-store`;
371
+ public catalog responses use short `private` caching. Full deployment details
372
+ will be documented in `docs/deployment.md`.
373
+
374
+ See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  backend/__init__.py
5
5
  backend/api/__init__.py
6
6
  backend/api/auth.py
7
+ backend/api/client_identity.py
7
8
  backend/api/main.py
8
9
  backend/api/middleware.py
9
10
  backend/api/rate_limit_store.py