regstack 0.2.0__tar.gz → 0.2.2__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 (176) hide show
  1. regstack-0.2.2/.github/workflows/test.yml +147 -0
  2. {regstack-0.2.0 → regstack-0.2.2}/CHANGELOG.md +21 -0
  3. {regstack-0.2.0 → regstack-0.2.2}/PKG-INFO +9 -11
  4. {regstack-0.2.0 → regstack-0.2.2}/README.md +8 -10
  5. {regstack-0.2.0 → regstack-0.2.2}/docs/architecture.md +35 -44
  6. {regstack-0.2.0 → regstack-0.2.2}/docs/changelog.md +55 -0
  7. {regstack-0.2.0 → regstack-0.2.2}/docs/embedding.md +10 -13
  8. regstack-0.2.2/docs/index.md +141 -0
  9. {regstack-0.2.0 → regstack-0.2.2}/docs/quickstart.md +11 -11
  10. {regstack-0.2.0 → regstack-0.2.2}/docs/security.md +49 -68
  11. {regstack-0.2.0 → regstack-0.2.2}/pyproject.toml +1 -1
  12. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/auth/dependencies.py +3 -4
  13. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/auth/lockout.py +2 -2
  14. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +2 -14
  15. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/repositories/pending_repo.py +2 -2
  16. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/repositories/user_repo.py +2 -2
  17. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/protocols.py +24 -6
  18. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/repositories/mfa_code_repo.py +1 -4
  19. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/models/_objectid.py +10 -2
  20. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/account.py +1 -1
  21. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/login.py +1 -1
  22. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/phone.py +1 -1
  23. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/register.py +1 -2
  24. regstack-0.2.2/src/regstack/version.py +1 -0
  25. regstack-0.2.2/tests/unit/test_base_install_imports.py +69 -0
  26. {regstack-0.2.0 → regstack-0.2.2}/uv.lock +1 -1
  27. regstack-0.2.0/.github/workflows/test.yml +0 -86
  28. regstack-0.2.0/docs/index.md +0 -99
  29. regstack-0.2.0/src/regstack/version.py +0 -1
  30. {regstack-0.2.0 → regstack-0.2.2}/.github/workflows/publish.yml +0 -0
  31. {regstack-0.2.0 → regstack-0.2.2}/.gitignore +0 -0
  32. {regstack-0.2.0 → regstack-0.2.2}/.python-version +0 -0
  33. {regstack-0.2.0 → regstack-0.2.2}/.readthedocs.yaml +0 -0
  34. {regstack-0.2.0 → regstack-0.2.2}/CLAUDE.md +0 -0
  35. {regstack-0.2.0 → regstack-0.2.2}/LICENSE +0 -0
  36. {regstack-0.2.0 → regstack-0.2.2}/NOTICE +0 -0
  37. {regstack-0.2.0 → regstack-0.2.2}/SECURITY.md +0 -0
  38. {regstack-0.2.0 → regstack-0.2.2}/docs/_static/.gitkeep +0 -0
  39. {regstack-0.2.0 → regstack-0.2.2}/docs/_templates/.gitkeep +0 -0
  40. {regstack-0.2.0 → regstack-0.2.2}/docs/api.md +0 -0
  41. {regstack-0.2.0 → regstack-0.2.2}/docs/cli.md +0 -0
  42. {regstack-0.2.0 → regstack-0.2.2}/docs/conf.py +0 -0
  43. {regstack-0.2.0 → regstack-0.2.2}/docs/configuration.md +0 -0
  44. {regstack-0.2.0 → regstack-0.2.2}/docs/theming.md +0 -0
  45. {regstack-0.2.0 → regstack-0.2.2}/examples/_common/__init__.py +0 -0
  46. {regstack-0.2.0 → regstack-0.2.2}/examples/_common/app.py +0 -0
  47. {regstack-0.2.0 → regstack-0.2.2}/examples/mongo/README.md +0 -0
  48. {regstack-0.2.0 → regstack-0.2.2}/examples/mongo/branding/theme.css +0 -0
  49. {regstack-0.2.0 → regstack-0.2.2}/examples/mongo/main.py +0 -0
  50. {regstack-0.2.0 → regstack-0.2.2}/examples/mongo/regstack.toml +0 -0
  51. {regstack-0.2.0 → regstack-0.2.2}/examples/postgres/README.md +0 -0
  52. {regstack-0.2.0 → regstack-0.2.2}/examples/postgres/main.py +0 -0
  53. {regstack-0.2.0 → regstack-0.2.2}/examples/postgres/regstack.toml +0 -0
  54. {regstack-0.2.0 → regstack-0.2.2}/examples/sqlite/README.md +0 -0
  55. {regstack-0.2.0 → regstack-0.2.2}/examples/sqlite/main.py +0 -0
  56. {regstack-0.2.0 → regstack-0.2.2}/examples/sqlite/regstack.toml +0 -0
  57. {regstack-0.2.0 → regstack-0.2.2}/regstack.toml.example +0 -0
  58. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/__init__.py +0 -0
  59. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/app.py +0 -0
  60. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/auth/__init__.py +0 -0
  61. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/auth/clock.py +0 -0
  62. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/auth/jwt.py +0 -0
  63. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/auth/mfa.py +0 -0
  64. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/auth/password.py +0 -0
  65. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/auth/tokens.py +0 -0
  66. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/__init__.py +0 -0
  67. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/base.py +0 -0
  68. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/factory.py +0 -0
  69. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/__init__.py +0 -0
  70. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/backend.py +0 -0
  71. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/client.py +0 -0
  72. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/indexes.py +0 -0
  73. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  74. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
  75. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  76. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/__init__.py +0 -0
  77. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/backend.py +0 -0
  78. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  79. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/migrations/env.py +0 -0
  80. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  81. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  82. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  83. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  84. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  85. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  86. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  87. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/schema.py +0 -0
  88. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/backends/sql/types.py +0 -0
  89. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/cli/__init__.py +0 -0
  90. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/cli/__main__.py +0 -0
  91. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/cli/_runtime.py +0 -0
  92. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/cli/admin.py +0 -0
  93. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/cli/doctor.py +0 -0
  94. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/cli/init.py +0 -0
  95. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/cli/migrate.py +0 -0
  96. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/config/__init__.py +0 -0
  97. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/config/loader.py +0 -0
  98. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/config/schema.py +0 -0
  99. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/config/secrets.py +0 -0
  100. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/__init__.py +0 -0
  101. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/base.py +0 -0
  102. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/composer.py +0 -0
  103. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/console.py +0 -0
  104. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/factory.py +0 -0
  105. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/ses.py +0 -0
  106. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/smtp.py +0 -0
  107. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/email_change.html +0 -0
  108. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/email_change.subject.txt +0 -0
  109. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/email_change.txt +0 -0
  110. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/password_reset.html +0 -0
  111. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  112. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/password_reset.txt +0 -0
  113. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  114. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  115. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/verification.html +0 -0
  116. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/verification.subject.txt +0 -0
  117. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/email/templates/verification.txt +0 -0
  118. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/hooks/__init__.py +0 -0
  119. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/hooks/events.py +0 -0
  120. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/models/__init__.py +0 -0
  121. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/models/login_attempt.py +0 -0
  122. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/models/mfa_code.py +0 -0
  123. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/models/pending_registration.py +0 -0
  124. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/models/user.py +0 -0
  125. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/__init__.py +0 -0
  126. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/_schemas.py +0 -0
  127. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/admin.py +0 -0
  128. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/logout.py +0 -0
  129. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/password.py +0 -0
  130. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/routers/verify.py +0 -0
  131. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/sms/__init__.py +0 -0
  132. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/sms/base.py +0 -0
  133. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/sms/factory.py +0 -0
  134. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/sms/null.py +0 -0
  135. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/sms/sns.py +0 -0
  136. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/sms/twilio.py +0 -0
  137. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/__init__.py +0 -0
  138. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/pages.py +0 -0
  139. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/static/css/core.css +0 -0
  140. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/static/css/theme.css +0 -0
  141. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/static/js/regstack.js +0 -0
  142. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  143. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/auth/forgot.html +0 -0
  144. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/auth/login.html +0 -0
  145. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/auth/me.html +0 -0
  146. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  147. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/auth/register.html +0 -0
  148. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/auth/reset.html +0 -0
  149. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/auth/verify.html +0 -0
  150. {regstack-0.2.0 → regstack-0.2.2}/src/regstack/ui/templates/base.html +0 -0
  151. {regstack-0.2.0 → regstack-0.2.2}/tasks.py +0 -0
  152. {regstack-0.2.0 → regstack-0.2.2}/tests/__init__.py +0 -0
  153. {regstack-0.2.0 → regstack-0.2.2}/tests/conftest.py +0 -0
  154. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/__init__.py +0 -0
  155. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_account_management.py +0 -0
  156. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_admin_router.py +0 -0
  157. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_happy_path.py +0 -0
  158. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_indexes.py +0 -0
  159. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_login_lockout.py +0 -0
  160. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_mfa.py +0 -0
  161. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_password_reset.py +0 -0
  162. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_sql_migrations.py +0 -0
  163. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_ui_router.py +0 -0
  164. {regstack-0.2.0 → regstack-0.2.2}/tests/integration/test_verification.py +0 -0
  165. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/__init__.py +0 -0
  166. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_cli.py +0 -0
  167. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_config_loader.py +0 -0
  168. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_jwt.py +0 -0
  169. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_lockout.py +0 -0
  170. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_mail_composer.py +0 -0
  171. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_mfa_code_repo.py +0 -0
  172. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_password.py +0 -0
  173. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_ses_backend.py +0 -0
  174. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_sms.py +0 -0
  175. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_smtp_backend.py +0 -0
  176. {regstack-0.2.0 → regstack-0.2.2}/tests/unit/test_ui_env.py +0 -0
