supython 0.1.14__tar.gz → 0.1.15__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 (266) hide show
  1. {supython-0.1.14 → supython-0.1.15}/CHANGELOG.md +114 -0
  2. {supython-0.1.14 → supython-0.1.15}/PKG-INFO +26 -9
  3. {supython-0.1.14 → supython-0.1.15}/README.md +25 -8
  4. {supython-0.1.14 → supython-0.1.15}/pyproject.toml +1 -1
  5. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/providers/__init__.py +8 -1
  6. supython-0.1.15/src/supython/auth/providers/github.py +50 -0
  7. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/providers/google.py +4 -0
  8. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/providers/oauth.py +11 -0
  9. supython-0.1.15/src/supython/auth/router.py +629 -0
  10. supython-0.1.15/src/supython/auth/schemas.py +177 -0
  11. supython-0.1.15/src/supython/auth/service.py +1388 -0
  12. {supython-0.1.14 → supython-0.1.15}/src/supython/client/__init__.py +4 -0
  13. {supython-0.1.14 → supython-0.1.15}/src/supython/client/_auth.py +169 -7
  14. supython-0.1.15/src/supython/migrations/0018_auth_email_confirmation.sql +25 -0
  15. supython-0.1.15/src/supython/migrations/0019_hash_refresh_tokens.sql +19 -0
  16. supython-0.1.15/src/supython/migrations/0020_email_change_and_metadata.sql +34 -0
  17. supython-0.1.15/src/supython/netutil.py +64 -0
  18. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/env.example.tmpl +21 -1
  19. {supython-0.1.14 → supython-0.1.15}/src/supython/settings.py +25 -0
  20. supython-0.1.14/src/supython/auth/providers/github.py +0 -22
  21. supython-0.1.14/src/supython/auth/router.py +0 -322
  22. supython-0.1.14/src/supython/auth/schemas.py +0 -92
  23. supython-0.1.14/src/supython/auth/service.py +0 -753
  24. {supython-0.1.14 → supython-0.1.15}/.gitignore +0 -0
  25. {supython-0.1.14 → supython-0.1.15}/LICENSE +0 -0
  26. {supython-0.1.14 → supython-0.1.15}/SECURITY.md +0 -0
  27. {supython-0.1.14 → supython-0.1.15}/src/supython/__init__.py +0 -0
  28. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/__init__.py +0 -0
  29. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/__init__.py +0 -0
  30. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/auth.py +0 -0
  31. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/auth_templates.py +0 -0
  32. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/auth_users.py +0 -0
  33. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/db.py +0 -0
  34. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/functions.py +0 -0
  35. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/jobs.py +0 -0
  36. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/ops.py +0 -0
  37. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/realtime.py +0 -0
  38. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_auth.py +0 -0
  39. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_auth_templates.py +0 -0
  40. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_auth_users.py +0 -0
  41. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_db.py +0 -0
  42. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_functions.py +0 -0
  43. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_jobs.py +0 -0
  44. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_ops.py +0 -0
  45. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_realtime.py +0 -0
  46. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/service_storage.py +0 -0
  47. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/storage.py +0 -0
  48. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/api/system.py +0 -0
  49. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/audit.py +0 -0
  50. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/deps.py +0 -0
  51. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/errors.py +0 -0
  52. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/schemas.py +0 -0
  53. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/session.py +0 -0
  54. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/spa.py +0 -0
  55. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Alert-dluGVkos.js +0 -0
  56. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Alert-dluGVkos.js.map +0 -0
  57. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Audit-Njung3HI.js +0 -0
  58. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Audit-Njung3HI.js.map +0 -0
  59. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Backups-DzPlFgrm.js +0 -0
  60. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Backups-DzPlFgrm.js.map +0 -0
  61. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Buckets-ByacGkU1.js +0 -0
  62. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Buckets-ByacGkU1.js.map +0 -0
  63. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Channels-BoIuTtam.js +0 -0
  64. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map +0 -0
  65. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +0 -0
  66. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map +0 -0
  67. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +0 -0
  68. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map +0 -0
  69. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Crons-B67vc39F.js +0 -0
  70. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Crons-B67vc39F.js.map +0 -0
  71. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js +0 -0
  72. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map +0 -0
  73. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/DataTable-COAAWEft.js +0 -0
  74. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map +0 -0
  75. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +0 -0
  76. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js.map +0 -0
  77. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js +0 -0
  78. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js.map +0 -0
  79. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Empty-cr2r7e2u.js +0 -0
  80. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Empty-cr2r7e2u.js.map +0 -0
  81. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js +0 -0
  82. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js.map +0 -0
  83. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Grid-hFkp9F4P.js +0 -0
  84. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Grid-hFkp9F4P.js.map +0 -0
  85. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Input-DppYTq9C.js +0 -0
  86. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Input-DppYTq9C.js.map +0 -0
  87. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js +0 -0
  88. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js.map +0 -0
  89. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/JsonField-DibyJgun.js +0 -0
  90. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/JsonField-DibyJgun.js.map +0 -0
  91. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/LoginView-BjLyE3Ds.css +0 -0
  92. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/LoginView-CoOjECT_.js +0 -0
  93. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/LoginView-CoOjECT_.js.map +0 -0
  94. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Logs-D9WYrnIT.js +0 -0
  95. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Logs-D9WYrnIT.js.map +0 -0
  96. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Logs-DS1XPa0h.css +0 -0
  97. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js +0 -0
  98. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js.map +0 -0
  99. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +0 -0
  100. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js.map +0 -0
  101. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Queue-CywZs6vI.js +0 -0
  102. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Queue-CywZs6vI.js.map +0 -0
  103. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +0 -0
  104. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js.map +0 -0
  105. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js +0 -0
  106. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js.map +0 -0
  107. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Routes-BiLXE49D.js +0 -0
  108. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Routes-BiLXE49D.js.map +0 -0
  109. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Routes-C-ianIGD.css +0 -0
  110. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +0 -0
  111. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +0 -0
  112. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js.map +0 -0
  113. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Select-DIzZyRZb.js +0 -0
  114. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Select-DIzZyRZb.js.map +0 -0
  115. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Space-n5-XcguU.js +0 -0
  116. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Space-n5-XcguU.js.map +0 -0
  117. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js +0 -0
  118. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js.map +0 -0
  119. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +0 -0
  120. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js.map +0 -0
  121. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/TableData-CQIagLKn.js +0 -0
  122. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/TableData-CQIagLKn.js.map +0 -0
  123. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Tag-D1fOKpTH.js +0 -0
  124. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Tag-D1fOKpTH.js.map +0 -0
  125. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Templates-BS-ugkdq.js +0 -0
  126. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Templates-BS-ugkdq.js.map +0 -0
  127. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Thing-CEAniuMg.js +0 -0
  128. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Thing-CEAniuMg.js.map +0 -0
  129. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Users-wzwajhlh.js +0 -0
  130. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/Users-wzwajhlh.js.map +0 -0
  131. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +0 -0
  132. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/dist-VXIJLCYq.js +0 -0
  133. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/dist-VXIJLCYq.js.map +0 -0
  134. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/format-length-CGCY1rMh.js +0 -0
  135. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/format-length-CGCY1rMh.js.map +0 -0
  136. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/get-Ca6unauB.js +0 -0
  137. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/get-Ca6unauB.js.map +0 -0
  138. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/index-CeE6v959.js +0 -0
  139. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/index-CeE6v959.js.map +0 -0
  140. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/pinia-COXwfrOX.js +0 -0
  141. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/pinia-COXwfrOX.js.map +0 -0
  142. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/resources-Bt6thQCD.js +0 -0
  143. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/resources-Bt6thQCD.js.map +0 -0
  144. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js +0 -0
  145. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js.map +0 -0
  146. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js +0 -0
  147. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js.map +0 -0
  148. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js +0 -0
  149. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js.map +0 -0
  150. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useResource-C_rJCY8C.js +0 -0
  151. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useResource-C_rJCY8C.js.map +0 -0
  152. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useTable-CnZc5zhi.js +0 -0
  153. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useTable-CnZc5zhi.js.map +0 -0
  154. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useTable-Dg0XlRlq.css +0 -0
  155. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useToast-DsZKx0IX.js +0 -0
  156. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/useToast-DsZKx0IX.js.map +0 -0
  157. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/utils-sbXoq7Ir.js +0 -0
  158. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/assets/utils-sbXoq7Ir.js.map +0 -0
  159. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/favicon.svg +0 -0
  160. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/icons.svg +0 -0
  161. {supython-0.1.14 → supython-0.1.15}/src/supython/admin/static/index.html +0 -0
  162. {supython-0.1.14 → supython-0.1.15}/src/supython/app.py +0 -0
  163. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/__init__.py +0 -0
  164. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/_email_job.py +0 -0
  165. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/claims.py +0 -0
  166. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/deps.py +0 -0
  167. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/providers/registry.py +0 -0
  168. {supython-0.1.14 → supython-0.1.15}/src/supython/auth/ratelimit.py +0 -0
  169. {supython-0.1.14 → supython-0.1.15}/src/supython/backups/__init__.py +0 -0
  170. {supython-0.1.14 → supython-0.1.15}/src/supython/backups/_backup_job.py +0 -0
  171. {supython-0.1.14 → supython-0.1.15}/src/supython/backups/schemas.py +0 -0
  172. {supython-0.1.14 → supython-0.1.15}/src/supython/backups/service.py +0 -0
  173. {supython-0.1.14 → supython-0.1.15}/src/supython/body_size.py +0 -0
  174. {supython-0.1.14 → supython-0.1.15}/src/supython/cli.py +0 -0
  175. {supython-0.1.14 → supython-0.1.15}/src/supython/client/_client.py +0 -0
  176. {supython-0.1.14 → supython-0.1.15}/src/supython/client/_config.py +0 -0
  177. {supython-0.1.14 → supython-0.1.15}/src/supython/client/_functions.py +0 -0
  178. {supython-0.1.14 → supython-0.1.15}/src/supython/client/_storage.py +0 -0
  179. {supython-0.1.14 → supython-0.1.15}/src/supython/client/py.typed +0 -0
  180. {supython-0.1.14 → supython-0.1.15}/src/supython/db.py +0 -0
  181. {supython-0.1.14 → supython-0.1.15}/src/supython/db_admin.py +0 -0
  182. {supython-0.1.14 → supython-0.1.15}/src/supython/extensions.py +0 -0
  183. {supython-0.1.14 → supython-0.1.15}/src/supython/functions/__init__.py +0 -0
  184. {supython-0.1.14 → supython-0.1.15}/src/supython/functions/context.py +0 -0
  185. {supython-0.1.14 → supython-0.1.15}/src/supython/functions/loader.py +0 -0
  186. {supython-0.1.14 → supython-0.1.15}/src/supython/functions/router.py +0 -0
  187. {supython-0.1.14 → supython-0.1.15}/src/supython/functions/schemas.py +0 -0
  188. {supython-0.1.14 → supython-0.1.15}/src/supython/gen/__init__.py +0 -0
  189. {supython-0.1.14 → supython-0.1.15}/src/supython/gen/_introspect.py +0 -0
  190. {supython-0.1.14 → supython-0.1.15}/src/supython/gen/types_py.py +0 -0
  191. {supython-0.1.14 → supython-0.1.15}/src/supython/gen/types_ts.py +0 -0
  192. {supython-0.1.14 → supython-0.1.15}/src/supython/health.py +0 -0
  193. {supython-0.1.14 → supython-0.1.15}/src/supython/hooks.py +0 -0
  194. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/__init__.py +0 -0
  195. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/backends.py +0 -0
  196. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/context.py +0 -0
  197. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/cron.py +0 -0
  198. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/cron_inproc.py +0 -0
  199. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/decorators.py +0 -0
  200. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/registry.py +0 -0
  201. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/router.py +0 -0
  202. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/schemas.py +0 -0
  203. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/service.py +0 -0
  204. {supython-0.1.14 → supython-0.1.15}/src/supython/jobs/worker.py +0 -0
  205. {supython-0.1.14 → supython-0.1.15}/src/supython/jwks.py +0 -0
  206. {supython-0.1.14 → supython-0.1.15}/src/supython/keyset.py +0 -0
  207. {supython-0.1.14 → supython-0.1.15}/src/supython/logging_config.py +0 -0
  208. {supython-0.1.14 → supython-0.1.15}/src/supython/mail.py +0 -0
  209. {supython-0.1.14 → supython-0.1.15}/src/supython/mailer.py +0 -0
  210. {supython-0.1.14 → supython-0.1.15}/src/supython/migrate.py +0 -0
  211. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0001_extensions_and_roles.sql +0 -0
  212. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0002_auth_schema.sql +0 -0
  213. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0004_auth_v0_2.sql +0 -0
  214. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0005_storage_schema.sql +0 -0
  215. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0006_realtime_schema.sql +0 -0
  216. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0007_jobs_schema.sql +0 -0
  217. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0008_jobs_last_error.sql +0 -0
  218. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0009_auth_rate_limits.sql +0 -0
  219. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0010_worker_heartbeat.sql +0 -0
  220. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0011_admin_schema.sql +0 -0
  221. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0012_auth_banned_until.sql +0 -0
  222. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0013_email_templates.sql +0 -0
  223. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0014_realtime_payload_warning.sql +0 -0
  224. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0015_backups_schema.sql +0 -0
  225. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0016_magic_link_redirect.sql +0 -0
  226. {supython-0.1.14 → supython-0.1.15}/src/supython/migrations/0017_auth_activated_at.sql +0 -0
  227. {supython-0.1.14 → supython-0.1.15}/src/supython/passwords.py +0 -0
  228. {supython-0.1.14 → supython-0.1.15}/src/supython/realtime/__init__.py +0 -0
  229. {supython-0.1.14 → supython-0.1.15}/src/supython/realtime/broker.py +0 -0
  230. {supython-0.1.14 → supython-0.1.15}/src/supython/realtime/protocol.py +0 -0
  231. {supython-0.1.14 → supython-0.1.15}/src/supython/realtime/router.py +0 -0
  232. {supython-0.1.14 → supython-0.1.15}/src/supython/realtime/schemas.py +0 -0
  233. {supython-0.1.14 → supython-0.1.15}/src/supython/realtime/service.py +0 -0
  234. {supython-0.1.14 → supython-0.1.15}/src/supython/realtime/topics.py +0 -0
  235. {supython-0.1.14 → supython-0.1.15}/src/supython/realtime/websocket.py +0 -0
  236. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/__init__.py +0 -0
  237. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/init_project.py +0 -0
  238. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/Caddyfile.tmpl +0 -0
  239. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/README.md.tmpl +0 -0
  240. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/apps_hooks_welcome.py.tmpl +0 -0
  241. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/apps_jobs_example.py.tmpl +0 -0
  242. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/asgi.py.tmpl +0 -0
  243. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/docker-compose.prod.yml.tmpl +0 -0
  244. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/docker-compose.yml.tmpl +0 -0
  245. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +0 -0
  246. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +0 -0
  247. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/functions_README.md.tmpl +0 -0
  248. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/gitignore.tmpl +0 -0
  249. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/manage.py.tmpl +0 -0
  250. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/package_hooks_init.py.tmpl +0 -0
  251. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/package_init.py.tmpl +0 -0
  252. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/package_jobs_init.py.tmpl +0 -0
  253. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/pyproject.toml.tmpl +0 -0
  254. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/settings.py.tmpl +0 -0
  255. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/static/0001_create_todos.sql +0 -0
  256. {supython-0.1.14 → supython-0.1.15}/src/supython/scaffold/templates/static/functions_hello.py +0 -0
  257. {supython-0.1.14 → supython-0.1.15}/src/supython/secretset.py +0 -0
  258. {supython-0.1.14 → supython-0.1.15}/src/supython/security_headers.py +0 -0
  259. {supython-0.1.14 → supython-0.1.15}/src/supython/settings_module.py +0 -0
  260. {supython-0.1.14 → supython-0.1.15}/src/supython/storage/__init__.py +0 -0
  261. {supython-0.1.14 → supython-0.1.15}/src/supython/storage/backends.py +0 -0
  262. {supython-0.1.14 → supython-0.1.15}/src/supython/storage/router.py +0 -0
  263. {supython-0.1.14 → supython-0.1.15}/src/supython/storage/schemas.py +0 -0
  264. {supython-0.1.14 → supython-0.1.15}/src/supython/storage/service.py +0 -0
  265. {supython-0.1.14 → supython-0.1.15}/src/supython/storage/signing.py +0 -0
  266. {supython-0.1.14 → supython-0.1.15}/src/supython/tokens.py +0 -0
