pyxle-auth 0.2.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.
Files changed (34) hide show
  1. pyxle_auth-0.2.0/.gitignore +37 -0
  2. pyxle_auth-0.2.0/CHANGELOG.md +96 -0
  3. pyxle_auth-0.2.0/LICENSE +21 -0
  4. pyxle_auth-0.2.0/PKG-INFO +298 -0
  5. pyxle_auth-0.2.0/README.md +268 -0
  6. pyxle_auth-0.2.0/pyproject.toml +52 -0
  7. pyxle_auth-0.2.0/pyxle_auth/__init__.py +134 -0
  8. pyxle_auth-0.2.0/pyxle_auth/_ddl.py +49 -0
  9. pyxle_auth-0.2.0/pyxle_auth/api_tokens.py +301 -0
  10. pyxle_auth-0.2.0/pyxle_auth/errors.py +67 -0
  11. pyxle_auth-0.2.0/pyxle_auth/guards.py +199 -0
  12. pyxle_auth-0.2.0/pyxle_auth/migrations/0001-pyxle-auth-core.mysql.sql +82 -0
  13. pyxle_auth-0.2.0/pyxle_auth/migrations/0001-pyxle-auth-core.sql +86 -0
  14. pyxle_auth-0.2.0/pyxle_auth/models.py +122 -0
  15. pyxle_auth-0.2.0/pyxle_auth/plugin.py +234 -0
  16. pyxle_auth-0.2.0/pyxle_auth/py.typed +0 -0
  17. pyxle_auth-0.2.0/pyxle_auth/ratelimit.py +179 -0
  18. pyxle_auth-0.2.0/pyxle_auth/rbac.py +317 -0
  19. pyxle_auth-0.2.0/pyxle_auth/service.py +797 -0
  20. pyxle_auth-0.2.0/pyxle_auth/settings.py +218 -0
  21. pyxle_auth-0.2.0/pyxle_auth/tokens.py +189 -0
  22. pyxle_auth-0.2.0/tests/__init__.py +0 -0
  23. pyxle_auth-0.2.0/tests/conftest.py +31 -0
  24. pyxle_auth-0.2.0/tests/test_api_tokens.py +356 -0
  25. pyxle_auth-0.2.0/tests/test_database_contract.py +181 -0
  26. pyxle_auth-0.2.0/tests/test_guards.py +418 -0
  27. pyxle_auth-0.2.0/tests/test_live_backends.py +149 -0
  28. pyxle_auth-0.2.0/tests/test_plugin.py +268 -0
  29. pyxle_auth-0.2.0/tests/test_ratelimit.py +99 -0
  30. pyxle_auth-0.2.0/tests/test_rbac.py +325 -0
  31. pyxle_auth-0.2.0/tests/test_security_fixes.py +131 -0
  32. pyxle_auth-0.2.0/tests/test_service.py +565 -0
  33. pyxle_auth-0.2.0/tests/test_settings.py +114 -0
  34. pyxle_auth-0.2.0/tests/test_tokens.py +323 -0