@@ -0,0 +1,147 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: test-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ pytest:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.11", "3.12"]
20
+ services:
21
+ mongodb:
22
+ image: mongo:7
23
+ ports:
24
+ - 27017:27017
25
+ options: >-
26
+ --health-cmd "mongosh --quiet --eval 'db.runCommand({ping:1}).ok' --port 27017"
27
+ --health-interval 5s
28
+ --health-timeout 5s
29
+ --health-retries 12
30
+ --health-start-period 10s
31
+ postgres:
32
+ image: postgres:16
33
+ ports:
34
+ - 5432:5432
35
+ env:
36
+ POSTGRES_USER: regstack
37
+ POSTGRES_PASSWORD: regstack
38
+ POSTGRES_DB: regstack
39
+ options: >-
40
+ --health-cmd "pg_isready -U regstack"
41
+ --health-interval 5s
42
+ --health-timeout 5s
43
+ --health-retries 12
44
+ --health-start-period 10s
45
+ env:
46
+ # Drives the parametrized backend fixture in tests/conftest.py.
47
+ # Connect as superuser so the per-test fixture can CREATE/DROP DATABASE.
48
+ REGSTACK_TEST_POSTGRES_URL: postgresql+asyncpg://regstack:regstack@localhost:5432
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+
52
+ - uses: astral-sh/setup-uv@v3
53
+ with:
54
+ enable-cache: true
55
+ cache-dependency-glob: "uv.lock"
56
+
57
+ - name: Pin Python ${{ matrix.python-version }}
58
+ run: uv python pin ${{ matrix.python-version }}
59
+
60
+ - name: Sync dependencies
61
+ run: uv sync --extra dev
62
+
63
+ - name: ruff check
64
+ run: uv run ruff check src tests
65
+
66
+ - name: ruff format check
67
+ run: uv run ruff format --check src tests
68
+
69
+ - name: pytest (parallel, all backends)
70
+ run: uv run python -m pytest -n auto --tb=short
71
+
72
+ docs:
73
+ runs-on: ubuntu-latest
74
+ steps:
75
+ - uses: actions/checkout@v4
76
+ - uses: astral-sh/setup-uv@v3
77
+ with:
78
+ enable-cache: true
79
+ cache-dependency-glob: "uv.lock"
80
+ - run: uv python pin 3.11
81
+ - run: uv sync --extra docs --extra dev
82
+ - run: uv run sphinx-build -b html -W --keep-going docs docs/_build/html
83
+ - uses: actions/upload-artifact@v4
84
+ with:
85
+ name: docs-html
86
+ path: docs/_build/html
87
+
88
+ base-install-smoketest:
89
+ # Catches the kind of regression that broke 0.2.0: an unconditional
90
+ # `from bson import ...` (or any other optional-extra import) at module
91
+ # top level makes `import regstack` fail for any user who didn't opt
92
+ # into the `mongo` extra. The unit test of the same name exercises the
93
+ # equivalent in-process via meta_path blocking; this job verifies the
94
+ # actual built wheel installs and imports against a clean Python.
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/checkout@v4
98
+ - uses: astral-sh/setup-uv@v3
99
+ with:
100
+ enable-cache: true
101
+ cache-dependency-glob: "uv.lock"
102
+ - run: uv python pin 3.11
103
+ - name: Build wheel
104
+ run: uv build --wheel
105
+ - name: Install wheel into a clean venv (no extras)
106
+ run: |
107
+ python -m venv /tmp/smoke
108
+ /tmp/smoke/bin/pip install --upgrade pip
109
+ /tmp/smoke/bin/pip install dist/regstack-*.whl
110
+ - name: Import smoketest
111
+ run: |
112
+ /tmp/smoke/bin/python -c "
113
+ import regstack
114
+ from regstack import RegStack, RegStackConfig
115
+ assert RegStack.__module__ == 'regstack.app'
116
+ print('ok', regstack.__version__)
117
+ "
118
+ - name: SQLite end-to-end smoketest
119
+ run: |
120
+ /tmp/smoke/bin/python <<'PY'
121
+ import asyncio, secrets, tempfile
122
+ from pathlib import Path
123
+ from regstack import RegStack, RegStackConfig
124
+ from regstack.models.user import BaseUser
125
+
126
+ async def main() -> None:
127
+ with tempfile.TemporaryDirectory() as td:
128
+ cfg = RegStackConfig.load(
129
+ toml_path=Path("/dev/null"),
130
+ secrets_env_path=Path("/dev/null"),
131
+ jwt_secret=secrets.token_urlsafe(64),
132
+ database_url=f"sqlite+aiosqlite:///{td}/rs.db",
133
+ require_verification=False,
134
+ allow_registration=True,
135
+ rate_limit_disabled=True,
136
+ )
137
+ rs = RegStack(config=cfg)
138
+ await rs.install_schema()
139
+ user = await rs.users.create(
140
+ BaseUser(email="a@b.com", hashed_password="h", is_verified=True)
141
+ )
142
+ assert user.id
143
+ await rs.aclose()
144
+ print("sqlite ok")
145
+
146
+ asyncio.run(main())
147
+ PY
@@ -5,6 +5,27 @@ authoritative copy lives at
5
5
  [`docs/changelog.md`](docs/changelog.md) and is rendered into the