@@ -30,6 +30,120 @@ Each entry links the relevant `PROJECT.md` section and decision-log row
30
30
 
31
31
  ---
32
32
 
33
+ ## [0.1.15] — 2026-07-21
34
+
35
+ Auth hardening from the GHSA security review (three stacked PRs, #10–#12).
36
+
37
+ ### Breaking
38
+ - Emailed verify links (`GET /auth/v1/{magiclink,confirm}/verify`) are now
39
+ side-effect-free HTML interstitials; the token is consumed by the form
40
+ **POST** to the same path. Mail scanners that prefetch the GET no longer
41
+ burn the token or (in redirect mode) receive the session. Any client
42
+ calling the verify endpoint directly must switch GET → POST (the bundled
43
+ SDK already does).
44
+ - OAuth `redirect_uri` must now match `OAUTH_REDIRECT_ALLOWLIST`
45
+ (comma-separated origins). The allowlist is empty by default, which
46
+ **fails closed**: OAuth sign-in returns `invalid_redirect` (400) until
47
+ the setting lists your app origins. Previously any `redirect_uri`
48
+ accepted by the provider was 302'd to with the token pair in the URL
49
+ fragment — a token-stealing open redirect on laxly configured
50
+ providers.
51
+ - Refresh tokens are stored as sha256 hashes (migration `0019` converts
52
+ existing rows in place; issued raw tokens keep working). Anything that
53
+ read raw tokens out of `auth.refresh_tokens` directly — including the
54
+ admin refresh-token list — now sees digests.
55
+ - `client.auth.sign_up(...)` now resolves to a `SignUpResponse`
56
+ (`{user, session, confirmation_sent_at}`) instead of a bare
57
+ `TokenResponse`; `session` is `None` when the server requires email
58
+ confirmation. Read tokens via `result.data.session.access_token`.
59
+ - OAuth sign-ins whose provider cannot vouch for the email address are
60
+ refused with `provider_email_unverified` (403). In particular, GitHub
61
+ accounts with no *verified* email can no longer sign in or link.
62
+
63
+ ### Added
64
+ - Email change with dual confirmation. `PUT /auth/v1/user` with `email`
65
+ starts it; a confirmation link is sent to **both** the current and the
66
+ new address, and the change applies only once both are verified
67
+ (`POST /auth/v1/email_change/verify`, 202 until the second side). The
68
+ current-inbox requirement means a stolen access token alone cannot
69
+ re-point the account email. New `EMAIL_CHANGE_TOKEN_TTL` (1 h) and two
70
+ operator-editable templates (migration `0020`).
71
+ - User/app metadata. `raw_user_meta_data` is now wired through: set at
72
+ signup via `data`, merged via `PUT /auth/v1/user` `data`, and returned
73
+ as `user_metadata` on the user object — user-controlled, display-only,
74
+ **never** an authorization input. New server-controlled
75
+ `raw_app_meta_data` (migration `0020`) records the auth provider(s) and
76
+ is surfaced as `app_metadata`. SDK: `auth.update_user(email=, data=)`,
77
+ `auth.verify_email_change(token)`.
78
+ - `TRUSTED_PROXIES` — proxy-aware client-IP resolution for rate limiting
79
+ and audit logging. When the TCP peer is a listed proxy, the IP is taken
80
+ from the rightmost untrusted `X-Forwarded-For` hop; empty (default)
81
+ ignores the header so a spoofed value can never shift a rate-limit
82
+ bucket.
83
+ - Signup email-confirmation flow (§9.2). With
84
+ `AUTH_REQUIRE_EMAIL_CONFIRMATION=true`, `POST /auth/v1/signup` returns
85
+ 202 without tokens and emails a confirmation link;
86
+ `GET /auth/v1/confirm/verify` redeems it (optionally 302-redirecting to
87
+ an allowlisted `redirect_url` with the token pair in the fragment) and
88
+ `POST /auth/v1/confirm/resend` re-sends it. Sign-in and refresh are
89
+ refused with `email_not_confirmed` (403) until the address is proven.
90
+ New settings: `AUTH_REQUIRE_EMAIL_CONFIRMATION` (default off),
91
+ `SIGNUP_CONFIRM_TOKEN_TTL` (24 h),
92
+ `AUTH_RATE_LIMIT_CONFIRM_PER_WINDOW` (5). New `signup_confirm`
93
+ one-time-token type and operator-editable email template
94
+ (migration `0018`). Client SDK grows `auth.verify_signup(token)` and
95
+ `auth.resend_confirmation(email)`.
96
+ - Scoped signout. `POST /auth/v1/logout` accepts
97
+ `{refresh_token?, scope: local|global|others}` (default `local`, wire-
98
+ compatible with the old body). `local` also revokes the token's rotated
99
+ descendants; `global` revokes every session and can be driven by the
100
+ bearer access token alone; `others` keeps only the presented session.
101
+ Global/others sign-outs write a `sign_out` audit event. Client SDK:
102
+ `auth.sign_out(scope=...)`.
103
+ - `PUT /auth/v1/user` — authenticated password change. Requires
104
+ `current_password` when one is set (a stolen access token alone cannot
105
+ take over the credential); passwordless (OAuth-only/invite) accounts
106
+ may set a first password with just their bearer. Revokes every refresh
107
+ token and returns a fresh pair. Client SDK:
108
+ `auth.update_password(new, current)`.
109
+
110
+ ### Changed
111
+ - `auth.users.email_confirmed_at` is now an honest "inbox ownership
112
+ proven" flag: stamped by signup confirmation, magic-link / OTP /
113
+ recovery verification, and provider-verified OAuth sign-ins — never by
114
+ signup itself (previously it was set unconditionally at signup). Rows
115
+ created before `0018` keep their old unproven stamp; see the migration
116
+ header for the strict-mode cleanup query.
117
+
118
+ ### Fixed
119
+ - One-time-token consumption (recover / magic link / OTP / signup
120
+ confirm) is now a single atomic `update … where used_at is null`,
121
+ closing the race where two concurrent verifies of the same token could
122
+ both succeed.
123
+
124
+ ### Security
125
+ - Emailed verify links are no longer consumable by GET prefetch (see
126
+ Breaking) — closes the link-scanner token-burn / session-leak window.
127
+ - `PUT /auth/v1/user` email change requires confirmation from the current
128
+ inbox as well as the new one, so a stolen access token cannot silently
129
+ hijack the account's email.
130
+ - A successful password reset (`/auth/v1/recover/verify`) now revokes
131
+ every existing refresh token and every other pending recover token, so
132
+ a suspected-stolen session does not survive the reset. Password change
133
+ via `PUT /auth/v1/user` does the same.
134
+ - Refresh tokens are hashed at rest (see Breaking) and OAuth redirect
135
+ targets are origin-allowlisted (see Breaking).
136
+ - Account pre-hijack defence (pre-hijack pair, review 2026-07-21):
137
+ OAuth account creation and link-by-email now require a
138
+ provider-verified email (Google: OIDC `email_verified`; GitHub:
139
+ verified entry from `/user/emails`), and linking into an unproven-email
140
+ account that has a password is refused with `email_conflict` (403) and
141
+ an `oauth_link_refused` audit event. Previously an attacker could
142
+ pre-register a victim's email with a password and silently gain a
143
+ backdoor into the account the victim later created via OAuth.
144
+
145
+ ---
146
+
33
147
  ## [0.1.14] — 2026-07-17
34
148
 
35
149
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supython
3
- Version: 0.1.14
3
+ Version: 0.1.15
4
4
  Summary: A lightweight Postgres-first BaaS framework for Python
5
5
  Project-URL: Homepage, https://github.com/Tkeby/supython
6
6
  Project-URL: Repository, https://github.com/Tkeby/supython
@@ -407,20 +407,29 @@ supython dev
407
407
 
408
408
  | Endpoint | Method | Purpose |
409
409
  |---|---|---|
410
- | `/auth/v1/signup` | POST | Create account, return token pair |
410
+ | `/auth/v1/signup` | POST | Create account; returns a token pair, or `202` + confirmation email when `AUTH_REQUIRE_EMAIL_CONFIRMATION` is on |
411
+ | `/auth/v1/confirm/verify` | GET / POST | GET renders a confirm page; POST redeems the signup-confirmation token |
412
+ | `/auth/v1/confirm/resend` | POST | Re-send the signup confirmation email |
411
413
  | `/auth/v1/token` | POST | Password login |
412
414
  | `/auth/v1/refresh` | POST | Rotate refresh token (reuse detection built in) |
413
- | `/auth/v1/logout` | POST | Revoke refresh token |
414
- | `/auth/v1/user` | GET | Return the caller's user (JWT required) |
415
+ | `/auth/v1/logout` | POST | Sign out `scope`: `local` (default) / `global` / `others` |
416
+ | `/auth/v1/user` | GET | Return the caller's user, incl. `user_metadata` / `app_metadata` (JWT required) |
417
+ | `/auth/v1/user` | PUT | Change password, OR start an email change / merge `data` into `user_metadata` |
418
+ | `/auth/v1/email_change/verify` | GET / POST | Confirm one side of a dual-confirmation email change |
415
419
  | `/auth/v1/recover` | POST | Request password-reset email |
416
- | `/auth/v1/recover/verify` | POST | Verify reset token, set new password |
420
+ | `/auth/v1/recover/verify` | POST | Verify reset token, set new password (revokes all sessions) |
417
421
  | `/auth/v1/magiclink` | POST | Request magic-link email |
418
- | `/auth/v1/magiclink/verify` | GET | Verify magic-link token (`?token=…`) |
422
+ | `/auth/v1/magiclink/verify` | GET / POST | GET renders a confirm page; POST redeems the magic-link token |
419
423
  | `/auth/v1/otp` | POST | Request email OTP |
420
424
  | `/auth/v1/otp/verify` | POST | Verify OTP code |
421
- | `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider) |
425
+ | `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider; `redirect_uri` must be allowlisted) |
422
426
  | `/auth/v1/callback/{provider}` | GET | Handle OAuth callback, redirect with tokens |