@@ -0,0 +1,37 @@
1
+ # Python bytecode
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ .eggs/
12
+
13
+ # Virtual environments
14
+ venv/
15
+ .venv/
16
+
17
+ # Test / coverage
18
+ htmlcov/
19
+ .coverage
20
+ .coverage.*
21
+ .pytest_cache/
22
+ coverage.xml
23
+
24
+ # Node
25
+ node_modules/
26
+
27
+ # IDE
28
+ .idea/
29
+ .vscode/
30
+ *.swp
31
+
32
+ # OS
33
+ .DS_Store
34
+ Thumbs.db
35
+
36
+ # Tooling caches
37
+ .ruff_cache/
@@ -0,0 +1,96 @@
1
+ # Changelog
2
+
3
+ All notable changes to `pyxle-auth` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-06-11
9
+
10
+ ### Changed (BREAKING)
11
+
12
+ - Requires `pyxle-db>=0.2.0`. Its transaction methods became
13
+ coroutines (`await tx.execute(...)`), and all pyxle-auth SQL is now
14
+ written in portable qmark style — the plugin works unchanged on
15
+ SQLite, PostgreSQL, and MySQL (DML fully portable; shipped DDL targets
16
+ SQLite/PostgreSQL — MySQL schema needs a dialect override, see README).
17
+ Upgraders from 0.1: the `ratelimit_buckets.key` column is now
18
+ `bucket_key` (KEY is reserved in MySQL); drop the old table — bucket
19
+ data is ephemeral hourly counters and recreates itself.
20
+ - The plugin now hard-requires the `pyxle-db` plugin to have run
21
+ first. List `"pyxle-db"` before `"pyxle-auth"` in
22
+ `pyxle.config.json::plugins`; startup aborts with an actionable
23
+ error otherwise.
24
+ - The `ensureSchema` plugin setting is removed. The plugin always
25
+ applies its bundled migrations and then runs each service's
26
+ idempotent `ensure_schema()` — both are no-ops on an up-to-date
27
+ database, so there is nothing left to opt out of.
28
+
29
+ ### Added
30
+
31
+ - Password reset and email verification flows, powered by
32
+ `TokenService`: single-use, purpose-scoped, expiring tokens with
33
+ only the SHA-256 stored at rest. The library never sends email —
34
+ your app delivers the token through its own mailer.
35
+ - `RoleService` (RBAC): roles, permissions, and per-user grants,
36
+ registered as `auth.rbac`.
37
+ - `ApiTokenService`: long-lived `pyxle_pat_` personal access tokens
38
+ with scopes, per-user caps enforced atomically, and revocation.
39
+ Registered as `auth.api_tokens`.
40
+ - Request guards: `current_user`, `require_user_page`,
41
+ `require_user_action`, `require_permission_page`,
42
+ `require_permission_action`, and `bearer_token`, re-exported from
43
+ the package root.
44
+ - New settings: `password_reset_ttl_seconds` (default 1800),
45
+ `email_verify_ttl_seconds` (default 86400), and
46
+ `rate_limit_password_reset_per_hour` (default 3), each with a
47
+ `PYXLE_AUTH_*` environment variable and a camelCase plugin key.
48
+ - Settings precedence: plugin `settings` in `pyxle.config.json`
49
+ override `PYXLE_AUTH_*` environment variables, which override the
50
+ built-in defaults. `AuthSettings.from_env` grew an `overrides`
51
+ parameter to express this.
52
+ - Bundled migrations (`pyxle_auth/migrations`) applied through
53
+ `pyxle_db.Migrator` at startup, with `ensure_schema()` as
54
+ belt-and-braces after.
55
+ - New exports: `SessionInfo`, `InvalidToken`, `TokenClaim`,
56
+ `TokenService`, `ApiToken`, `ApiTokenService`, `TokenLimitReached`,
57
+ `TOKEN_PREFIX`, and `RoleService`.
58
+ - Live-backend test suite (`tests/test_live_backends.py`) running the
59
+ real plugin schema path and a full account lifecycle against
60
+ PostgreSQL and MySQL (gated on `PYXLE_DB_TEST_POSTGRES_URL` /
61
+ `PYXLE_DB_TEST_MYSQL_URL`, shared with pyxle-db's suites).
62
+ - `PYXLE_AUTH_STRICT` environment variable: `strict` now resolves
63
+ config > env > secure-default(True), so a committed config can stay
64
+ production-safe (strict + Secure cookies) while local HTTP dev
65
+ relaxes via the environment.
66
+ - **Bring your own database.** Services and the plugin now bind to the
67
+ `pyxle_db.DatabaseLike` protocol instead of the concrete `Database`
68
+ class, and the plugin's requirement is the `db.database` service
69
+ *name* — any plugin registering a protocol-satisfying object can back
70
+ pyxle-auth (pyxle-db remains the reference provider and a hard
71
+ dependency for the error types and migrator). The contract (surface,
72
+ `IntegrityError` translation, dialect, datetimes) is documented in the
73
+ README and enforced by `tests/test_database_contract.py`, which runs
74
+ the full lifecycle against a deliberately foreign database object.
75
+
76
+ ### Fixed
77
+
78
+ - **Schema is now genuinely portable** (found by the live-server
79
+ suites). Key and indexed columns are `VARCHAR(n)` instead of `TEXT`
80
+ (MySQL cannot index bare `TEXT`), a `0001-pyxle-auth-core.mysql.sql`
81
+ override uses `DATETIME(6)` (MySQL `TIMESTAMP` is 2038-capped,
82
+ second-rounded, and session-time-zone converted), and
83
+ `ensure_schema()` creates indexes through an `information_schema`
84
+ probe on MySQL, which has no `CREATE INDEX IF NOT EXISTS`.
85
+ - Dependency floor corrected to `pyxle-framework>=0.4.0` — the
86
+ `pyxle.plugins` API first shipped in 0.4.0.
87
+
88
+ ## [0.1.0] - 2026-04-23
89
+
90
+ ### Added
91
+
92
+ - Initial release: email+password `AuthService` (argon2id hashing,
93
+ sliding sessions with an absolute cap, SHA-256 token storage,
94
+ enumeration-resistant errors, fixed-window rate limits), the
95
+ `pyxle-auth` plugin registering `auth.service`/`auth.settings`, and
96
+ `AuthSettings` loadable from the environment.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pyxle
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,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyxle-auth
3
+ Version: 0.2.0
4
+ Summary: Authentication plugin for Pyxle: argon2id sessions, password reset and email verification flows, RBAC, scoped API tokens, and request guards.
5
+ Project-URL: Homepage, https://pyxle.dev
6
+ Project-URL: Source, https://github.com/pyxle-dev/pyxle-plugins
7
+ Project-URL: Changelog, https://github.com/pyxle-dev/pyxle-plugins/blob/main/packages/pyxle-auth/CHANGELOG.md
8
+ Author-email: Pyxle <dev@pyxle.dev>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: argon2,auth,pyxle,rbac,sessions,tokens
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Security
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: argon2-cffi>=23.1
24
+ Requires-Dist: pyxle-db>=0.2.0
25
+ Requires-Dist: pyxle-framework>=0.4.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # pyxle-auth
32
+
33
+ Django-grade authentication for [Pyxle](https://pyxle.dev) apps:
34
+ sessions, password reset and email verification flows, roles and
35
+ permissions, API tokens, and one-line request guards. Built on
36
+ [pyxle-db](https://github.com/pyxle-dev/pyxle-plugins/tree/main/packages/pyxle-db),
37
+ so the same code runs on SQLite, PostgreSQL, and MySQL. (Caveat: every query is portable across all three, but the *shipped schema files* target SQLite and PostgreSQL; MySQL needs a dialect-override migration — `0001-pyxle-auth-core.mysql.sql` — because MySQL requires key lengths on TEXT keys. On the roadmap; contributions welcome.)
38
+
39
+ - **Sessions** — argon2id-hashed passwords, server-side sessions with
40
+ sliding expiry and an absolute cap, `HttpOnly; Secure; SameSite=Lax`
41
+ cookies.
42
+ - **Password reset & email verification** — single-use, purpose-scoped,
43
+ expiring tokens. The library never sends email; your app delivers the
44
+ link through its own mailer.
45
+ - **RBAC** — roles, permissions, per-user grants, and
46
+ `require_permission_*` guards.
47
+ - **API tokens** — long-lived `pyxle_pat_` personal access tokens with
48
+ scopes, per-user caps, and revocation, for CLIs and CI.
49
+ - **Guards** — `require_user_page(request)` and friends protect a
50
+ loader or action in one line.
51
+ - **Rate limits** — database-backed fixed-window buckets on sign-in,
52
+ sign-up, and reset requests; they survive process restarts.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install pyxle-auth
58
+ ```
59
+
60
+ ## Quickstart
61
+
62
+ List `pyxle-db` **before** `pyxle-auth` in `pyxle.config.json` — the
63
+ auth services run on the database that plugin opens:
64
+
65
+ ```json
66
+ {
67
+ "plugins": [
68
+ "pyxle-db",
69
+ "pyxle-auth"
70
+ ]
71
+ }
72
+ ```
73
+
74
+ That's the whole wire-up. At startup the plugin applies its bundled
75
+ migrations (idempotent, checksum-tracked) and registers the services
76
+ listed [below](#plugin-services).
77
+
78
+ Protect a page with a guard in its `@server` loader:
79
+
80
+ ```python
81
+ # pages/dashboard.pyxl — Python section
82
+ from pyxle.runtime import server
83
+ from pyxle_auth import require_user_page
84
+
85
+
86
+ @server
87
+ async def load(request):
88
+ user = await require_user_page(request) # 401 → error boundary when signed out
89
+ return {"email": user.email}
90
+ ```
91
+
92
+ Sign-in needs to put a `Set-Cookie` header on the response, so it lives
93
+ in an [API route](https://pyxle.dev/docs) (actions return plain JSON
94
+ payloads and can't attach cookies):
95
+
96
+ ```python
97
+ # pages/api/sign_in.py
98
+ from starlette.requests import Request
99
+ from starlette.responses import JSONResponse
100
+
101
+ from pyxle_auth import AuthError, RateLimited, get_auth_service
102
+
103
+
104
+ async def endpoint(request: Request) -> JSONResponse:
105
+ body = await request.json()
106
+ auth = get_auth_service()
107
+ try:
108
+ user, cookie = await auth.sign_in(
109
+ email=body["email"],
110
+ password=body["password"],
111
+ ip=request.client.host,
112
+ user_agent=request.headers.get("user-agent", ""),
113
+ )
114
+ except RateLimited as exc:
115
+ return JSONResponse(
116
+ {"ok": False, "error": str(exc)},
117
+ status_code=429,
118
+ headers={"Retry-After": str(exc.retry_after_seconds)},
119
+ )
120
+ except AuthError as exc:
121
+ # InvalidCredentials and friends share one deliberately vague
122
+ # message — don't replace it with something more "helpful".
123
+ return JSONResponse({"ok": False, "error": str(exc)}, status_code=401)
124
+
125
+ response = JSONResponse({"ok": True, "userId": user.id})
126
+ response.set_cookie(**cookie.kwargs())
127
+ return response
128
+ ```
129
+
130
+ `sign_up` has the same shape. `sign_out(cookie_value=...)` returns a
131
+ cookie that clears the browser's copy — set it the same way.
132
+
133
+ ## Bring your own mailer
134
+
135
+ pyxle-auth never sends email. Flows that need delivery return a raw,
136
+ single-use token exactly once; your app puts it in a link and hands it
137
+ to whatever mailer it already uses:
138
+
139
+ ```python
140
+ # pages/api/forgot_password.py
141
+ async def endpoint(request: Request) -> JSONResponse:
142
+ body = await request.json()
143
+ auth = get_auth_service()
144
+ result = await auth.request_password_reset(
145
+ email=body["email"], ip=request.client.host
146
+ )
147
+ if result is not None:
148
+ user, token = result
149
+ await my_mailer.send(
150
+ to=user.email,
151
+ subject="Reset your password",
152
+ body=f"https://example.com/reset?token={token}",
153
+ )
154
+ # Same response whether the account exists or not — this endpoint
155
+ # must not be usable to probe for accounts.
156
+ return JSONResponse({"ok": True, "message": "Check your inbox."})
157
+ ```
158
+
159
+ The user completes the flow with
160
+ `await auth.reset_password(raw_token=token, new_password=...)`, which
161
+ burns the token and revokes every session. Email verification mirrors
162
+ the pattern: `request_email_verification(user_id=...)` returns a token,
163
+ `confirm_email(raw_token=...)` redeems it. Both raise `InvalidToken`
164
+ for anything stale, used, unknown, or wrong-purpose —
165
+ indistinguishably.
166
+
167
+ For your own flows (invite links, magic links), the same machinery is
168
+ registered as `auth.tokens`: issue with a custom `purpose`, consume it
169
+ once, never store the raw value.
170
+
171
+ ## Bring your own database
172
+
173
+ pyxle-auth binds to the **`db.database` plugin service**, not to the
174
+ pyxle-db package. The reference provider is pyxle-db, but any plugin (or
175
+ test fixture) that registers an object satisfying
176
+ `pyxle_db.DatabaseLike` works — an adapter over SQLAlchemy's async
177
+ engine, a bespoke driver wrapper, an in-memory fake.
178
+
179
+ The full contract a replacement must honour:
180
+
181
+ 1. **Surface** — the five members of `pyxle_db.DatabaseLike`:
182
+ `execute`, `fetchone`, `fetchall`, an async-context-manager
183
+ `transaction()` (yielding the same query surface), and a `dialect`
184
+ property returning a `pyxle_db.Dialect`. SQL arrives in canonical
185
+ qmark style (`?` placeholders); rows go back as `pyxle_db.Row`.
186
+ 2. **Errors** — unique-constraint violations must raise
187
+ `pyxle_db.IntegrityError`. pyxle-auth converts it into domain
188
+ behaviour (`AccountExists` on duplicate sign-up, idempotent role
189
+ grants); raise your driver's own error type and those paths break.
190
+ 3. **Dialect** — `dialect.name` drives portable DDL. `sqlite`,
191
+ `postgresql`, and `mysql` have live-tested paths; any other name
192
+ falls back to the SQLite/PostgreSQL-flavoured DDL (right for
193
+ PostgreSQL-compatible engines, wrong for e.g. MSSQL).
194
+ 4. **Datetimes** — reads return timezone-aware UTC; binds accept naive
195
+ (assumed UTC) or aware (converted) datetimes.
196
+
197
+ `tests/test_database_contract.py` runs the entire auth lifecycle against
198
+ a wrapper that exposes *only* this surface — it is both the executable
199
+ specification and a template for writing your own adapter.
200
+
201
+ ## Security properties
202
+
203
+ - **Password hashing** — argon2id, `t=3, m=64 MiB, p=2` by default
204
+ (~300 ms on a 2020-era laptop), tunable via settings. Hashes are
205
+ transparently upgraded on sign-in when parameters change.
206
+ - **Nothing secret at rest** — session cookies, reset/verification
207
+ tokens, and API tokens all store only the SHA-256 of the secret. A
208
+ leaked database cannot resurrect a session or replay a reset link.
209
+ - **Enumeration resistance** — sign-in failures share one message and
210
+ run a dummy argon2 verify on unknown emails so timing stays flat;
211
+ password-reset requests do token-shaped work and return the same
212
+ shape whether the account exists or not; token redemption never says
213
+ *why* it failed.
214
+ - **Single-use tokens** — redemption burns the token atomically, so two
215
+ racing requests can't both succeed, and requesting a new reset link
216
+ invalidates earlier unused ones.
217
+ - **Rate limits** — sign-in is capped per IP *and* per email (10/hour
218
+ each), sign-up per IP (5/hour), reset requests per email and per IP
219
+ (3/hour). Buckets live in the database and survive restarts.
220
+ - **Session lifecycle** — sliding expiry (30 days) under an absolute
221
+ cap (90 days); password change and password reset revoke every
222
+ session; `list_sessions`/`revoke_session` power a "your devices"
223
+ screen.
224
+ - **Cookie posture** — `HttpOnly`, `Secure`, `SameSite=Lax` by default.
225
+ Strict mode (the default) refuses to start with `cookie_secure=False`.
226
+
227
+ ## Plugin services
228
+
229
+ | Service | Type | Use it for |
230
+ |---|---|---|
231
+ | `auth.service` | `AuthService` | Sign-up/in/out, sessions, password change/reset, email verification |
232
+ | `auth.rbac` | `RoleService` | Define roles, grant them, check permissions |
233
+ | `auth.tokens` | `TokenService` | Custom single-use token flows (invites, magic links) |
234
+ | `auth.api_tokens` | `ApiTokenService` | `pyxle_pat_` personal access tokens |
235
+ | `auth.settings` | `AuthSettings` | The resolved configuration (cookie name, TTLs, …) |
236
+
237
+ Reach them with `ctx.require(...)`, `pyxle.plugins.plugin(...)`, or the
238
+ typed helpers `get_auth_service()` / `get_auth_settings()`.
239
+
240
+ Guards resolve `auth.service` / `auth.rbac` automatically; pass
241
+ `service=` / `rbac=` explicitly in tests. For API routes authenticating
242
+ with personal access tokens, pair `bearer_token(request)` with
243
+ `ApiTokenService.resolve(raw_token=..., required_scope=...)`.
244
+
245
+ ## Settings
246
+
247
+ Precedence: plugin `settings` in `pyxle.config.json` **>**
248
+ `PYXLE_AUTH_*` environment variables **>** defaults.
249
+
250
+ | Config key | Environment variable | Default | Meaning |
251
+ |---|---|---|---|
252
+ | `argonTimeCost` | `PYXLE_AUTH_ARGON_T` | `3` | Argon2 time cost |
253
+ | `argonMemoryKib` | `PYXLE_AUTH_ARGON_M` | `65536` | Argon2 memory (KiB) |
254
+ | `argonParallelism` | `PYXLE_AUTH_ARGON_P` | `2` | Argon2 parallelism |
255
+ | `passwordMinLength` | `PYXLE_AUTH_PW_MIN` | `8` | Reject shorter passwords |
256
+ | `passwordMaxLength` | — | `1024` | Reject pathological inputs |
257
+ | `sessionTtlSeconds` | `PYXLE_AUTH_SESSION_TTL` | `2592000` (30 d) | Sliding session lifetime |
258
+ | `sessionAbsoluteMaxSeconds` | `PYXLE_AUTH_SESSION_ABS_MAX` | `7776000` (90 d) | Hard cap from creation |
259
+ | `cookieName` | `PYXLE_AUTH_COOKIE_NAME` | `pyxle_session` | Session cookie name |
260
+ | `cookieSecure` | `PYXLE_AUTH_COOKIE_SECURE` | `true` | `Secure` cookie flag |
261
+ | `cookieSameSite` | `PYXLE_AUTH_COOKIE_SAMESITE` | `Lax` | `Lax` / `Strict` / `None` |
262
+ | `cookieDomain` | `PYXLE_AUTH_COOKIE_DOMAIN` | unset | Share across subdomains |
263
+ | `cookiePath` | — | `/` | Cookie path |
264
+ | `passwordResetTtlSeconds` | `PYXLE_AUTH_PASSWORD_RESET_TTL_SECONDS` | `1800` (30 min) | Reset-token lifetime |
265
+ | `emailVerifyTtlSeconds` | `PYXLE_AUTH_EMAIL_VERIFY_TTL_SECONDS` | `86400` (24 h) | Verify-token lifetime |
266
+ | `rateLimitSignInPerHour` | `PYXLE_AUTH_RL_SIGN_IN_PER_HOUR` | `10` | Per IP and per email |
267
+ | `rateLimitSignUpPerHour` | `PYXLE_AUTH_RL_SIGN_UP_PER_HOUR` | `5` | Per IP |
268
+ | `rateLimitPasswordResetPerHour` | `PYXLE_AUTH_RATE_LIMIT_PASSWORD_RESET_PER_HOUR` | `3` | Per email and per IP |
269
+ | `requireEmailVerified` | `PYXLE_AUTH_REQUIRE_VERIFIED` | `false` | Gate sign-in on verification |
270
+ | `strict` | — | `true` | Enforce `cookieSecure=true`; set `false` for HTTP dev servers |
271
+
272
+ Outside the plugin, load the same configuration with
273
+ `AuthSettings.from_env()`, and use `AuthSettings(...).for_tests()` in
274
+ test suites — it drops argon costs and TTLs so suites stay fast.
275
+
276
+ ## Schema
277
+
278
+ The plugin owns its tables (`users`, `sessions`, `auth_tokens`,
279
+ `api_tokens`, `roles`, `user_roles`, `ratelimit_buckets`): bundled
280
+ migrations are applied through `pyxle_db.Migrator` at startup, followed
281
+ by each service's idempotent `ensure_schema()`. Repeated startups are
282
+ no-ops. The SQL is portable qmark style throughout, so the plugin works
283
+ on every pyxle-db backend without per-database configuration.
284
+
285
+ ## Roadmap
286
+
287
+ Honest status — these are **not implemented yet**:
288
+
289
+ - OAuth / OIDC sign-in (Google, GitHub, generic OIDC)
290
+ - Multi-factor authentication (TOTP, WebAuthn)
291
+
292
+ If you need them today, the building blocks (sessions, `TokenService`,
293
+ guards) compose underneath whatever you bring; contributions are
294
+ welcome.
295
+
296
+ ## License
297
+
298
+ MIT.