enlace_auth 0.1.1__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 (39) hide show
  1. enlace_auth-0.1.1/.github/workflows/ci.yml +256 -0
  2. enlace_auth-0.1.1/.gitignore +121 -0
  3. enlace_auth-0.1.1/LICENSE +21 -0
  4. enlace_auth-0.1.1/PKG-INFO +106 -0
  5. enlace_auth-0.1.1/README.md +78 -0
  6. enlace_auth-0.1.1/enlace_auth/__init__.py +40 -0
  7. enlace_auth-0.1.1/enlace_auth/__main__.py +145 -0
  8. enlace_auth-0.1.1/enlace_auth/admin/__init__.py +14 -0
  9. enlace_auth-0.1.1/enlace_auth/admin/routes.py +157 -0
  10. enlace_auth-0.1.1/enlace_auth/auth/__init__.py +35 -0
  11. enlace_auth-0.1.1/enlace_auth/auth/cookies.py +46 -0
  12. enlace_auth-0.1.1/enlace_auth/auth/middleware.py +393 -0
  13. enlace_auth-0.1.1/enlace_auth/auth/oauth.py +168 -0
  14. enlace_auth-0.1.1/enlace_auth/auth/passwords.py +40 -0
  15. enlace_auth-0.1.1/enlace_auth/auth/routes.py +202 -0
  16. enlace_auth-0.1.1/enlace_auth/auth/sessions.py +55 -0
  17. enlace_auth-0.1.1/enlace_auth/config.py +89 -0
  18. enlace_auth-0.1.1/enlace_auth/diagnostics.py +185 -0
  19. enlace_auth-0.1.1/enlace_auth/plugin.py +254 -0
  20. enlace_auth-0.1.1/enlace_auth/stores/__init__.py +28 -0
  21. enlace_auth-0.1.1/enlace_auth/stores/backends.py +117 -0
  22. enlace_auth-0.1.1/enlace_auth/stores/middleware.py +124 -0
  23. enlace_auth-0.1.1/enlace_auth/stores/prefixed.py +73 -0
  24. enlace_auth-0.1.1/enlace_auth/stores/validation.py +59 -0
  25. enlace_auth-0.1.1/pyproject.toml +66 -0
  26. enlace_auth-0.1.1/tests/__init__.py +0 -0
  27. enlace_auth-0.1.1/tests/conftest.py +98 -0
  28. enlace_auth-0.1.1/tests/test_admin.py +210 -0
  29. enlace_auth-0.1.1/tests/test_auth_e2e.py +212 -0
  30. enlace_auth-0.1.1/tests/test_auth_failfast.py +103 -0
  31. enlace_auth-0.1.1/tests/test_auth_middleware.py +204 -0
  32. enlace_auth-0.1.1/tests/test_base_auth_config.py +75 -0
  33. enlace_auth-0.1.1/tests/test_csrf.py +110 -0
  34. enlace_auth-0.1.1/tests/test_doctor.py +220 -0
  35. enlace_auth-0.1.1/tests/test_oauth.py +98 -0
  36. enlace_auth-0.1.1/tests/test_passwords.py +29 -0
  37. enlace_auth-0.1.1/tests/test_prefixed_store.py +84 -0
  38. enlace_auth-0.1.1/tests/test_sessions.py +46 -0
  39. enlace_auth-0.1.1/tests/test_store_middleware.py +123 -0