6
6
  Sphinx docs.
7
7
 
8
+ ## 0.2.2 — 2026-04-28
9
+
10
+ Docs-only release. The README and Sphinx docs landing page now lead
11
+ with the same pitch (problem framing, "Why not just use…?" comparison
12
+ vs Auth0 / Clerk / Keycloak / fastapi-users) before diving into
13
+ architecture. Hyperlink density trimmed back: only major external
14
+ packages, products, and JWT (RFC 7519) are linked — Wikipedia trivia,
15
+ MDN basics, OWASP article links, and deep-dependency helper-class
16
+ docs were removed.
17
+
18
+ ## 0.2.1 — 2026-04-28
19
+
20
+ Hotfix for 0.2.0: `import regstack` failed on a base install because
21
+ several modules in the import path (`models/_objectid.py`,
22
+ `backends/protocols.py`, four routers, and the SQL `mfa_code_repo`)
23
+ had unconditional `from bson …` / `from regstack.backends.mongo …`
24
+ imports — but `pymongo` became an optional `mongo` extra in 0.2.0.
25
+ Added a CI smoketest that builds the wheel and imports it in a
26
+ no-extras venv, plus an in-process regression test that blocks `bson`
27
+ / `pymongo` via `sys.meta_path`.
28
+
8
29
  ## 0.2.0 — 2026-04-28