423
427
 
428
+ Emailed verify links (signup confirm, magic link, email change) are
429
+ **GET landing pages** that consume nothing — the token is redeemed by the
430
+ form **POST** they submit, so mail-scanner GET prefetch can't burn a token
431
+ or leak a session.
432
+
424
433
  **Email backend** — set `EMAIL_BACKEND=console` (default, logs to stdout) or
425
434
  `EMAIL_BACKEND=smtp` and configure `SMTP_HOST / SMTP_PORT / SMTP_USERNAME /
426
435
  SMTP_PASSWORD`.
@@ -428,8 +437,10 @@ SMTP_PASSWORD`.
428
437
  **OAuth** — add `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (and/or GitHub
429
438
  equivalents) to `.env`. Providers without credentials are silently disabled.
430
439
 
431
- **Logout semantics (stateless JWT)** — `/auth/v1/logout` revokes the
432
- **refresh** token; the access token remains valid until its `exp` claim. This
440
+ **Logout semantics (stateless JWT)** — `/auth/v1/logout` revokes **refresh**
441
+ tokens (`scope=local` for this session and its rotated descendants, `global`
442
+ for all, `others` for all but this one); the access token remains valid until
443
+ its `exp` claim. This
433
444
  is the standard stateless-JWT posture (same as Supabase Auth, Auth0, Cognito):
