django-datastar 0.1.0__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.
- django_datastar-0.1.0/.gitignore +10 -0
- django_datastar-0.1.0/LICENSE +21 -0
- django_datastar-0.1.0/PKG-INFO +170 -0
- django_datastar-0.1.0/README.md +138 -0
- django_datastar-0.1.0/docs/api.rst +13 -0
- django_datastar-0.1.0/docs/compatibility.rst +63 -0
- django_datastar-0.1.0/docs/conf.py +22 -0
- django_datastar-0.1.0/docs/csrf.rst +64 -0
- django_datastar-0.1.0/docs/index.rst +23 -0
- django_datastar-0.1.0/docs/installation.rst +80 -0
- django_datastar-0.1.0/docs/security.rst +43 -0
- django_datastar-0.1.0/pyproject.toml +96 -0
- django_datastar-0.1.0/src/django_datastar/__init__.py +13 -0
- django_datastar-0.1.0/src/django_datastar/apps.py +8 -0
- django_datastar-0.1.0/src/django_datastar/middleware.py +80 -0
- django_datastar-0.1.0/src/django_datastar/py.typed +0 -0
- django_datastar-0.1.0/src/django_datastar/static/django_datastar/datastar-csrf.js +154 -0
- django_datastar-0.1.0/src/django_datastar/templatetags/__init__.py +0 -0
- django_datastar-0.1.0/src/django_datastar/templatetags/django_datastar.py +44 -0
- django_datastar-0.1.0/tests/__init__.py +0 -0
- django_datastar-0.1.0/tests/installed_package_check.py +43 -0
- django_datastar-0.1.0/tests/settings.py +33 -0
- django_datastar-0.1.0/tests/templates/bootstrap.html +11 -0
- django_datastar-0.1.0/tests/test_csrf.py +88 -0
- django_datastar-0.1.0/tests/test_datastar_csrf.mjs +376 -0
- django_datastar-0.1.0/tests/test_middleware.py +107 -0
- django_datastar-0.1.0/tests/typing_example.py +13 -0
- django_datastar-0.1.0/tests/urls.py +20 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcus A. Lee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-datastar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Request metadata and opt-in CSRF integration for Django and Datastar
|
|
5
|
+
Project-URL: Documentation, https://django-datastar.readthedocs.io/
|
|
6
|
+
Project-URL: Repository, https://github.com/MarcusL11/django_datastar
|
|
7
|
+
Project-URL: Issues, https://github.com/MarcusL11/django_datastar/issues
|
|
8
|
+
Author-email: "Marcus A. Lee" <hello@marcusalee.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 5.2
|
|
14
|
+
Classifier: Framework :: Django :: 6.0
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: django<6.1,>=5.2
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: furo>=2024.8.6; extra == 'docs'
|
|
25
|
+
Requires-Dist: sphinx>=8.1; extra == 'docs'
|
|
26
|
+
Provides-Extra: tests
|
|
27
|
+
Requires-Dist: django-stubs>=5.2; extra == 'tests'
|
|
28
|
+
Requires-Dist: mypy>=1.15; extra == 'tests'
|
|
29
|
+
Requires-Dist: pytest-django>=4.10; extra == 'tests'
|
|
30
|
+
Requires-Dist: pytest>=8.3; extra == 'tests'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# django-datastar
|
|
34
|
+
|
|
35
|
+
`django-datastar` provides small, focused integration points between Django and
|
|
36
|
+
[Datastar](https://data-star.dev/):
|
|
37
|
+
|
|
38
|
+
- exact request classification through `Datastar-Request: true`;
|
|
39
|
+
- sync/async middleware-backed `request.datastar` metadata; and
|
|
40
|
+
- an opt-in bridge that supplies Django CSRF tokens to qualifying Datastar
|
|
41
|
+
backend-action requests.
|
|
42
|
+
|
|
43
|
+
It does not provide Datastar response or SSE APIs. Use
|
|
44
|
+
[`datastar-py`](https://github.com/starfederation/datastar/tree/main/sdk/python)
|
|
45
|
+
as a companion when those APIs are needed.
|
|
46
|
+
|
|
47
|
+
> The `Datastar-Request` header is client-controlled metadata. Never use it for
|
|
48
|
+
> authentication, authorization, permissions, or a CSRF bypass.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Install the package from PyPI:
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
python -m pip install django-datastar
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To install a development checkout instead, run `python -m pip install .` from
|
|
59
|
+
the repository root.
|
|
60
|
+
|
|
61
|
+
Add the middleware before Django's CSRF middleware:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
MIDDLEWARE = [
|
|
65
|
+
# ...
|
|
66
|
+
"django_datastar.middleware.DatastarMiddleware",
|
|
67
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
68
|
+
# ...
|
|
69
|
+
]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use either the helper or the attached details in a view:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from django.http import HttpResponse
|
|
76
|
+
from django_datastar import DatastarHttpRequest
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def update(request: DatastarHttpRequest) -> HttpResponse:
|
|
80
|
+
if request.datastar:
|
|
81
|
+
return HttpResponse("Datastar request")
|
|
82
|
+
return HttpResponse("ordinary request")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`DatastarHttpRequest` is an annotation for requests processed by the middleware;
|
|
86
|
+
Django still creates the actual request object.
|
|
87
|
+
|
|
88
|
+
When middleware-backed request details are not needed, classify a request directly:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from django.http import HttpRequest
|
|
92
|
+
from django.http import HttpResponse
|
|
93
|
+
from django_datastar import is_datastar
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def update(request: HttpRequest) -> HttpResponse:
|
|
97
|
+
if is_datastar(request):
|
|
98
|
+
return HttpResponse("Datastar request")
|
|
99
|
+
return HttpResponse("ordinary request")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Automatic CSRF headers
|
|
103
|
+
|
|
104
|
+
The bridge is optional. Add the app when you want its template tag and static
|
|
105
|
+
module:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
INSTALLED_APPS = [
|
|
109
|
+
# ...
|
|
110
|
+
"django_datastar",
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Load the tag before your chosen Datastar bundle:
|
|
115
|
+
|
|
116
|
+
```django
|
|
117
|
+
{% load django_datastar static %}
|
|
118
|
+
|
|
119
|
+
{% datastar_csrf %}
|
|
120
|
+
<script type="module" src="{% static 'js/datastar.js' %}"></script>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For nonce-based Content Security Policies:
|
|
124
|
+
|
|
125
|
+
```django
|
|
126
|
+
{% datastar_csrf nonce=request.csp_nonce %}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The tag calls Django's CSRF token machinery and emits a masked token in the DOM,
|
|
130
|
+
so it works with `CSRF_COOKIE_HTTPONLY=True`. The external module reads that token
|
|
131
|
+
at request time and injects `X-CSRFToken` only when all of these conditions hold:
|
|
132
|
+
|
|
133
|
+
- the effective method is unsafe;
|
|
134
|
+
- `Datastar-Request` is exactly `true`;
|
|
135
|
+
- the effective target is same-origin;
|
|
136
|
+
- the request mode is compatible with `same-origin`;
|
|
137
|
+
- no CSRF header was supplied explicitly; and
|
|
138
|
+
- a DOM token is present.
|
|
139
|
+
|
|
140
|
+
Django's `CsrfViewMiddleware` remains solely responsible for validation. The
|
|
141
|
+
bridge assumes Datastar resolves `window.fetch` at request time; see the
|
|
142
|
+
[compatibility documentation](https://django-datastar.readthedocs.io/en/latest/compatibility.html)
|
|
143
|
+
before upgrading Datastar.
|
|
144
|
+
|
|
145
|
+
## Documentation
|
|
146
|
+
|
|
147
|
+
Build the documentation locally with:
|
|
148
|
+
|
|
149
|
+
```console
|
|
150
|
+
python -m pip install ".[docs]"
|
|
151
|
+
sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```console
|
|
157
|
+
uv sync --group dev
|
|
158
|
+
uv run pytest
|
|
159
|
+
node --test tests/test_datastar_csrf.mjs
|
|
160
|
+
uv run ruff check .
|
|
161
|
+
uv run ruff format --check .
|
|
162
|
+
uv run mypy
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Node is contributor and CI tooling only. It is not a runtime dependency for
|
|
166
|
+
Django applications.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# django-datastar
|
|
2
|
+
|
|
3
|
+
`django-datastar` provides small, focused integration points between Django and
|
|
4
|
+
[Datastar](https://data-star.dev/):
|
|
5
|
+
|
|
6
|
+
- exact request classification through `Datastar-Request: true`;
|
|
7
|
+
- sync/async middleware-backed `request.datastar` metadata; and
|
|
8
|
+
- an opt-in bridge that supplies Django CSRF tokens to qualifying Datastar
|
|
9
|
+
backend-action requests.
|
|
10
|
+
|
|
11
|
+
It does not provide Datastar response or SSE APIs. Use
|
|
12
|
+
[`datastar-py`](https://github.com/starfederation/datastar/tree/main/sdk/python)
|
|
13
|
+
as a companion when those APIs are needed.
|
|
14
|
+
|
|
15
|
+
> The `Datastar-Request` header is client-controlled metadata. Never use it for
|
|
16
|
+
> authentication, authorization, permissions, or a CSRF bypass.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Install the package from PyPI:
|
|
21
|
+
|
|
22
|
+
```console
|
|
23
|
+
python -m pip install django-datastar
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
To install a development checkout instead, run `python -m pip install .` from
|
|
27
|
+
the repository root.
|
|
28
|
+
|
|
29
|
+
Add the middleware before Django's CSRF middleware:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
MIDDLEWARE = [
|
|
33
|
+
# ...
|
|
34
|
+
"django_datastar.middleware.DatastarMiddleware",
|
|
35
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
36
|
+
# ...
|
|
37
|
+
]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use either the helper or the attached details in a view:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from django.http import HttpResponse
|
|
44
|
+
from django_datastar import DatastarHttpRequest
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def update(request: DatastarHttpRequest) -> HttpResponse:
|
|
48
|
+
if request.datastar:
|
|
49
|
+
return HttpResponse("Datastar request")
|
|
50
|
+
return HttpResponse("ordinary request")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`DatastarHttpRequest` is an annotation for requests processed by the middleware;
|
|
54
|
+
Django still creates the actual request object.
|
|
55
|
+
|
|
56
|
+
When middleware-backed request details are not needed, classify a request directly:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from django.http import HttpRequest
|
|
60
|
+
from django.http import HttpResponse
|
|
61
|
+
from django_datastar import is_datastar
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def update(request: HttpRequest) -> HttpResponse:
|
|
65
|
+
if is_datastar(request):
|
|
66
|
+
return HttpResponse("Datastar request")
|
|
67
|
+
return HttpResponse("ordinary request")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Automatic CSRF headers
|
|
71
|
+
|
|
72
|
+
The bridge is optional. Add the app when you want its template tag and static
|
|
73
|
+
module:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
INSTALLED_APPS = [
|
|
77
|
+
# ...
|
|
78
|
+
"django_datastar",
|
|
79
|
+
]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Load the tag before your chosen Datastar bundle:
|
|
83
|
+
|
|
84
|
+
```django
|
|
85
|
+
{% load django_datastar static %}
|
|
86
|
+
|
|
87
|
+
{% datastar_csrf %}
|
|
88
|
+
<script type="module" src="{% static 'js/datastar.js' %}"></script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For nonce-based Content Security Policies:
|
|
92
|
+
|
|
93
|
+
```django
|
|
94
|
+
{% datastar_csrf nonce=request.csp_nonce %}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The tag calls Django's CSRF token machinery and emits a masked token in the DOM,
|
|
98
|
+
so it works with `CSRF_COOKIE_HTTPONLY=True`. The external module reads that token
|
|
99
|
+
at request time and injects `X-CSRFToken` only when all of these conditions hold:
|
|
100
|
+
|
|
101
|
+
- the effective method is unsafe;
|
|
102
|
+
- `Datastar-Request` is exactly `true`;
|
|
103
|
+
- the effective target is same-origin;
|
|
104
|
+
- the request mode is compatible with `same-origin`;
|
|
105
|
+
- no CSRF header was supplied explicitly; and
|
|
106
|
+
- a DOM token is present.
|
|
107
|
+
|
|
108
|
+
Django's `CsrfViewMiddleware` remains solely responsible for validation. The
|
|
109
|
+
bridge assumes Datastar resolves `window.fetch` at request time; see the
|
|
110
|
+
[compatibility documentation](https://django-datastar.readthedocs.io/en/latest/compatibility.html)
|
|
111
|
+
before upgrading Datastar.
|
|
112
|
+
|
|
113
|
+
## Documentation
|
|
114
|
+
|
|
115
|
+
Build the documentation locally with:
|
|
116
|
+
|
|
117
|
+
```console
|
|
118
|
+
python -m pip install ".[docs]"
|
|
119
|
+
sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```console
|
|
125
|
+
uv sync --group dev
|
|
126
|
+
uv run pytest
|
|
127
|
+
node --test tests/test_datastar_csrf.mjs
|
|
128
|
+
uv run ruff check .
|
|
129
|
+
uv run ruff format --check .
|
|
130
|
+
uv run mypy
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Node is contributor and CI tooling only. It is not a runtime dependency for
|
|
134
|
+
Django applications.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
API reference
|
|
2
|
+
=============
|
|
3
|
+
|
|
4
|
+
.. autofunction:: django_datastar.is_datastar
|
|
5
|
+
|
|
6
|
+
.. autoclass:: django_datastar.DatastarDetails
|
|
7
|
+
:members:
|
|
8
|
+
|
|
9
|
+
.. autoclass:: django_datastar.DatastarHttpRequest
|
|
10
|
+
:members:
|
|
11
|
+
|
|
12
|
+
.. autoclass:: django_datastar.DatastarMiddleware
|
|
13
|
+
:members:
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Compatibility
|
|
2
|
+
=============
|
|
3
|
+
|
|
4
|
+
Supported matrix
|
|
5
|
+
----------------
|
|
6
|
+
|
|
7
|
+
The initial compatibility target is:
|
|
8
|
+
|
|
9
|
+
.. list-table::
|
|
10
|
+
:header-rows: 1
|
|
11
|
+
|
|
12
|
+
* - Python
|
|
13
|
+
- Django 5.2
|
|
14
|
+
- Django 6.0
|
|
15
|
+
* - 3.12
|
|
16
|
+
- tested
|
|
17
|
+
- tested
|
|
18
|
+
* - 3.13
|
|
19
|
+
- tested
|
|
20
|
+
- tested
|
|
21
|
+
* - 3.14
|
|
22
|
+
- tested
|
|
23
|
+
- tested
|
|
24
|
+
|
|
25
|
+
All six combinations pass locally and in CI against the built wheel. The
|
|
26
|
+
runtime dependency is ``Django>=5.2,<6.1``. Node 20 and 22 test the JavaScript
|
|
27
|
+
extracted from the wheel in CI; consuming Django projects do not need Node.
|
|
28
|
+
|
|
29
|
+
Datastar and global fetch
|
|
30
|
+
-------------------------
|
|
31
|
+
|
|
32
|
+
The CSRF bridge wraps ``window.fetch`` before the consumer's Datastar module
|
|
33
|
+
loads. Compatibility therefore assumes that the Datastar bundle resolves the
|
|
34
|
+
global fetch function when it sends a request rather than capturing an earlier
|
|
35
|
+
reference or using another transport.
|
|
36
|
+
|
|
37
|
+
Keep the bridge script before Datastar and repeat a real-browser Network-panel
|
|
38
|
+
check when upgrading Datastar. Confirm that an unsafe same-origin backend action
|
|
39
|
+
contains both ``Datastar-Request: true`` and ``X-CSRFToken`` and is accepted by
|
|
40
|
+
Django.
|
|
41
|
+
|
|
42
|
+
Companion Python package
|
|
43
|
+
------------------------
|
|
44
|
+
|
|
45
|
+
``datastar-py`` supplies response and SSE helpers and can be installed
|
|
46
|
+
separately. This package neither imports nor pins it.
|
|
47
|
+
|
|
48
|
+
Extraction acceptance
|
|
49
|
+
---------------------
|
|
50
|
+
|
|
51
|
+
Before the first release, build a wheel and install it into the original Django
|
|
52
|
+
application that proved the behavior. Replace the local middleware and static
|
|
53
|
+
bridge with the package, render ``{% datastar_csrf %}``, and run that
|
|
54
|
+
application's focused tests, Node tests, full suite, lint, formatting, Django
|
|
55
|
+
checks, and staticfiles collection.
|
|
56
|
+
|
|
57
|
+
Then repeat the real-browser backend-action check. Verify the exact marker and
|
|
58
|
+
masked CSRF header, successful Django response, truthy ``request.datastar``, and
|
|
59
|
+
continued rejection of missing or invalid tokens. Include a temporary
|
|
60
|
+
same-origin endpoint that redirects to a second local origin: the qualifying
|
|
61
|
+
fetch must reject in ``same-origin`` mode, and the second origin must receive
|
|
62
|
+
neither the redirected request nor the CSRF header. Do not remove the local
|
|
63
|
+
implementation until the package-backed path passes.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as package_version
|
|
4
|
+
|
|
5
|
+
project = "django-datastar"
|
|
6
|
+
author = "Marcus A. Lee"
|
|
7
|
+
release = package_version("django-datastar")
|
|
8
|
+
version = release
|
|
9
|
+
|
|
10
|
+
extensions = [
|
|
11
|
+
"sphinx.ext.autodoc",
|
|
12
|
+
"sphinx.ext.intersphinx",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
intersphinx_mapping = {
|
|
16
|
+
"django": ("https://docs.djangoproject.com/en/stable/", None),
|
|
17
|
+
"python": ("https://docs.python.org/3/", None),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
templates_path = ["_templates"]
|
|
21
|
+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|
22
|
+
html_theme = "furo"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Automatic CSRF bridge
|
|
2
|
+
=====================
|
|
3
|
+
|
|
4
|
+
Django's ``CsrfViewMiddleware`` remains authoritative. This bridge does not
|
|
5
|
+
validate tokens, exempt views, or bypass Django's protection. It only supplies
|
|
6
|
+
a masked token to a narrow class of requests.
|
|
7
|
+
|
|
8
|
+
Template setup
|
|
9
|
+
--------------
|
|
10
|
+
|
|
11
|
+
Load the package tag and render it before the consumer's Datastar module:
|
|
12
|
+
|
|
13
|
+
.. code-block:: django
|
|
14
|
+
|
|
15
|
+
{% load django_datastar static %}
|
|
16
|
+
|
|
17
|
+
{% datastar_csrf %}
|
|
18
|
+
<script type="module" src="{% static 'js/datastar.js' %}"></script>
|
|
19
|
+
|
|
20
|
+
The tag requires a request-aware template context. It calls Django's
|
|
21
|
+
``get_token(request)`` and renders:
|
|
22
|
+
|
|
23
|
+
* ``<meta name="datastar-csrf-token" ...>`` containing the escaped masked token;
|
|
24
|
+
* the namespaced ``django_datastar/datastar-csrf.js`` external module.
|
|
25
|
+
|
|
26
|
+
Calling Django's token machinery maintains the CSRF cookie and ``Vary: Cookie``
|
|
27
|
+
response behavior. Because JavaScript reads the masked DOM token rather than the
|
|
28
|
+
cookie, ``CSRF_COOKIE_HTTPONLY=True`` is supported.
|
|
29
|
+
|
|
30
|
+
The order is required: the bridge wraps ``window.fetch``, so it must execute
|
|
31
|
+
before Datastar's module initializes.
|
|
32
|
+
|
|
33
|
+
Content Security Policy
|
|
34
|
+
-----------------------
|
|
35
|
+
|
|
36
|
+
A conventional ``script-src 'self'`` policy can permit the packaged same-origin
|
|
37
|
+
external module. For a nonce-based policy, pass the nonce explicitly:
|
|
38
|
+
|
|
39
|
+
.. code-block:: django
|
|
40
|
+
|
|
41
|
+
{% datastar_csrf nonce=request.csp_nonce %}
|
|
42
|
+
|
|
43
|
+
The nonce value is escaped as an HTML attribute. The package emits no inline
|
|
44
|
+
JavaScript. Applications remain responsible for their complete CSP and for
|
|
45
|
+
configuring their static asset origin.
|
|
46
|
+
|
|
47
|
+
Injection rules
|
|
48
|
+
---------------
|
|
49
|
+
|
|
50
|
+
The bridge adds ``X-CSRFToken`` only when the effective request:
|
|
51
|
+
|
|
52
|
+
* uses an unsafe method;
|
|
53
|
+
* has ``Datastar-Request`` with the exact value ``true``;
|
|
54
|
+
* targets ``window.location.origin``;
|
|
55
|
+
* has a request mode compatible with ``same-origin``;
|
|
56
|
+
* has no explicit case-insensitive CSRF header; and
|
|
57
|
+
* has a current DOM token.
|
|
58
|
+
|
|
59
|
+
It resolves URL, method, headers, and mode from both ``Request`` input and
|
|
60
|
+
``fetch`` init overrides. Token-bearing requests use or preserve
|
|
61
|
+
``mode: "same-origin"``. Accessor-backed ``RequestInit`` members are left to
|
|
62
|
+
native ``fetch`` unchanged rather than being read or rebound by the bridge. If
|
|
63
|
+
the request cannot be safely inspected or transformed, ordinary ``fetch``
|
|
64
|
+
behavior is used without modifying the input.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
django-datastar
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
``django-datastar`` provides request metadata and an opt-in automatic CSRF
|
|
5
|
+
bridge for Django applications using Datastar.
|
|
6
|
+
|
|
7
|
+
It deliberately does not duplicate Datastar response or SSE APIs. The
|
|
8
|
+
``datastar-py`` project can be used alongside it when those APIs are needed.
|
|
9
|
+
|
|
10
|
+
.. warning::
|
|
11
|
+
|
|
12
|
+
``Datastar-Request: true`` is client-controlled metadata. It is never an
|
|
13
|
+
authentication, authorization, permission, or CSRF boundary.
|
|
14
|
+
|
|
15
|
+
.. toctree::
|
|
16
|
+
:maxdepth: 2
|
|
17
|
+
:caption: Contents
|
|
18
|
+
|
|
19
|
+
installation
|
|
20
|
+
csrf
|
|
21
|
+
security
|
|
22
|
+
compatibility
|
|
23
|
+
api
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Installation
|
|
2
|
+
============
|
|
3
|
+
|
|
4
|
+
Install the package from PyPI:
|
|
5
|
+
|
|
6
|
+
.. code-block:: console
|
|
7
|
+
|
|
8
|
+
python -m pip install django-datastar
|
|
9
|
+
|
|
10
|
+
To install a development checkout instead, run ``python -m pip install .`` from
|
|
11
|
+
the repository root.
|
|
12
|
+
|
|
13
|
+
Request metadata
|
|
14
|
+
----------------
|
|
15
|
+
|
|
16
|
+
Add the middleware before Django's CSRF middleware:
|
|
17
|
+
|
|
18
|
+
.. code-block:: python
|
|
19
|
+
|
|
20
|
+
MIDDLEWARE = [
|
|
21
|
+
# ...
|
|
22
|
+
"django_datastar.middleware.DatastarMiddleware",
|
|
23
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
24
|
+
# ...
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
Every request that reaches the rest of the middleware chain then has a
|
|
28
|
+
``datastar`` details object. Its truth value is true only when the
|
|
29
|
+
``Datastar-Request`` header value is exactly ``true``.
|
|
30
|
+
|
|
31
|
+
For a typed view annotation:
|
|
32
|
+
|
|
33
|
+
.. code-block:: python
|
|
34
|
+
|
|
35
|
+
from django.http import HttpResponse
|
|
36
|
+
from django_datastar import DatastarHttpRequest
|
|
37
|
+
|
|
38
|
+
def update(request: DatastarHttpRequest) -> HttpResponse:
|
|
39
|
+
if request.datastar:
|
|
40
|
+
return HttpResponse("Datastar request")
|
|
41
|
+
return HttpResponse("ordinary request")
|
|
42
|
+
|
|
43
|
+
``DatastarHttpRequest`` describes the middleware-added attribute for type
|
|
44
|
+
checkers. Django continues to construct its normal request object; do not use
|
|
45
|
+
the annotation class as a runtime ``isinstance`` test.
|
|
46
|
+
|
|
47
|
+
Direct request classification
|
|
48
|
+
-----------------------------
|
|
49
|
+
|
|
50
|
+
Use ``is_datastar`` when middleware-backed request details are not needed:
|
|
51
|
+
|
|
52
|
+
.. code-block:: python
|
|
53
|
+
|
|
54
|
+
from django.http import HttpRequest
|
|
55
|
+
from django.http import HttpResponse
|
|
56
|
+
from django_datastar import is_datastar
|
|
57
|
+
|
|
58
|
+
def update(request: HttpRequest) -> HttpResponse:
|
|
59
|
+
if is_datastar(request):
|
|
60
|
+
return HttpResponse("Datastar request")
|
|
61
|
+
return HttpResponse("ordinary request")
|
|
62
|
+
|
|
63
|
+
The helper and ``request.datastar`` use the same exact, case-sensitive header
|
|
64
|
+
check. The header remains client-controlled metadata and must not authorize a
|
|
65
|
+
request or bypass CSRF protection.
|
|
66
|
+
|
|
67
|
+
Optional CSRF bridge
|
|
68
|
+
--------------------
|
|
69
|
+
|
|
70
|
+
To use the template tag and packaged static module, add the app:
|
|
71
|
+
|
|
72
|
+
.. code-block:: python
|
|
73
|
+
|
|
74
|
+
INSTALLED_APPS = [
|
|
75
|
+
# ...
|
|
76
|
+
"django_datastar",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
Django's ``staticfiles`` app and a normal staticfiles deployment are required.
|
|
80
|
+
Continue with :doc:`csrf` for template setup.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Security
|
|
2
|
+
========
|
|
3
|
+
|
|
4
|
+
Untrusted marker
|
|
5
|
+
----------------
|
|
6
|
+
|
|
7
|
+
The ``Datastar-Request`` header can be sent by any client. Its exact ``true``
|
|
8
|
+
value identifies the request format; it does not prove that Datastar, this
|
|
9
|
+
package, or a trusted browser originated the request.
|
|
10
|
+
|
|
11
|
+
Never use the marker or ``request.datastar`` to:
|
|
12
|
+
|
|
13
|
+
* authenticate a user;
|
|
14
|
+
* grant a permission;
|
|
15
|
+
* authorize an operation;
|
|
16
|
+
* skip CSRF checks; or
|
|
17
|
+
* weaken any other security control.
|
|
18
|
+
|
|
19
|
+
Normal Django authentication, authorization, method, origin, and CSRF controls
|
|
20
|
+
must still run.
|
|
21
|
+
|
|
22
|
+
CSRF authority
|
|
23
|
+
--------------
|
|
24
|
+
|
|
25
|
+
``CsrfViewMiddleware`` performs all server-side CSRF validation. A missing,
|
|
26
|
+
invalid, or inapplicable token is rejected exactly as it would be without this
|
|
27
|
+
package. Do not decorate Datastar endpoints with ``csrf_exempt`` merely because
|
|
28
|
+
the bridge is installed.
|
|
29
|
+
|
|
30
|
+
Header disclosure boundary
|
|
31
|
+
--------------------------
|
|
32
|
+
|
|
33
|
+
The browser bridge limits token injection to same-origin effective targets and
|
|
34
|
+
sets ``mode: "same-origin"``. Cross-origin, explicitly incompatible-mode, safe,
|
|
35
|
+
unmarked, and uninspectable requests pass through unchanged. Explicit CSRF
|
|
36
|
+
headers always win and are not validated or replaced by the bridge.
|
|
37
|
+
|
|
38
|
+
Caching
|
|
39
|
+
-------
|
|
40
|
+
|
|
41
|
+
If a cacheable response changes according to ``request.datastar``, include the
|
|
42
|
+
marker in the cache key, for example with Django's
|
|
43
|
+
``vary_on_headers("Datastar-Request")`` decorator.
|