@@ -0,0 +1,256 @@
1
+ name: Continuous Integration (uv)
2
+ on: [push, pull_request]
3
+
4
+ # Note: Environment variables (PROJECT_NAME and vars from [tool.wads.ci.env])
5
+ # are set by the read-ci-config action in the setup job and made available
6
+ # to all subsequent jobs via GITHUB_ENV
7
+
8
+ jobs:
9
+ # First job: Read configuration from pyproject.toml
10
+ setup:
11
+ name: Read Configuration
12
+ runs-on: ubuntu-latest
13
+ outputs:
14
+ project-name: ${{ steps.config.outputs.project-name }}
15
+ python-versions: ${{ steps.config.outputs.python-versions }}
16
+ pytest-args: ${{ steps.config.outputs.pytest-args }}
17
+ coverage-enabled: ${{ steps.config.outputs.coverage-enabled }}
18
+ exclude-paths: ${{ steps.config.outputs.exclude-paths }}
19
+ test-on-windows: ${{ steps.config.outputs.test-on-windows }}
20
+ build-sdist: ${{ steps.config.outputs.build-sdist }}
21
+ build-wheel: ${{ steps.config.outputs.build-wheel }}
22
+ metrics-enabled: ${{ steps.config.outputs.metrics-enabled }}
23
+ metrics-config-path: ${{ steps.config.outputs.metrics-config-path }}
24
+ metrics-storage-branch: ${{ steps.config.outputs.metrics-storage-branch }}
25
+ metrics-python-version: ${{ steps.config.outputs.metrics-python-version }}
26
+ metrics-force-run: ${{ steps.config.outputs.metrics-force-run }}
27
+
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Set up uv
32
+ uses: astral-sh/setup-uv@v5
33
+
34
+ - name: Set up Python
35
+ run: uv python install 3.11
36
+
37
+ - name: Read CI Config
38
+ id: config
39
+ uses: i2mint/wads/actions/read-ci-config@master
40
+ with:
41
+ pyproject-path: .
42
+
43
+ # Second job: Validation using the config
44
+ validation:
45
+ name: Validation
46
+ if: "!contains(github.event.head_commit.message, '[skip ci]')"
47
+ needs: setup
48
+ runs-on: ubuntu-latest
49
+ strategy:
50
+ matrix:
51
+ python-version: ${{ fromJson(needs.setup.outputs.python-versions) }}
52
+
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+
56
+ - name: Set up uv
57
+ uses: astral-sh/setup-uv@v5
58
+ with:
59
+ enable-cache: true
60
+
61
+ - name: Set up Python ${{ matrix.python-version }}
62
+ run: |
63
+ uv python install ${{ matrix.python-version }}
64
+ uv venv --python ${{ matrix.python-version }}
65
+
66
+ - name: Install System Dependencies
67
+ uses: i2mint/wads/actions/install-system-deps@master
68
+ with:
69
+ pyproject-path: .
70
+
71
+ - name: Install Dependencies
72
+ run: |
73
+ source .venv/bin/activate
74
+ uv pip install -e ".[dev]"
75
+
76
+ - name: Format Source Code
77
+ run: uvx ruff format .
78
+
79
+ - name: Lint Validation
80
+ run: uvx ruff check --output-format=github ${{ needs.setup.outputs.project-name }}
81
+
82
+ - name: Run Tests
83
+ run: |
84
+ source .venv/bin/activate
85
+ PYTEST_ARGS="${{ needs.setup.outputs.pytest-args }}"
86
+ EXCLUDE="${{ needs.setup.outputs.exclude-paths }}"
87
+ COVERAGE="${{ needs.setup.outputs.coverage-enabled }}"
88
+ ROOT_DIR="${{ needs.setup.outputs.project-name }}"
89
+
90
+ CMD="python -m pytest"
91
+
92
+ # Add coverage flags
93
+ if [ "$COVERAGE" = "true" ]; then
94
+ uv pip install pytest-cov
95
+ CMD="$CMD --cov=$ROOT_DIR --cov-report=term-missing"
96
+ fi
97
+
98
+ # Add doctest flags
99
+ CMD="$CMD --doctest-modules"
100
+ CMD="$CMD -o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL'"
101
+
102
+ # Add exclude paths
103
+ if [ -n "$EXCLUDE" ]; then
104
+ IFS=',' read -ra PATHS <<< "$EXCLUDE"
105
+ for path in "${PATHS[@]}"; do
106
+ path=$(echo "$path" | xargs)
107
+ CMD="$CMD --ignore=$path"
108
+ done
109
+ fi
110
+
111
+ # Add extra pytest args
112
+ if [ -n "$PYTEST_ARGS" ]; then
113
+ CMD="$CMD $PYTEST_ARGS"
114
+ fi
115
+
116
+ echo "Running: $CMD"
117
+ eval $CMD
118
+
119
+ - name: Track Code Metrics
120
+ if: needs.setup.outputs.metrics-enabled == 'true'
121
+ uses: i2mint/umpyre/actions/track-metrics@master
122
+ continue-on-error: true
123
+ with:
124
+ github-token: ${{ secrets.GITHUB_TOKEN }}
125
+ config-path: ${{ needs.setup.outputs.metrics-config-path }}
126
+ storage-branch: ${{ needs.setup.outputs.metrics-storage-branch }}
127
+ python-version: ${{ needs.setup.outputs.metrics-python-version }}
128
+ force-run: ${{ needs.setup.outputs.metrics-force-run }}
129
+
130
+ # Optional Windows testing (if enabled in config)
131
+ windows-validation:
132
+ name: Windows Tests
133
+ if: "!contains(github.event.head_commit.message, '[skip ci]') && needs.setup.outputs.test-on-windows == 'true'"
134
+ needs: setup
135
+ runs-on: windows-latest
136
+ continue-on-error: true
137
+
138
+ steps:
139
+ - uses: actions/checkout@v4
140
+
141
+ - name: Set up uv
142
+ uses: astral-sh/setup-uv@v5
143
+ with:
144
+ enable-cache: true
145
+
146
+ - name: Set up Python
147
+ run: |
148
+ uv python install ${{ fromJson(needs.setup.outputs.python-versions)[0] }}
149
+ uv venv
150
+
151
+ - name: Install System Dependencies
152
+ uses: i2mint/wads/actions/install-system-deps@master
153
+ with:
154
+ pyproject-path: .
155
+
156
+ - name: Install Dependencies
157
+ run: |
158
+ .venv\Scripts\activate
159
+ uv pip install -e ".[dev]"
160
+
161
+ - name: Run tests
162
+ id: test
163
+ continue-on-error: true
164
+ run: |
165
+ .venv\Scripts\activate
166
+ pytest
167
+
168
+ - name: Report test results
169
+ if: always()
170
+ run: |
171
+ if ("${{ steps.test.outcome }}" -eq "failure") {
172
+ echo "::warning::Windows tests failed but workflow continues"
173
+ echo "## ⚠️ Windows Tests Failed" >> $env:GITHUB_STEP_SUMMARY
174
+ echo "Tests failed on Windows but this is informational only." >> $env:GITHUB_STEP_SUMMARY
175
+ } else {
176
+ echo "## ✅ Windows Tests Passed" >> $env:GITHUB_STEP_SUMMARY
177
+ }
178
+
179
+ # Publishing job
180
+ publish:
181
+ name: Publish
182
+ permissions:
183
+ contents: write
184
+ if: "!contains(github.event.head_commit.message, '[skip ci]') && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')"
185
+ needs: [setup, validation]
186
+ runs-on: ubuntu-latest
187
+
188
+ steps:
189
+ - uses: actions/checkout@v4
190
+ with:
191
+ fetch-depth: 0
192
+ token: ${{ secrets.GITHUB_TOKEN }}
193
+
194
+ - name: Set up uv
195
+ uses: astral-sh/setup-uv@v5
196
+
197
+ - name: Set up Python
198
+ run: uv python install ${{ fromJson(needs.setup.outputs.python-versions)[0] }}
199
+
200
+ - name: Format Source Code
201
+ run: uvx ruff format .
202
+
203
+ - name: Update Version Number
204
+ id: version
205
+ uses: i2mint/isee/actions/bump-version-number@master
206
+
207
+ - name: Build Distribution
208
+ run: |
209
+ BUILD_ARGS=""
210
+ if [ "${{ needs.setup.outputs.build-sdist }}" = "false" ]; then
211
+ BUILD_ARGS="$BUILD_ARGS --no-sdist"
212
+ fi
213
+ if [ "${{ needs.setup.outputs.build-wheel }}" = "false" ]; then
214
+ BUILD_ARGS="$BUILD_ARGS --no-wheel"
215
+ fi
216
+ uv build $BUILD_ARGS
217
+
218
+ - name: Publish to PyPI
219
+ env:
220
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_PASSWORD }}
221
+ run: uv publish dist/*
222
+
223
+ - name: Force SSH for git remote
224
+ run: |
225
+ git remote set-url origin git@github.com:${{ github.repository }}.git
226
+
227
+ - name: Commit Changes
228
+ uses: i2mint/wads/actions/git-commit@master
229
+ with:
230
+ commit-message: "**CI** Formatted code + Updated version to ${{ env.VERSION }} [skip ci]"
231
+ ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
232
+ push: true
233
+
234
+ - name: Tag Repository
235
+ uses: i2mint/wads/actions/git-tag@master
236
+ with:
237
+ tag: ${{ env.VERSION }}
238
+ message: "Release version ${{ env.VERSION }}"
239
+ push: true
240
+
241
+ # Optional GitHub Pages
242
+ github-pages:
243
+ name: Publish GitHub Pages
244
+ permissions:
245
+ contents: write
246
+ pages: write
247
+ id-token: write
248
+ if: "!contains(github.event.head_commit.message, '[skip ci]') && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)"
249
+ needs: publish
250
+ runs-on: ubuntu-latest
251
+
252
+ steps:
253
+ - uses: i2mint/epythet/actions/publish-github-pages@master
254
+ with:
255
+ github-token: ${{ secrets.GITHUB_TOKEN }}
256
+ ignore: "tests/,scrap/,examples/"
@@ -0,0 +1,121 @@
1
+ wads_configs.json
2
+ data/wads_configs.json
3
+ wads/data/wads_configs.json
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+
11
+ .DS_Store
12
+ # C extensions
13
+ *.so
14
+
15
+ # TLS certificates
16
+ ## Ignore all PEM files anywhere
17
+ *.pem
18
+ ## Also ignore any certs directory
19
+ certs/
20
+
21
+ # Distribution / packaging
22
+ .Python
23
+ build/
24
+ develop-eggs/
25
+ dist/
26
+ downloads/
27
+ eggs/
28
+ .eggs/
29
+ lib/
30
+ lib64/
31
+ parts/
32
+ sdist/
33
+ var/
34
+ wheels/
35
+ *.egg-info/
36
+ .installed.cfg
37
+ *.egg
38
+ MANIFEST
39
+ _build
40
+
41
+ # PyInstaller
42
+ # Usually these files are written by a python script from a template
43
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
44
+ *.manifest
45
+ *.spec
46
+
47
+ # Installer logs
48
+ pip-log.txt
49
+ pip-delete-this-directory.txt
50
+
51
+ # Unit test / coverage reports
52
+ htmlcov/
53
+ .tox/
54
+ .coverage
55
+ .coverage.*
56
+ .cache
57
+ nosetests.xml
58
+ coverage.xml
59
+ *.cover
60
+ .hypothesis/
61
+ .pytest_cache/
62
+
63
+ # Translations
64
+ *.mo
65
+ *.pot
66
+
67
+ # Django stuff:
68
+ *.log
69
+ local_settings.py
70
+ db.sqlite3
71
+
72
+ # Flask stuff:
73
+ instance/
74
+ .webassets-cache
75
+
76
+ # Scrapy stuff:
77
+ .scrapy
78
+
79
+ # Sphinx documentation
80
+ docs/_build/
81
+ docs/*
82
+
83
+ # PyBuilder
84
+ target/
85
+
86
+ # Jupyter Notebook
87
+ .ipynb_checkpoints
88
+
89
+ # pyenv
90
+ .python-version
91
+
92
+ # celery beat schedule file
93
+ celerybeat-schedule
94
+
95
+ # SageMath parsed files
96
+ *.sage.py
97
+
98
+ # Environments
99
+ .env
100
+ .venv
101
+ env/
102
+ venv/
103
+ ENV/
104
+ env.bak/
105
+ venv.bak/
106
+
107
+ # Spyder project settings
108
+ .spyderproject
109
+ .spyproject
110
+
111
+ # Rope project settings
112
+ .ropeproject
113
+
114
+ # mkdocs documentation
115
+ /site
116
+
117
+ # mypy
118
+ .mypy_cache/
119
+
120
+ # PyCharm
121
+ .idea
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thor Whalen
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,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: enlace_auth
3
+ Version: 0.1.1
4
+ Summary: Authentication, sessions, admin dashboard, and per-user stores for the enlace platform
5
+ Project-URL: Homepage, https://github.com/i2mint/enlace_auth
6
+ Author: Thor Whalen
7
+ License: Apache-2.0
8
+ License-File: LICENSE
9
+ Keywords: admin,argon2,auth,enlace,fastapi,platform,session
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: argh>=0.31.0
12
+ Requires-Dist: argon2-cffi>=23
13
+ Requires-Dist: dol>=0.2
14
+ Requires-Dist: email-validator>=2.0
15
+ Requires-Dist: enlace>=0.1.0
16
+ Requires-Dist: fastapi>=0.100.0
17
+ Requires-Dist: itsdangerous>=2.1
18
+ Requires-Dist: pydantic>=2.0.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: authlib>=1.3; extra == 'dev'
21
+ Requires-Dist: httpx; extra == 'dev'
22
+ Requires-Dist: pytest; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio; extra == 'dev'
24
+ Provides-Extra: oauth
25
+ Requires-Dist: authlib>=1.3; extra == 'oauth'
26
+ Requires-Dist: httpx>=0.24.0; extra == 'oauth'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # enlace_auth
30
+
31
+ Authentication, sessions, an admin dashboard, and per-user stores for the
32
+ [enlace](https://github.com/i2mint/enlace) multi-app platform.
33
+
34
+ `enlace` itself is auth-agnostic — it composes apps and routes traffic. This
35
+ package plugs in at compose time and adds:
36
+
37
+ - `/auth/login`, `/auth/logout`, `/auth/register`, `/auth/whoami`,
38
+ `/auth/csrf`, `/auth/me/password`, `/auth/shared-login`
39
+ - `/_admin/api/*` — list/create/delete users, admin password reset, view app
40
+ policy. Gated by an admin allowlist.
41
+ - per-user data injection via `request.state.store`
42
+ - `PlatformAuthMiddleware` + `CSRFMiddleware`
43
+ - optional OAuth2 / OIDC via Authlib
44
+
45
+ ## Quick start
46
+
47
+ ```python
48
+ from enlace import build_backend, PlatformConfig
49
+ from enlace_auth import plugin as auth_plugin
50
+
51
+ config = PlatformConfig.from_toml("platform.toml")
52
+ app = build_backend(config, plugins=[auth_plugin])
53
+ ```
54
+
55
+ Or, if you serve via `uvicorn --factory enlace.compose:create_app`, set:
56
+
57
+ ```bash
58
+ export ENLACE_PLUGINS=enlace_auth:plugin
59
+ ```
60
+
61
+ ## Configuration
62
+
63
+ In `platform.toml`:
64
+
65
+ ```toml
66
+ [auth]
67
+ enabled = true
68
+ session_cookie_name = "enlace_session"
69
+ session_max_age_seconds = 86400
70
+ signing_key_env = "ENLACE_SIGNING_KEY"
71
+ secure_cookies = true
72
+
73
+ [auth.stores]
74
+ backend = "file"
75
+ path = "~/.enlace/platform_store"
76
+
77
+ [stores.user_data]
78
+ backend = "file"
79
+ path = "~/.enlace/user_data"
80
+ ```
81
+
82
+ Plus environment variables:
83
+
84
+ - `ENLACE_SIGNING_KEY` — signing key (32+ chars). Generate with `python -c
85
+ "import secrets; print(secrets.token_urlsafe(32))"`.
86
+ - `ENLACE_ADMIN_EMAILS` — comma-separated admin emails (gate `/_admin`).
87
+ - `ENLACE_ALLOW_UNSIGNED=1` — opt-out from fail-fast (diagnostics only).
88
+
89
+ ## Doctor checks
90
+
91
+ ```python
92
+ from enlace.doctor import run_doctor
93
+ from enlace_auth.diagnostics import static_checks, http_checks
94
+
95
+ report = run_doctor(
96
+ config,
97
+ base_url="http://localhost:8000",
98
+ extra_static_checks=static_checks,
99
+ extra_http_checks=http_checks,
100
+ )
101
+ ```
102
+
103
+ ## Status
104
+
105
+ Extracted from `enlace` 0.0.11. The Python API is stable; an admin frontend
106
+ ships separately as a normal enlaced app.
@@ -0,0 +1,78 @@
1
+ # enlace_auth
2
+
3
+ Authentication, sessions, an admin dashboard, and per-user stores for the
4
+ [enlace](https://github.com/i2mint/enlace) multi-app platform.
5
+
6
+ `enlace` itself is auth-agnostic — it composes apps and routes traffic. This
7
+ package plugs in at compose time and adds:
8
+
9
+ - `/auth/login`, `/auth/logout`, `/auth/register`, `/auth/whoami`,
10
+ `/auth/csrf`, `/auth/me/password`, `/auth/shared-login`
11
+ - `/_admin/api/*` — list/create/delete users, admin password reset, view app
12
+ policy. Gated by an admin allowlist.
13
+ - per-user data injection via `request.state.store`
14
+ - `PlatformAuthMiddleware` + `CSRFMiddleware`
15
+ - optional OAuth2 / OIDC via Authlib
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ from enlace import build_backend, PlatformConfig
21
+ from enlace_auth import plugin as auth_plugin
22
+
23
+ config = PlatformConfig.from_toml("platform.toml")
24
+ app = build_backend(config, plugins=[auth_plugin])
25
+ ```
26
+
27
+ Or, if you serve via `uvicorn --factory enlace.compose:create_app`, set:
28
+
29
+ ```bash
30
+ export ENLACE_PLUGINS=enlace_auth:plugin
31
+ ```
32
+
33
+ ## Configuration
34
+
35
+ In `platform.toml`:
36
+
37
+ ```toml
38
+ [auth]
39
+ enabled = true
40
+ session_cookie_name = "enlace_session"
41
+ session_max_age_seconds = 86400
42
+ signing_key_env = "ENLACE_SIGNING_KEY"
43
+ secure_cookies = true
44
+
45
+ [auth.stores]
46
+ backend = "file"
47
+ path = "~/.enlace/platform_store"
48
+
49
+ [stores.user_data]
50
+ backend = "file"
51
+ path = "~/.enlace/user_data"
52
+ ```
53
+
54
+ Plus environment variables:
55
+
56
+ - `ENLACE_SIGNING_KEY` — signing key (32+ chars). Generate with `python -c
57
+ "import secrets; print(secrets.token_urlsafe(32))"`.
58
+ - `ENLACE_ADMIN_EMAILS` — comma-separated admin emails (gate `/_admin`).
59
+ - `ENLACE_ALLOW_UNSIGNED=1` — opt-out from fail-fast (diagnostics only).
60
+
61
+ ## Doctor checks
62
+
63
+ ```python
64
+ from enlace.doctor import run_doctor
65
+ from enlace_auth.diagnostics import static_checks, http_checks
66
+
67
+ report = run_doctor(
68
+ config,
69
+ base_url="http://localhost:8000",
70
+ extra_static_checks=static_checks,
71
+ extra_http_checks=http_checks,
72
+ )
73
+ ```
74
+
75
+ ## Status
76
+
77
+ Extracted from `enlace` 0.0.11. The Python API is stable; an admin frontend
78
+ ships separately as a normal enlaced app.
@@ -0,0 +1,40 @@
1
+ """enlace_auth — authentication, sessions, admin dashboard, and per-user stores.
2
+
3
+ Plug into ``enlace`` at compose time::
4
+
5
+ from enlace import build_backend, PlatformConfig
6
+ from enlace_auth import plugin as auth_plugin
7
+
8
+ config = PlatformConfig.from_toml()
9
+ app = build_backend(config, plugins=[auth_plugin])
10
+
11
+ When ``config.auth.enabled`` is True the plugin mounts:
12
+
13
+ - ``/auth/*`` — login, logout, register, whoami, csrf, me/password
14
+ - ``/_admin/api/*`` — admin user/app management (gated by admin allowlist)
15
+ - ``/api/{app}/store/*`` — per-user data store
16
+ - middleware: PlatformAuthMiddleware, CSRFMiddleware, StoreInjectionMiddleware
17
+
18
+ When it's False the plugin is a no-op, so installing this package never
19
+ changes platform behavior unless the operator opts in.
20
+ """
21
+
22
+ from enlace_auth.config import (
23
+ AccessLevel,
24
+ AuthConfig,
25
+ OAuthProviderConfig,
26
+ StoreBackendConfig,
27
+ )
28
+ from enlace_auth.plugin import EnlaceAuthConfigError, plugin, wire
29
+
30
+ __version__ = "0.0.1"
31
+
32
+ __all__ = [
33
+ "AccessLevel",
34
+ "AuthConfig",
35
+ "EnlaceAuthConfigError",
36
+ "OAuthProviderConfig",
37
+ "StoreBackendConfig",
38
+ "plugin",
39
+ "wire",
40
+ ]