434
445
  both supython and PostgREST verify access tokens from the JWKS with **no
435
446
  per-request DB lookup**, so there is no place to consult a server-side
@@ -530,6 +541,12 @@ need any claim beyond the user UUID.
530
541
  | `AUTH_RATE_LIMIT_RECOVER_PER_WINDOW` | `3` | `/auth/v1/recover` attempts per IP/window |
531
542
  | `AUTH_RATE_LIMIT_OTP_PER_WINDOW` | `5` | `/auth/v1/otp` attempts per IP/window |
532
543
  | `AUTH_RATE_LIMIT_MAGICLINK_PER_WINDOW` | `5` | `/auth/v1/magiclink` attempts per IP/window |
544
+ | `AUTH_RATE_LIMIT_CONFIRM_PER_WINDOW` | `5` | `/auth/v1/confirm/*` attempts per IP/window |
545
+ | `AUTH_REQUIRE_EMAIL_CONFIRMATION` | `false` | Require new signups to confirm their email (signup returns 202 without tokens; sign-in refused with `email_not_confirmed` until `/auth/v1/confirm/verify`) |
546
+ | `OAUTH_REDIRECT_ALLOWLIST` | *(empty)* | Comma-separated origins an OAuth `redirect_uri` may target. Empty fails closed — OAuth sign-in is refused until configured |
547
+ | `TRUSTED_PROXIES` | *(empty)* | Comma-separated IPs/CIDRs of reverse proxies. When the TCP peer is trusted, rate limiting and audit logs use the rightmost untrusted `X-Forwarded-For` hop; empty ignores the header entirely |
548
+ | `EMAIL_CHANGE_TOKEN_TTL` | `3600` | Email-change confirmation token lifetime in seconds (both inboxes must confirm) |
549
+ | `SIGNUP_CONFIRM_TOKEN_TTL` | `86400` | Signup confirmation token lifetime in seconds (24 h) |
533
550
 
