litestar-vite 0.15.0__py3-none-any.whl → 0.15.0rc2__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 (55) hide show
  1. litestar_vite/_codegen/__init__.py +26 -0
  2. litestar_vite/_codegen/inertia.py +407 -0
  3. litestar_vite/{codegen/_openapi.py → _codegen/openapi.py} +11 -58
  4. litestar_vite/{codegen/_routes.py → _codegen/routes.py} +43 -110
  5. litestar_vite/{codegen/_ts.py → _codegen/ts.py} +19 -19
  6. litestar_vite/_handler/__init__.py +8 -0
  7. litestar_vite/{handler/_app.py → _handler/app.py} +29 -117
  8. litestar_vite/cli.py +254 -155
  9. litestar_vite/codegen.py +39 -0
  10. litestar_vite/commands.py +6 -0
  11. litestar_vite/{config/__init__.py → config.py} +726 -99
  12. litestar_vite/deploy.py +3 -14
  13. litestar_vite/doctor.py +6 -8
  14. litestar_vite/executor.py +1 -45
  15. litestar_vite/handler.py +9 -0
  16. litestar_vite/html_transform.py +5 -148
  17. litestar_vite/inertia/__init__.py +0 -24
  18. litestar_vite/inertia/_utils.py +0 -5
  19. litestar_vite/inertia/exception_handler.py +16 -22
  20. litestar_vite/inertia/helpers.py +18 -546
  21. litestar_vite/inertia/plugin.py +11 -77
  22. litestar_vite/inertia/request.py +0 -48
  23. litestar_vite/inertia/response.py +17 -113
  24. litestar_vite/inertia/types.py +0 -19
  25. litestar_vite/loader.py +7 -7
  26. litestar_vite/plugin.py +2184 -0
  27. litestar_vite/templates/angular/package.json.j2 +1 -2
  28. litestar_vite/templates/angular-cli/package.json.j2 +1 -2
  29. litestar_vite/templates/base/package.json.j2 +1 -2
  30. litestar_vite/templates/react-inertia/package.json.j2 +1 -2
  31. litestar_vite/templates/vue-inertia/package.json.j2 +1 -2
  32. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/METADATA +5 -5
  33. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/RECORD +36 -49
  34. litestar_vite/codegen/__init__.py +0 -48
  35. litestar_vite/codegen/_export.py +0 -229
  36. litestar_vite/codegen/_inertia.py +0 -619
  37. litestar_vite/codegen/_utils.py +0 -141
  38. litestar_vite/config/_constants.py +0 -97
  39. litestar_vite/config/_deploy.py +0 -70
  40. litestar_vite/config/_inertia.py +0 -241
  41. litestar_vite/config/_paths.py +0 -63
  42. litestar_vite/config/_runtime.py +0 -235
  43. litestar_vite/config/_spa.py +0 -93
  44. litestar_vite/config/_types.py +0 -94
  45. litestar_vite/handler/__init__.py +0 -9
  46. litestar_vite/inertia/precognition.py +0 -274
  47. litestar_vite/plugin/__init__.py +0 -687
  48. litestar_vite/plugin/_process.py +0 -185
  49. litestar_vite/plugin/_proxy.py +0 -689
  50. litestar_vite/plugin/_proxy_headers.py +0 -244
  51. litestar_vite/plugin/_static.py +0 -37
  52. litestar_vite/plugin/_utils.py +0 -489
  53. /litestar_vite/{handler/_routing.py → _handler/routing.py} +0 -0
  54. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +0 -0
  55. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -1,244 +0,0 @@