9
30
 
10
31
  Multi-backend support — SQLite (default), Postgres, MongoDB — switched
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
5
5
  Project-URL: Homepage, https://github.com/jdrumgoole/regstack
6
6
  Project-URL: Repository, https://github.com/jdrumgoole/regstack
@@ -96,17 +96,15 @@ the admin panel, lock out brute-force attackers, and ideally a second
96
96
  factor. Every one of those endpoints has a well-known way to get
97
97
  subtly wrong:
98
98
 
99
- - **Password hashing.** Use [Argon2](https://en.wikipedia.org/wiki/Argon2)
100
- (the [PHC winner](https://www.password-hashing.net/)), not MD5, SHA-1,
101
- bcrypt-without-pepper, or — somehow still common — plain text.
99
+ - **Password hashing.** Use Argon2id, not MD5, SHA-1, bcrypt-without-pepper,
100
+ or somehow still common — plain text.
102
101
  - **Token revocation.** A [JWT](https://datatracker.ietf.org/doc/html/rfc7519)
103
102
  is signed and self-contained: the server can't "log it out" unless you
104
103
  build a revocation list. Forget this and a stolen token works until it
105
104
  expires.
106
105
  - **Account enumeration.** A login or password-reset endpoint that
107
106
  responds differently for "user exists" vs "user doesn't" lets an
108
- attacker harvest your customer list. See
109
- [OWASP WSTG-IDNT-04](https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/03-Identity_Management_Testing/04-Testing_for_Account_Enumeration_and_Guessable_User_Account).
107
+ attacker harvest your customer list.
110
108
  - **Bulk session invalidation.** When a user changes their password
111
109
  because they think they were compromised, every existing token they
112
110
  hold should stop working immediately. Most homegrown JWT layers don't
@@ -115,9 +113,9 @@ subtly wrong:
115
113
  random, hashed at rest, single-use, and expire fast. Storing the raw
116
114
  token in the database is a "now your DB backup is also a credential
117
115
  dump" mistake.
118
- - **Phone numbers.** SMS codes need [E.164](https://en.wikipedia.org/wiki/E.164)-validated
119
- numbers, attempt limits, and an upstream provider. Wiring all of that
120
- yourself for a single feature is rarely worth it.
116
+ - **Phone numbers.** SMS codes need E.164-validated numbers, attempt
117
+ limits, and an upstream provider. Wiring all of that yourself for a
118
+ single feature is rarely worth it.
121
119
 
122
120
  Doing all of these correctly, with tests, is two to four weeks of
123
121
  engineering for a competent team. Doing them once and embedding the
@@ -244,9 +242,9 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
244
242
 
245
243
  Alpha. Single-file SQLite is the default and runs with no infrastructure;
246
244
  PostgreSQL and MongoDB backends pass the same parametrized integration
247
- suite. The next tagged release is `v0.2.0`. See the
245
+ suite. Latest tagged release: `v0.2.1`. See the
248
246
  [changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
249
- for the per-milestone breakdown.
247
+ for the per-release breakdown.
250
248
 
251
249
  ## Contributing
252
250
 
@@ -32,17 +32,15 @@ the admin panel, lock out brute-force attackers, and ideally a second
32
32
  factor. Every one of those endpoints has a well-known way to get
33
33
  subtly wrong:
34
34
 
35
- - **Password hashing.** Use [Argon2](https://en.wikipedia.org/wiki/Argon2)
36
- (the [PHC winner](https://www.password-hashing.net/)), not MD5, SHA-1,
37
- bcrypt-without-pepper, or — somehow still common — plain text.
35
+ - **Password hashing.** Use Argon2id, not MD5, SHA-1, bcrypt-without-pepper,
36
+ or somehow still common — plain text.
38
37
  - **Token revocation.** A [JWT](https://datatracker.ietf.org/doc/html/rfc7519)
39
38
  is signed and self-contained: the server can't "log it out" unless you
40
39
  build a revocation list. Forget this and a stolen token works until it
41
40
  expires.
42
41
  - **Account enumeration.** A login or password-reset endpoint that
43
42
  responds differently for "user exists" vs "user doesn't" lets an
44
- attacker harvest your customer list. See
45
- [OWASP WSTG-IDNT-04](https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/03-Identity_Management_Testing/04-Testing_for_Account_Enumeration_and_Guessable_User_Account).
43
+ attacker harvest your customer list.
46
44
  - **Bulk session invalidation.** When a user changes their password
47
45
  because they think they were compromised, every existing token they
48
46
  hold should stop working immediately. Most homegrown JWT layers don't
@@ -51,9 +49,9 @@ subtly wrong:
51
49
  random, hashed at rest, single-use, and expire fast. Storing the raw
52
50
  token in the database is a "now your DB backup is also a credential
53
51
  dump" mistake.
54
- - **Phone numbers.** SMS codes need [E.164](https://en.wikipedia.org/wiki/E.164)-validated
55
- numbers, attempt limits, and an upstream provider. Wiring all of that
56
- yourself for a single feature is rarely worth it.
52
+ - **Phone numbers.** SMS codes need E.164-validated numbers, attempt
53
+ limits, and an upstream provider. Wiring all of that yourself for a
54
+ single feature is rarely worth it.
57
55
 
58
56
  Doing all of these correctly, with tests, is two to four weeks of
59
57
  engineering for a competent team. Doing them once and embedding the
@@ -180,9 +178,9 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
180
178
 
181
179
  Alpha. Single-file SQLite is the default and runs with no infrastructure;
182
180
  PostgreSQL and MongoDB backends pass the same parametrized integration
183
- suite. The next tagged release is `v0.2.0`. See the
181
+ suite. Latest tagged release: `v0.2.1`. See the
184
182
  [changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
185
- for the per-milestone breakdown.
183
+ for the per-release breakdown.
186
184
 
187
185
  ## Contributing
188
186
 
@@ -5,10 +5,11 @@ who wants to embed it, extend it, or contribute to it. If you only
5
5
  want to use it, [Quickstart](quickstart.md) is shorter.
6
6
 
7
7
  regstack is a single embeddable façade — `RegStack` — that wires
8
- together storage, password and JWT primitives, an email service, an
9
- SMS service, a [hooks bus](#hooks), and a [FastAPI](https://fastapi.tiangolo.com/)
10
- router. Hosts construct one façade per application and mount its
11
- router(s) wherever they like.
8
+ together storage, password and [JWT](https://datatracker.ietf.org/doc/html/rfc7519)
9
+ primitives, an email service, an SMS service, a [hooks bus](#hooks),
10
+ and a [FastAPI](https://fastapi.tiangolo.com/) router. Hosts
11
+ construct one façade per application and mount its router(s) wherever
12
+ they like.
12
13
 
13
14
  ```text
14
15
  ┌────────────────────────────────────────────────┐
@@ -36,10 +37,9 @@ router(s) wherever they like.
36
37
  └────────┘ └──────────┘ └──────────┘
37
38
  ```
38
39
 
39
- The pattern is the
40
- [façade pattern](https://en.wikipedia.org/wiki/Facade_pattern): one
41
- object that owns and exposes a coherent set of related sub-systems,
42
- so the host has a single import to learn.
40
+ The pattern is the façade pattern: one object that owns and exposes
41
+ a coherent set of related sub-systems, so the host has a single
42
+ import to learn.
43
43
 
44
44
  ## One façade per process
45
45
 
@@ -48,8 +48,7 @@ sms_service=…, mail_composer=…)` is the only public constructor.
48
48
  Everything downstream (routers, dependencies, repos) takes its
49
49
  dependencies from this instance, so you can run **two regstack
50
50
  instances in the same process** without shared state — useful for
51
- [multi-tenant](https://en.wikipedia.org/wiki/Multitenancy) deployments
52
- where a single FastAPI app serves multiple
51
+ multi-tenant deployments where a single FastAPI app serves multiple
53
52
  `<host, database, branding>` triples.
54
53
 
55
54
  The backend is auto-built from `config.database_url` if not supplied
@@ -64,9 +63,7 @@ The façade exposes:
64
63
  - `router` — the JSON `APIRouter` to mount under `config.api_prefix`.
65
64
  - `ui_router` — the SSR `APIRouter` (built on first access; only
66
65
  meaningful when `enable_ui_router=True`).
67
- - `static_files` — Starlette
68
- [`StaticFiles`](https://www.starlette.io/staticfiles/) over the
69
- bundled CSS/JS.
66
+ - `static_files` — Starlette `StaticFiles` over the bundled CSS/JS.
70
67
  - `deps` — `AuthDependencies` factory for `current_user` /
71
68
  `current_admin` (each call returns a closure-bound dep).
72
69
  - `users`, `pending`, `blacklist`, `attempts`, `mfa_codes` — repos.
@@ -75,7 +72,7 @@ The façade exposes:
75
72
  - `backend` — the active `regstack.backends.base.Backend`.
76
73
  - `install_schema()` — install indexes (Mongo) or run
77
74
  [Alembic](https://alembic.sqlalchemy.org/) migrations (SQL).
78
- `install_indexes()` is kept as a backwards-compat alias.
75
+ `install_indexes()` is kept as a back-compat alias.
79
76
  - `aclose()` — tear down the backend's connection pool.
80
77
  - `bootstrap_admin(email, password)`,
81
78
  `add_template_dir(path)`, `set_email_backend(...)`,
@@ -85,9 +82,8 @@ The façade exposes:
85
82
 
86
83
  The Backend ABC owns the persistence story. Each backend ships:
87
84
 
88
- - One concrete repository per
89
- [Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol):
90
- `UserRepoProtocol`, `PendingRepoProtocol`, `BlacklistRepoProtocol`,
85
+ - One concrete repository per Protocol: `UserRepoProtocol`,
86
+ `PendingRepoProtocol`, `BlacklistRepoProtocol`,
91
87
  `LoginAttemptRepoProtocol`, `MfaCodeRepoProtocol`.
92
88
  - `install_schema()` to create indexes (Mongo) or run table creation /
93
89
  Alembic migrations (SQL).
@@ -95,16 +91,15 @@ The Backend ABC owns the persistence story. Each backend ships:
95
91
  - `aclose()` for clean shutdown.
96
92
 
97
93
  The Mongo backend lives at `regstack.backends.mongo`; the SQL backend
98
- (driving both SQLite and Postgres via [SQLAlchemy 2 async](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html))
99
- lives at `regstack.backends.sql`. Both are routed via the
94
+ (driving both SQLite and Postgres via [SQLAlchemy](https://www.sqlalchemy.org/)
95
+ async) lives at `regstack.backends.sql`. Both are routed via the
100
96
  `regstack.backends.factory.build_backend(config)` factory.
101
97
 
102
98
  ### TTL handling differences
103
99
 
104
- Mongo gets free expiry via [TTL indexes](https://www.mongodb.com/docs/manual/core/index-ttl/)
105
- `pending_registrations`, `token_blacklist`, `login_attempts`, and
106
- `mfa_codes` all have `expireAfterSeconds` indexes that the Mongo
107
- background task reaps.
100
+ Mongo gets free expiry via TTL indexes — `pending_registrations`,
101
+ `token_blacklist`, `login_attempts`, and `mfa_codes` all have
102
+ `expireAfterSeconds` indexes that the Mongo background task reaps.
108
103
 
109
104
  The SQL backends have no equivalent. Two safety nets:
110
105
 
@@ -112,22 +107,21 @@ The SQL backends have no equivalent. Two safety nets:
112
107
  checks `expires_at > now()` (or equivalent). A stale row in the
113
108
  table is harmless because it's never returned.
114
109
  - **Periodic reaper**: each repo exposes `purge_expired(...)`. Hosts
115
- that care about disk usage can run it on a schedule (e.g. via
116
- [APScheduler](https://apscheduler.readthedocs.io/) or a
117
- `regstack reap` cron job).
110
+ that care about disk usage can run it on a schedule (e.g. a cron
111
+ job calling a small `regstack reap` script).
118
112
 
119
113
  This means SQL backends are functionally correct without the reaper,
120
114
  but accumulate dead rows over time. Mongo doesn't.
121
115
 
122
116
  ## Repositories
123
117
 
124
- Each backend ships a thin async repo per collection / table. The Mongo
125
- repos are tz-aware because `make_client` configures
118
+ Each backend ships a thin async repo per collection / table. The
119
+ Mongo repos are tz-aware because `make_client` configures
126
120
  `AsyncMongoClient(..., tz_aware=True)`; the SQL repos use a custom
127
- `UtcDateTime` [TypeDecorator](https://docs.sqlalchemy.org/en/20/core/custom_types.html#sqlalchemy.types.TypeDecorator)
128
- that stores UTC and re-attaches the UTC tzinfo on read. Every layer
129
- above the repo assumes UTC-aware datetimes — there is no naive
130
- datetime anywhere in the public API.
121
+ `UtcDateTime` SQLAlchemy `TypeDecorator` that stores UTC and
122
+ re-attaches the UTC tzinfo on read. Every layer above the repo
123
+ assumes UTC-aware datetimes — there is no naive datetime anywhere in
124
+ the public API.
131
125
 
132
126
  `UserRepo` accepts an injected `Clock`; bulk-revoke writes
133
127
  (`update_password`, `update_email`, `set_tokens_invalidated_after`)
@@ -179,20 +173,18 @@ one mechanism:
179
173
  - `build_ui_environment` — SSR HTML templates under
180
174
  `regstack/ui/templates/`.
181
175
 
182
- Both wrap a
183
- [`ChoiceLoader([host_dirs..., regstack_default])`](https://jinja.palletsprojects.com/en/stable/api/#jinja2.ChoiceLoader)
184
- so a host override drops a same-named file into its template
185
- directory and wins over the bundled version.
186
- `RegStack.add_template_dir(path)` feeds both loaders simultaneously.
176
+ Both wrap a `ChoiceLoader([host_dirs..., regstack_default])` so a
177
+ host override drops a same-named file into its template directory and
178
+ wins over the bundled version. `RegStack.add_template_dir(path)`
179
+ feeds both loaders simultaneously.
187
180
 
188
181
  ## CLI runtime
189
182
 
190
183
  `regstack init`, `regstack create-admin`, and `regstack doctor` share
191
- `cli/_runtime.py`. `open_regstack(toml_path=None)` is an
192
- [async context manager](https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager)
193
- that builds a real RegStack against a real backend, runs
194
- `install_schema()`, and tears the connection down on exit — the right
195
- pattern for a short-lived CLI invocation.
184
+ `cli/_runtime.py`. `open_regstack(toml_path=None)` is an async
185
+ context manager that builds a real RegStack against a real backend,
186
+ runs `install_schema()`, and tears the connection down on exit — the
187
+ right pattern for a short-lived CLI invocation.
196
188
 
197
189
  ## Testing seams
198
190
 
@@ -206,5 +198,4 @@ pattern for a short-lived CLI invocation.
206
198
  test build multiple `RegStack` instances against per-worker DBs to
207
199
  exercise different config combinations without leaking. The
208
200
  parametrized `backend_kind` fixture runs every integration test
209
- against every active backend in parallel via
210
- [`pytest-xdist`](https://pytest-xdist.readthedocs.io/).
201
+ against every active backend in parallel via pytest-xdist.
@@ -3,6 +3,61 @@
3
3
  All notable changes to this project are documented here. Versions follow
4
4
  [Semantic Versioning](https://semver.org/) once `1.0.0` ships.
5
5
 
6
+ ## 0.2.2 — 2026-04-28
7
+
8
+ **Docs-only release.**
9
+
10
+ ### Changed
11
+
12
+ - README and `docs/index.md` both now lead with the same pitch — a
13
+ tagline ("Production-grade user accounts for your FastAPI app —
14
+ without the vendor lock-in, the second service to run, or the
15
+ homegrown auth bugs"), a "The problem regstack solves" section
16
+ (Argon2, JWT revocation, account enumeration, bulk session
17
+ invalidation, hashed one-time tokens, E.164 phone numbers), and a
18
+ "Why not just use…?" comparison table covering hosted SaaS
19
+ (Auth0 / Clerk / WorkOS / Stytch), self-hosted IAM (Keycloak /
20
+ Authentik / Authelia / Ory Kratos), `fastapi-users`, and DIY.
21
+ - Trimmed hyperlink density back. Only major external packages,
22
+ products, and JWT (RFC 7519) are linked. Wikipedia articles on
23
+ CS concepts (façade pattern, multitenancy, idempotence, E.164,
24
+ SHA-256, HMAC), MDN web platform basics (CSP, fetch, localStorage,
25
+ HTTP 429, Retry-After, HTTPS, CSS custom properties), OWASP article
26
+ links, Python stdlib pages, and deep-dependency helper-class docs
27
+ (pwdlib, pydantic, asyncpg, pymongo, ChoiceLoader, TypeDecorator,
28
+ StaticFiles, ProxyHeadersMiddleware, slowapi, APScheduler,
29
+ pytest-xdist, Kubernetes probes) were removed.
30
+
31
+ ## 0.2.1 — 2026-04-28
32
+
33
+ **Hotfix for 0.2.0.** `import regstack` was broken on any install that
34
+ didn't include the new `mongo` extra: `models/_objectid.py` imported
35
+ `bson` unconditionally, and four routers + the SQL MFA repo imported
36
+ shared error / enum types out of `regstack.backends.mongo.*`, which
37
+ in turn imports `pymongo` at module top level.
38
+
39
+ ### Fixed
40
+
41
+ - `models/_objectid.py` now imports `bson.ObjectId` lazily inside a
42
+ `try / except ImportError` and only uses it for `isinstance` checks
43
+ when present.
44
+ - `UserAlreadyExistsError`, `PendingAlreadyExistsError`,
45
+ `MfaVerifyOutcome`, and `MfaVerifyResult` moved from their backend
46
+ modules to `regstack.backends.protocols` (the backend-agnostic
47
+ location). Mongo modules re-export them for backwards compatibility.
48
+ - All consumer modules (`routers/register.py`, `routers/account.py`,
49
+ `routers/login.py`, `routers/phone.py`, the SQL MFA repo) updated to
50
+ import from `regstack.backends.protocols`.
51
+
52
+ ### Added
53
+
54
+ - New `base-install-smoketest` CI job: builds the wheel and runs
55
+ `import regstack` + a SQLite end-to-end RegStack lifecycle in a
56
+ fresh venv with **no extras**. Will catch any future regression.
57
+ - New `tests/unit/test_base_install_imports.py` regression test that
58
+ uses `sys.meta_path` to block `bson` / `pymongo` and confirm
59
+ `import regstack` still succeeds.
60
+
6
61
  ## 0.2.0 — 2026-04-28
7
62
 
8
63
  **Multi-backend support + Alembic migrations.** SQLite is now the
@@ -110,9 +110,8 @@ regstack.add_template_dir(Path("/app/host/templates"))
110
110
  ```
111
111
 
112
112
  Drop a same-named file into your directory to win against the bundled
113
- default — regstack uses Jinja2's
114
- [`ChoiceLoader`](https://jinja.palletsprojects.com/en/stable/api/#jinja2.ChoiceLoader)
115
- so the host directory is searched first. Examples:
113
+ default — regstack uses Jinja2's `ChoiceLoader` so the host directory
114
+ is searched first. Examples:
116
115
 
117
116
  - `auth/login.html` — replaces the SSR sign-in page.
118
117
  - `verification.html` / `verification.txt` /
@@ -125,8 +124,8 @@ A list of every overridable file lives in
125
124
  ## Switching the SSR theme without templates
126
125
 
127
126
  If you only want to flip colors / fonts, you don't need to override
128
- any templates — just supply a CSS file that overrides the
129
- [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*):
127
+ any templates — just supply a CSS file that overrides the bundled
128
+ CSS custom properties:
130
129
 
131
130
  ```toml
132
131
  # regstack.toml
@@ -171,8 +170,8 @@ In code:
171
170
  await regstack.bootstrap_admin("admin@example.com", "long-strong-password")
172
171
  ```
173
172
 
174
- This is [idempotent](https://en.wikipedia.org/wiki/Idempotence)
175
- promotes an existing user, creates one if not present.
173
+ This is idempotent — promotes an existing user, creates one if not
174
+ present.
176
175
 
177
176
  ## Health-check and probes
178
177
 
@@ -183,16 +182,14 @@ uv run regstack doctor [--config ...] [--check-dns] [--send-test-email <addr>]
183
182
  `doctor` reports JWT secret strength, database reachability, indexes,
184
183
  the email backend's instantiability, and optionally DNS (SPF/DKIM/MX)
185
184
  and a real email send. Exit code is the number of failed checks —
186
- wire it into a container health check or a
187
- [Kubernetes liveness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)
185
+ wire it into a container health check or a Kubernetes liveness probe
188
186
  for production probes that need more than a TCP hit.
189
187
 
190
188
  ## What regstack does *not* do
191
189
 
192
- - It does not mount a [CSRF](https://owasp.org/www-community/attacks/csrf)
193
- middleware. The bundled SSR pages don't use cookies, so they don't
194
- need it; if you swap the bundled JS for a cookie-based variant,
195
- configure CSRF at the host.
190
+ - It does not mount a CSRF middleware. The bundled SSR pages don't
191
+ use cookies, so they don't need it; if you swap the bundled JS for
192
+ a cookie-based variant, configure CSRF at the host.
196
193
  - It does not enforce HTTPS. Run behind a TLS terminator.
197
194
  - It does not provision SES identities, Route 53 records, IAM users,
198
195
  or anything else outside the database.