534
551
  **`AUTHENTICATOR_PASSWORD`** — the password used for the `authenticator` Postgres
535
552
  role that PostgREST connects as. Defaults to `authenticator` (matches the
@@ -348,20 +348,29 @@ supython dev
348
348
 
349
349
  | Endpoint | Method | Purpose |
350
350
  |---|---|---|
351
- | `/auth/v1/signup` | POST | Create account, return token pair |
351
+ | `/auth/v1/signup` | POST | Create account; returns a token pair, or `202` + confirmation email when `AUTH_REQUIRE_EMAIL_CONFIRMATION` is on |
352
+ | `/auth/v1/confirm/verify` | GET / POST | GET renders a confirm page; POST redeems the signup-confirmation token |
353
+ | `/auth/v1/confirm/resend` | POST | Re-send the signup confirmation email |
352
354
  | `/auth/v1/token` | POST | Password login |
353
355
  | `/auth/v1/refresh` | POST | Rotate refresh token (reuse detection built in) |
354
- | `/auth/v1/logout` | POST | Revoke refresh token |
355
- | `/auth/v1/user` | GET | Return the caller's user (JWT required) |
356
+ | `/auth/v1/logout` | POST | Sign out `scope`: `local` (default) / `global` / `others` |
357
+ | `/auth/v1/user` | GET | Return the caller's user, incl. `user_metadata` / `app_metadata` (JWT required) |
358
+ | `/auth/v1/user` | PUT | Change password, OR start an email change / merge `data` into `user_metadata` |
359
+ | `/auth/v1/email_change/verify` | GET / POST | Confirm one side of a dual-confirmation email change |
356
360
  | `/auth/v1/recover` | POST | Request password-reset email |
357
- | `/auth/v1/recover/verify` | POST | Verify reset token, set new password |
361
+ | `/auth/v1/recover/verify` | POST | Verify reset token, set new password (revokes all sessions) |
358
362
  | `/auth/v1/magiclink` | POST | Request magic-link email |
359
- | `/auth/v1/magiclink/verify` | GET | Verify magic-link token (`?token=…`) |
363
+ | `/auth/v1/magiclink/verify` | GET / POST | GET renders a confirm page; POST redeems the magic-link token |
360
364
  | `/auth/v1/otp` | POST | Request email OTP |
361
365
  | `/auth/v1/otp/verify` | POST | Verify OTP code |
362
- | `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider) |
366
+ | `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider; `redirect_uri` must be allowlisted) |
363
367
  | `/auth/v1/callback/{provider}` | GET | Handle OAuth callback, redirect with tokens |