1
- """Proxy headers middleware for handling X-Forwarded-* headers securely.
2
-
3
- This module provides middleware to handle X-Forwarded-* headers from reverse proxies
4
- like Railway, Heroku, AWS ALB, nginx, etc.
5
-
6
- Security: Headers are only trusted when the direct caller IP is in the configured
7
- trusted_proxies list. This prevents header spoofing attacks.
8
-
9
- Related: https://github.com/litestar-org/litestar-vite/issues/167
10
- """
11
-
12
- import ipaddress
13
- from typing import TYPE_CHECKING, Any, cast
14
-
15
- from litestar.enums import ScopeType
16
- from litestar.middleware import AbstractMiddleware
17
-
18
- if TYPE_CHECKING:
19
- from litestar.types import ASGIApp, Receive, Scope, Send
20
-
21
- __all__ = ("ProxyHeadersMiddleware", "TrustedHosts")
22
-
23
-
24
- class TrustedHosts:
25
- """Container for trusted proxy hosts and networks.
26
-
27
- Provides efficient lookup for IP addresses and CIDR networks.
28
- Following Uvicorn's security model for proxy header validation.
29
-
30
- Supports:
31
- - Wildcard "*" to trust all hosts (for controlled environments)
32
- - IPv4 addresses: "192.168.1.1"
33
- - IPv6 addresses: "::1"
34
- - CIDR notation: "10.0.0.0/8", "fd00::/8"
35
- - Literals for non-IP hosts (e.g., Unix socket paths)
36
- """
37
-
38
- __slots__ = ("always_trust", "trusted_hosts", "trusted_literals", "trusted_networks")
39
-
40
- def __init__(self, trusted_hosts: "list[str] | str") -> None:
41
- """Initialize trusted hosts container.
42
-
43
- Args:
44
- trusted_hosts: A single host, comma-separated string, or list of hosts.
45
- Use "*" to trust all hosts (only in controlled environments).
46
- """
47
- self.always_trust: bool = trusted_hosts in ("*", ["*"])
48
- self.trusted_literals: set[str] = set()
49
- self.trusted_hosts: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set()
50
- self.trusted_networks: set[ipaddress.IPv4Network | ipaddress.IPv6Network] = set()
51
-
52
- if not self.always_trust:
53
- hosts_list: list[str]
54
- if isinstance(trusted_hosts, str):
55
- hosts_list = [h.strip() for h in trusted_hosts.split(",") if h.strip()]
56
- else:
57
- hosts_list = trusted_hosts
58
-
59
- for host in hosts_list:
60
- if "/" in host:
61
- # CIDR notation
62
- try:
63
- self.trusted_networks.add(ipaddress.ip_network(host, strict=False))
64
- except ValueError:
65
- # Not a valid network, treat as literal
66
- self.trusted_literals.add(host)
67
- else:
68
- try:
69
- self.trusted_hosts.add(ipaddress.ip_address(host))
70
- except ValueError:
71
- # Not a valid IP, treat as literal (e.g., Unix socket path)
72
- self.trusted_literals.add(host)
73
-
74
- def __contains__(self, host: "str | None") -> bool:
75
- """Check if a host is trusted.
76
-
77
- Args:
78
- host: The host to check. Can be an IP address or literal.
79
-
80
- Returns:
81
- True if the host is trusted, False otherwise.
82
- """
83
- # None and empty string are never trusted
84
- if not host:
85
- return False
86
- if self.always_trust:
87
- return True
88
-
89
- try:
90
- ip = ipaddress.ip_address(host)
91
- if ip in self.trusted_hosts:
92
- return True
93
- return any(ip in net for net in self.trusted_networks)
94
- except ValueError:
95
- return host in self.trusted_literals
96
-
97
- def get_trusted_client_host(self, x_forwarded_for: str) -> str:
98
- """Extract the real client IP from X-Forwarded-For header.
99
-
100
- The X-Forwarded-For header contains a comma-separated list of IPs.
101
- Each proxy appends the client IP to the list. We find the first
102
- untrusted host (reading from right to left) which is the real client.
103
-
104
- Args:
105
- x_forwarded_for: The X-Forwarded-For header value.
106
-
107
- Returns:
108
- The first untrusted host in the chain, or the original client
109
- if all hosts are trusted.
110
- """
111
- hosts = [h.strip() for h in x_forwarded_for.split(",") if h.strip()]
112
-
113
- if not hosts:
114
- return ""
115
-
116
- if self.always_trust:
117
- # When trusting all, return the leftmost (original client)
118
- return hosts[0]
119
-
120
- # Each proxy appends to the list, so check in reverse
121
- # Find the first untrusted host from the right
122
- for host in reversed(hosts):
123
- if host not in self:
124
- return host
125
-
126
- # All hosts are trusted - return the original client
127
- return hosts[0]
128
-
129
-
130
- class ProxyHeadersMiddleware(AbstractMiddleware):
131
- """ASGI middleware for secure proxy header handling.
132
-
133
- Only processes X-Forwarded-* headers when the direct caller (scope["client"])
134
- is in the trusted hosts list. This prevents header spoofing attacks.
135
-
136
- Handles:
137
- - X-Forwarded-Proto: Sets scope["scheme"] (http/https/ws/wss)
138
- - X-Forwarded-For: Sets scope["client"] to the real client IP
139
- - X-Forwarded-Host: Optionally sets the Host header
140
-
141
- Security:
142
- Never blindly trusts headers from any client. Validates caller IP
143
- against trusted hosts before reading headers. Validates scheme values
144
- to only allow http/https/ws/wss.
145
-
146
- Example::
147
-
148
- from litestar_vite import VitePlugin, ViteConfig
149
- from litestar_vite.config import RuntimeConfig
150
-
151
- # Trust all proxies (Railway, Heroku, container environments)
152
- app = Litestar(
153
- plugins=[VitePlugin(config=ViteConfig(
154
- runtime=RuntimeConfig(trusted_proxies="*")
155
- ))]
156
- )
157
-
158
- # Trust specific proxy IPs
159
- app = Litestar(
160
- plugins=[VitePlugin(config=ViteConfig(
161
- runtime=RuntimeConfig(trusted_proxies=["10.0.0.0/8", "172.16.0.0/12"])
162
- ))]
163
- )
164
- """
165
-
166
- scopes = {ScopeType.HTTP, ScopeType.WEBSOCKET}
167
-
168
- def __init__(
169
- self, app: "ASGIApp", trusted_hosts: "list[str] | str" = "127.0.0.1", handle_forwarded_host: bool = True
170
- ) -> None:
171
- """Initialize the proxy headers middleware.
172
-
173
- Args:
174
- app: The ASGI application to wrap.
175
- trusted_hosts: Hosts to trust for X-Forwarded-* headers.
176
- Defaults to "127.0.0.1" (localhost only).
177
- handle_forwarded_host: Whether to handle X-Forwarded-Host header
178
- for Host header rewriting. Defaults to True.
179
- """
180
- super().__init__(app)
181
- self.trusted_hosts = TrustedHosts(trusted_hosts)
182
- self.handle_forwarded_host = handle_forwarded_host
183
-
184
- async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
185
- """Process the request and apply proxy headers if trusted.
186
-
187
- Args:
188
- scope: The ASGI scope.
189
- receive: The receive callable.
190
- send: The send callable.
191
- """
192
- client_addr = scope.get("client") # pyright: ignore[reportUnknownMemberType]
193
- client_host = client_addr[0] if client_addr else None
194
-
195
- if client_host in self.trusted_hosts:
196
- # Build a dict of headers for efficient lookup
197
- headers: dict[bytes, bytes] = {}
198
- for key, value in scope.get("headers", []): # pyright: ignore[reportUnknownMemberType]
199
- # Use first occurrence only (as per HTTP spec)
200
- if key not in headers:
201
- headers[key] = value
202
-
203
- scope_dict = cast("dict[str, Any]", scope)
204
-
205
- # X-Forwarded-Proto -> scope["scheme"]
206
- if b"x-forwarded-proto" in headers:
207
- proto = headers[b"x-forwarded-proto"].decode("latin-1").strip().lower()
208
- if proto in {"http", "https", "ws", "wss"}:
209
- # For WebSocket, ensure ws/wss scheme
210
- if scope["type"] == "websocket":
211
- if proto == "https":
212
- scope_dict["scheme"] = "wss"
213
- elif proto == "http":
214
- scope_dict["scheme"] = "ws"
215
- else:
216
- scope_dict["scheme"] = proto
217
- else:
218
- scope_dict["scheme"] = proto
219
-
220
- # X-Forwarded-For -> scope["client"]
221
- if b"x-forwarded-for" in headers:
222
- x_forwarded_for = headers[b"x-forwarded-for"].decode("latin-1")
223
- real_client = self.trusted_hosts.get_trusted_client_host(x_forwarded_for)
224
- if real_client:
225
- scope_dict["client"] = (real_client, 0)
226
-
227
- # X-Forwarded-Host -> replace Host header
228
- if self.handle_forwarded_host and b"x-forwarded-host" in headers:
229
- forwarded_host = headers[b"x-forwarded-host"]
230
- # Rebuild headers list with replaced Host
231
- new_headers: list[tuple[bytes, bytes]] = []
232
- host_replaced = False
233
- for key, value in scope.get("headers", []): # pyright: ignore[reportUnknownMemberType]
234
- if key == b"host" and not host_replaced:
235
- new_headers.append((b"host", forwarded_host))
236
- host_replaced = True
237
- else:
238
- new_headers.append((key, value))
239
- # If no Host header existed, add it
240
- if not host_replaced:
241
- new_headers.append((b"host", forwarded_host))
242
- scope_dict["headers"] = new_headers
243
-
244
- await self.app(scope, receive, send)
@@ -1,37 +0,0 @@
1
- """Static files configuration dataclass."""
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, Any
5
-
6
- if TYPE_CHECKING:
7
- from collections.abc import Sequence
8
-
9
- from litestar.datastructures import CacheControlHeader
10
- from litestar.openapi.spec import SecurityRequirement
11
- from litestar.types import (
12
- AfterRequestHookHandler, # pyright: ignore[reportUnknownVariableType]
13
- AfterResponseHookHandler, # pyright: ignore[reportUnknownVariableType]
14
- BeforeRequestHookHandler, # pyright: ignore[reportUnknownVariableType]
15
- ExceptionHandlersMap,
16
- Guard, # pyright: ignore[reportUnknownVariableType]
17
- Middleware,
18
- )
19
-
20
-
21
- @dataclass
22
- class StaticFilesConfig:
23
- """Configuration for static file serving.
24
-
25
- This configuration is passed to Litestar's static files router.
26
- """
27
-
28
- after_request: "AfterRequestHookHandler | None" = None
29
- after_response: "AfterResponseHookHandler | None" = None
30
- before_request: "BeforeRequestHookHandler | None" = None
31
- cache_control: "CacheControlHeader | None" = None
32
- exception_handlers: "ExceptionHandlersMap | None" = None
33
- guards: "list[Guard] | None" = None # pyright: ignore[reportUnknownVariableType]
34
- middleware: "Sequence[Middleware] | None" = None
35
- opt: "dict[str, Any] | None" = None
36
- security: "Sequence[SecurityRequirement] | None" = None
37
- tags: "Sequence[str] | None" = None