364
368
 
369
+ Emailed verify links (signup confirm, magic link, email change) are
370
+ **GET landing pages** that consume nothing — the token is redeemed by the
371
+ form **POST** they submit, so mail-scanner GET prefetch can't burn a token
372
+ or leak a session.
373
+
365
374
  **Email backend** — set `EMAIL_BACKEND=console` (default, logs to stdout) or
366
375
  `EMAIL_BACKEND=smtp` and configure `SMTP_HOST / SMTP_PORT / SMTP_USERNAME /
367
376
  SMTP_PASSWORD`.
@@ -369,8 +378,10 @@ SMTP_PASSWORD`.
369
378
  **OAuth** — add `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (and/or GitHub
370
379
  equivalents) to `.env`. Providers without credentials are silently disabled.
371
380
 
372
- **Logout semantics (stateless JWT)** — `/auth/v1/logout` revokes the
373
- **refresh** token; the access token remains valid until its `exp` claim. This
381
+ **Logout semantics (stateless JWT)** — `/auth/v1/logout` revokes **refresh**
382
+ tokens (`scope=local` for this session and its rotated descendants, `global`
383
+ for all, `others` for all but this one); the access token remains valid until
384
+ its `exp` claim. This
374
385
  is the standard stateless-JWT posture (same as Supabase Auth, Auth0, Cognito):
375
386
  both supython and PostgREST verify access tokens from the JWKS with **no
376
387
  per-request DB lookup**, so there is no place to consult a server-side
@@ -471,6 +482,12 @@ need any claim beyond the user UUID.
471
482
  | `AUTH_RATE_LIMIT_RECOVER_PER_WINDOW` | `3` | `/auth/v1/recover` attempts per IP/window |
472
483
  | `AUTH_RATE_LIMIT_OTP_PER_WINDOW` | `5` | `/auth/v1/otp` attempts per IP/window |
473
484
  | `AUTH_RATE_LIMIT_MAGICLINK_PER_WINDOW` | `5` | `/auth/v1/magiclink` attempts per IP/window |
485
+ | `AUTH_RATE_LIMIT_CONFIRM_PER_WINDOW` | `5` | `/auth/v1/confirm/*` attempts per IP/window |
486
+ | `AUTH_REQUIRE_EMAIL_CONFIRMATION` | `false` | Require new signups to confirm their email (signup returns 202 without tokens; sign-in refused with `email_not_confirmed` until `/auth/v1/confirm/verify`) |
487
+ | `OAUTH_REDIRECT_ALLOWLIST` | *(empty)* | Comma-separated origins an OAuth `redirect_uri` may target. Empty fails closed — OAuth sign-in is refused until configured |
488
+ | `TRUSTED_PROXIES` | *(empty)* | Comma-separated IPs/CIDRs of reverse proxies. When the TCP peer is trusted, rate limiting and audit logs use the rightmost untrusted `X-Forwarded-For` hop; empty ignores the header entirely |
489
+ | `EMAIL_CHANGE_TOKEN_TTL` | `3600` | Email-change confirmation token lifetime in seconds (both inboxes must confirm) |
490
+ | `SIGNUP_CONFIRM_TOKEN_TTL` | `86400` | Signup confirmation token lifetime in seconds (24 h) |
474
491
 
475
492
  **`AUTHENTICATOR_PASSWORD`** — the password used for the `authenticator` Postgres
476
493
  role that PostgREST connects as. Defaults to `authenticator` (matches the
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "supython"
7
- version = "0.1.14"
7
+ version = "0.1.15"
8
8
  description = "A lightweight Postgres-first BaaS framework for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,10 +7,17 @@ from typing import Any
7
7
 
8
8
  @dataclass
9
9
  class ProviderProfile:
10
- """Normalised identity returned by any OAuth provider."""
10
+ """Normalised identity returned by any OAuth provider.
11
+
12
+ ``email_verified`` must be True only when the provider itself vouches for
13
+ ownership of ``email``. It defaults to False so a provider that never sets
14
+ it fails closed: the auth service refuses to create or link an account by
15
+ email on an unverified profile (pre-hijack defence).
16
+ """
11
17
 
12
18
  provider_user_id: str
13
19
  email: str
20
+ email_verified: bool = False
14
21
  raw: dict[str, Any] = field(default_factory=dict)
15
22
 
16
23
 
@@ -0,0 +1,50 @@
1
+ """GitHub OAuth2 provider."""
2
+
3
+ from authlib.integrations.httpx_client import AsyncOAuth2Client
4
+
5
+ from . import ProviderProfile
6
+ from .oauth import OAuthProvider
7
+
8
+
9
+ class GitHubProvider(OAuthProvider):
10
+ name = "github"
11
+ AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
12
+ # GitHub requires Accept: application/json to return JSON (not form-encoded).
13
+ TOKEN_URL = "https://github.com/login/oauth/access_token"
14
+ USERINFO_URL = "https://api.github.com/user"
15
+ EMAILS_URL = "https://api.github.com/user/emails"
16
+ DEFAULT_SCOPE = "read:user user:email"
17
+
18
+ async def _build_profile(
19
+ self, client: AsyncOAuth2Client, data: dict
20
+ ) -> ProviderProfile:
21
+ # The public profile email on /user is optional and carries no
22
+ # verified flag, so it must not be trusted for account linking.
23
+ # /user/emails (covered by the user:email scope) lists every address
24
+ # with primary/verified bits; only a verified one may be used.
25
+ email, verified = "", False
26
+ resp = await client.get(self.EMAILS_URL)
27
+ if resp.status_code == 200:
28
+ entries = [e for e in resp.json() if e.get("verified")]
29
+ primary = [e for e in entries if e.get("primary")]
30
+ chosen = primary[0] if primary else (entries[0] if entries else None)
31
+ if chosen:
32
+ email, verified = chosen["email"], True
33
+ # Fall back to the public profile email for display only — the
34
+ # service layer refuses email matching when verified is False.
35
+ if not email:
36
+ email = data.get("email") or ""
37
+ return ProviderProfile(
38
+ provider_user_id=str(data["id"]),
39
+ email=email,
40
+ email_verified=verified,
41
+ raw=data,
42
+ )
43
+
44
+ def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
45
+ return ProviderProfile(
46
+ provider_user_id=str(data["id"]),
47
+ email=data.get("email") or "",
48
+ email_verified=False,
49
+ raw=data,
50
+ )
@@ -12,8 +12,12 @@ class GoogleProvider(OAuthProvider):
12
12
  DEFAULT_SCOPE = "openid email profile"
13
13
 
14
14
  def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
15
+ # OIDC userinfo carries `email_verified`; Google may serve it as a
16
+ # bool or the string "true" depending on endpoint version.
17
+ verified = data.get("email_verified")
15
18
  return ProviderProfile(
16
19
  provider_user_id=data["sub"],
17
20
  email=data.get("email", ""),
21
+ email_verified=verified is True or verified == "true",
18
22
  raw=data,
19
23
  )
@@ -50,6 +50,17 @@ class OAuthProvider(Provider):
50
50
  resp = await client.get(self.USERINFO_URL)
51
51
  resp.raise_for_status()
52
52
  data: dict = resp.json()
53
+ return await self._build_profile(client, data)
54
+
55
+ async def _build_profile(
56
+ self, client: AsyncOAuth2Client, data: dict
57
+ ) -> ProviderProfile:
58
+ """Turn the userinfo payload into a profile.
59
+
60
+ Runs while the authenticated ``client`` is still open so providers that
61
+ need extra API calls (e.g. GitHub's /user/emails) can override this
62
+ instead of re-implementing ``exchange``.
63
+ """
53
64
  return self._profile_from_userinfo(data)
54
65
 
55
66
  def _profile_from_userinfo(self, data: dict) -> ProviderProfile: