tina4-python 3.13.78__tar.gz → 3.13.79__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 (168) hide show
  1. {tina4_python-3.13.78 → tina4_python-3.13.79}/PKG-INFO +1 -1
  2. {tina4_python-3.13.78 → tina4_python-3.13.79}/pyproject.toml +1 -1
  3. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/request.py +33 -9
  4. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/server.py +18 -6
  5. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/session/__init__.py +40 -4
  6. {tina4_python-3.13.78 → tina4_python-3.13.79}/.gitignore +0 -0
  7. {tina4_python-3.13.78 → tina4_python-3.13.79}/README.md +0 -0
  8. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/CLAUDE.md +0 -0
  9. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/__init__.py +0 -0
  12. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/ai/__init__.py +0 -0
  13. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/cli/__init__.py +0 -0
  17. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/context/__init__.py +0 -0
  19. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/context/chunker.py +0 -0
  20. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/rate_limiter.py +0 -0
  26. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/response.py +0 -0
  27. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/core/router.py +0 -0
  28. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/crud/__init__.py +0 -0
  29. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/__init__.py +0 -0
  30. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/adapter.py +0 -0
  31. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/connection.py +0 -0
  32. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/firebird.py +0 -0
  33. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/mongodb.py +0 -0
  34. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/mssql.py +0 -0
  35. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/mysql.py +0 -0
  36. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/odbc.py +0 -0
  37. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/postgres.py +0 -0
  38. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/database/sqlite.py +0 -0
  39. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/debug/__init__.py +0 -0
  40. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/debug/error_overlay.py +0 -0
  41. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/dev_admin/__init__.py +0 -0
  42. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/dev_admin/metrics.py +0 -0
  43. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/dev_admin/plan.py +0 -0
  44. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/dev_admin/project_index.py +0 -0
  45. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/docs.py +0 -0
  46. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/docstore/__init__.py +0 -0
  47. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/dotenv/__init__.py +0 -0
  48. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/env.py +0 -0
  49. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/frond/FROND.md +0 -0
  50. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/frond/__init__.py +0 -0
  51. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/frond/engine.py +0 -0
  52. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/auth/meta.json +0 -0
  53. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  54. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/database/meta.json +0 -0
  55. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  56. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/error-overlay/meta.json +0 -0
  57. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  58. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/orm/meta.json +0 -0
  59. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  60. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  61. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/queue/meta.json +0 -0
  62. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  63. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/rest-api/meta.json +0 -0
  64. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  65. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/templates/meta.json +0 -0
  66. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  67. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  68. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/graphql/__init__.py +0 -0
  69. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/i18n/__init__.py +0 -0
  70. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/mcp/__init__.py +0 -0
  71. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/mcp/protocol.py +0 -0
  72. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/mcp/tools.py +0 -0
  73. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/messenger/__init__.py +0 -0
  74. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/migration/__init__.py +0 -0
  75. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/migration/runner.py +0 -0
  76. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/orm/__init__.py +0 -0
  77. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/orm/fields.py +0 -0
  78. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/orm/model.py +0 -0
  79. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/__feedback/widget.js +0 -0
  80. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/css/tina4.css +0 -0
  81. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/css/tina4.min.css +0 -0
  82. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/favicon.ico +0 -0
  83. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/images/logo.svg +0 -0
  84. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  85. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/js/frond.js +0 -0
  86. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/js/frond.min.js +0 -0
  87. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  88. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  89. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/js/tina4.min.js +0 -0
  90. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/js/tina4js.min.js +0 -0
  91. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/swagger/index.html +0 -0
  92. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  93. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/query_builder/__init__.py +0 -0
  94. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue/__init__.py +0 -0
  95. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue/job.py +0 -0
  96. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue/kafka_backend.py +0 -0
  97. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue/lite_backend.py +0 -0
  98. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue/mongo_backend.py +0 -0
  99. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue_backends/__init__.py +0 -0
  101. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue_backends/kafka_backend.py +0 -0
  102. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue_backends/mongo_backend.py +0 -0
  103. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  104. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/realtime/__init__.py +0 -0
  105. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/realtime/models/Attachment.py +0 -0
  106. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/realtime/models/Channel.py +0 -0
  107. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/realtime/models/ChannelMember.py +0 -0
  108. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/realtime/models/Message.py +0 -0
  109. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/realtime/models/Workspace.py +0 -0
  110. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/realtime/models/__init__.py +0 -0
  111. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/realtime/storage.py +0 -0
  112. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/__init__.py +0 -0
  113. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  114. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_badges.scss +0 -0
  115. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  116. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_cards.scss +0 -0
  117. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_forms.scss +0 -0
  118. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_grid.scss +0 -0
  119. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_modals.scss +0 -0
  120. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_nav.scss +0 -0
  121. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_reset.scss +0 -0
  122. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_tables.scss +0 -0
  123. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_typography.scss +0 -0
  124. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  125. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/_variables.scss +0 -0
  126. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/base.scss +0 -0
  127. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/colors.scss +0 -0
  128. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/scss/tina4css/tina4.scss +0 -0
  129. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/seeder/__init__.py +0 -0
  130. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/service/__init__.py +0 -0
  131. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  133. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/session_handlers/redis_handler.py +0 -0
  134. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/session_handlers/valkey_handler.py +0 -0
  135. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/swagger/__init__.py +0 -0
  136. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/components/crud.twig +0 -0
  137. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  138. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  139. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/docker/python/Dockerfile +0 -0
  140. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  141. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/errors/302.twig +0 -0
  142. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/errors/401.twig +0 -0
  143. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/errors/403.twig +0 -0
  144. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/errors/404.twig +0 -0
  145. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/errors/500.twig +0 -0
  146. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/errors/502.twig +0 -0
  147. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/errors/503.twig +0 -0
  148. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/errors/base.twig +0 -0
  149. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/frontend/README.md +0 -0
  150. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/templates/readme.md +0 -0
  151. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/test/__init__.py +0 -0
  152. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/test_client/__init__.py +0 -0
  153. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/validator/__init__.py +0 -0
  166. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/websocket/__init__.py +0 -0
  167. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/websocket/backplane.py +0 -0
  168. {tina4_python-3.13.78 → tina4_python-3.13.79}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.78
3
+ Version: 3.13.79
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.78"
3
+ version = "3.13.79"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -70,8 +70,8 @@ class Request:
70
70
  """Parsed HTTP request — everything a route handler needs."""
71
71
 
72
72
  __slots__ = (
73
- "method", "path", "url", "query_string", "params", "query", "headers",
74
- "body", "raw_body", "cookies", "files", "ip", "remote_ip",
73
+ "method", "path", "url", "scheme", "query_string", "params", "query",
74
+ "headers", "body", "raw_body", "cookies", "files", "ip", "remote_ip",
75
75
  "content_type", "session", "_route_params", "_handler",
76
76
  )
77
77
 
@@ -79,6 +79,7 @@ class Request:
79
79
  self.method: str = "GET"
80
80
  self.path: str = "/"
81
81
  self.url: str = "/"
82
+ self.scheme: str = "http" # Native connection scheme (see is_secure_scheme)
82
83
  self.query_string: str = ""
83
84
  self.params: dict = {} # Query string + route params merged
84
85
  self.query: dict = {} # Query string params only (separate from route params)
@@ -97,6 +98,27 @@ class Request:
97
98
  # middleware (e.g. CsrfMiddleware) can read
98
99
  # handler metadata like _noauth.
99
100
 
101
+ def is_secure_scheme(self) -> bool:
102
+ """True when the client's request scheme is https.
103
+
104
+ Proxy-aware: TLS is normally terminated at a proxy (nginx, HAProxy,
105
+ ALB, Cloudflare, most container deploys) which then forwards plain
106
+ HTTP, so the native ASGI scheme reads ``http`` on exactly the deploys
107
+ that ARE encrypted. ``x-forwarded-proto`` carries the scheme the
108
+ client actually used; the FIRST hop of a comma-separated chain
109
+ (``"https, http"``) is the client-facing one. Falls back to the
110
+ native connection scheme when no forwarded header is present.
111
+
112
+ This is the single source of truth for the URL scheme AND for the
113
+ Secure flag on the session cookie, so both agree instead of one
114
+ concluding ``https://`` while the other drops Secure (#95; mirrors
115
+ tina4-php ``Request::isSecureScheme()``, php#175).
116
+ """
117
+ forwarded = self.headers.get("x-forwarded-proto", "")
118
+ if forwarded:
119
+ return forwarded.split(",")[0].strip().lower() == "https"
120
+ return (self.scheme or "").lower() == "https"
121
+
100
122
  @classmethod
101
123
  def from_scope(cls, scope: dict, body: bytes = b"") -> "Request":
102
124
  """Build a Request from an ASGI scope + body."""
@@ -117,14 +139,16 @@ class Request:
117
139
  _client = scope.get("client")
118
140
  req.remote_ip = _client[0] if _client else ""
119
141
 
142
+ # Native connection scheme (ASGI: "http"/"https"; TLS terminated at a
143
+ # proxy shows "http" here — x-forwarded-proto carries the real one).
144
+ req.scheme = scope.get("scheme") or "http"
145
+
120
146
  # Reconstruct the full absolute URL — scheme://host[:port]/path[?query].
121
- # Honours x-forwarded-proto and x-forwarded-host so apps behind a proxy
122
- # still see the URL the client used. Matches PHP/Ruby/Node parity.
123
- scheme = (
124
- req.headers.get("x-forwarded-proto")
125
- or scope.get("scheme")
126
- or "http"
127
- )
147
+ # is_secure_scheme() is the single source of truth for the scheme (and
148
+ # for the session cookie's Secure flag): it honours x-forwarded-proto
149
+ # (first hop of a comma chain wins) then the native scheme, so an app
150
+ # behind a TLS-terminating proxy still sees https. PHP/Ruby/Node parity.
151
+ scheme = "https" if req.is_secure_scheme() else "http"
128
152
  host = (
129
153
  req.headers.get("x-forwarded-host")
130
154
  or req.headers.get("host")
@@ -1108,8 +1108,13 @@ def _init_session(request: Request) -> None:
1108
1108
  Session creation is skipped for WebSocket upgrade requests — they don't send
1109
1109
  cookies and would create orphaned session files that are never cleaned up.
1110
1110
  A session is only created when:
1111
- - A tina4_session cookie is already present (resume existing session), OR
1111
+ - A session cookie is already present (resume existing session), OR
1112
1112
  - The request is a normal HTTP request (not a WebSocket upgrade)
1113
+
1114
+ The incoming cookie is read by the SAME configured name the write side emits
1115
+ (``TINA4_SESSION_NAME``, default ``tina4_session``) via
1116
+ ``session.session_cookie_name()`` — otherwise a renamed cookie would be
1117
+ written but never read back and the session would silently never resume.
1113
1118
  """
1114
1119
  if request.session is not None:
1115
1120
  return
@@ -1122,12 +1127,13 @@ def _init_session(request: Request) -> None:
1122
1127
  return
1123
1128
 
1124
1129
  try:
1125
- from tina4_python.session import Session
1130
+ from tina4_python.session import Session, session_cookie_name
1126
1131
  cookie_header = request.headers.get("cookie", "")
1127
1132
  sid_match = None
1133
+ cookie_prefix = session_cookie_name() + "="
1128
1134
  for part in cookie_header.split(";"):
1129
1135
  part = part.strip()
1130
- if part.startswith("tina4_session="):
1136
+ if part.startswith(cookie_prefix):
1131
1137
  sid_match = part.split("=", 1)[1]
1132
1138
  break
1133
1139
 
@@ -1770,9 +1776,15 @@ def _finalize_response(
1770
1776
  request.session.save()
1771
1777
  sid = request.session.session_id if hasattr(request.session, 'session_id') else getattr(request.session, 'id', None)
1772
1778
  if sid:
1773
- ttl = int(os.environ.get("TINA4_SESSION_TTL", "3600"))
1774
- samesite = os.environ.get("TINA4_SESSION_SAMESITE", "Lax")
1775
- response.header("set-cookie", f"tina4_session={sid}; Path=/; HttpOnly; SameSite={samesite}; Max-Age={ttl}")
1779
+ # Route through the single cookie-builder so every
1780
+ # TINA4_SESSION_* knob (Secure/HttpOnly/SameSite) and the
1781
+ # proxy-aware Secure detection actually take effect. Do NOT
1782
+ # hand-write a second Set-Cookie here — that bypass is what
1783
+ # made TINA4_SESSION_SECURE a silent no-op (#95).
1784
+ response.header(
1785
+ "set-cookie",
1786
+ request.session.cookie_header(request=request),
1787
+ )
1776
1788
  import random
1777
1789
  if random.randint(1, 100) == 1:
1778
1790
  request.session.gc()
@@ -18,6 +18,21 @@ import secrets
18
18
  from pathlib import Path
19
19
 
20
20
 
21
+ def session_cookie_name() -> str:
22
+ """Resolve the session cookie name — the single source of truth shared by
23
+ the WRITE side (``Session.cookie_header``) and the READ side
24
+ (``core/server._init_session``), so a cookie written under a renamed name is
25
+ read back on the next request.
26
+
27
+ TINA4_SESSION_NAME Cookie name (default: ``tina4_session``)
28
+
29
+ Keeping this in one place means the default can never drift between the two
30
+ sides: an operator who sets ``TINA4_SESSION_NAME`` renames the cookie on both
31
+ the emit and the parse paths at once.
32
+ """
33
+ return os.environ.get("TINA4_SESSION_NAME", "tina4_session")
34
+
35
+
21
36
  class SessionHandler:
22
37
  """Base class for session storage backends."""
23
38
 
@@ -379,7 +394,7 @@ class Session:
379
394
  result = self.flash(key)
380
395
  return result if result is not None else default
381
396
 
382
- def cookie_header(self, cookie_name: str = None) -> str:
397
+ def cookie_header(self, cookie_name: str = None, request=None) -> str:
383
398
  """Return a Set-Cookie header value for this session.
384
399
 
385
400
  Cookie attributes are env-driven so deployments can flip security
@@ -387,17 +402,37 @@ class Session:
387
402
 
388
403
  TINA4_SESSION_NAME Cookie name (default: tina4_session)
389
404
  TINA4_SESSION_HTTPONLY HttpOnly flag (default: true)
390
- TINA4_SESSION_SECURE Secure flag (default: false). Set to
391
- true only when serving over HTTPS.
405
+ TINA4_SESSION_SECURE Secure flag (default: false). Forces
406
+ Secure on regardless of request scheme.
392
407
  TINA4_SESSION_SAMESITE SameSite attribute (default: Lax)
408
+
409
+ The Secure attribute is set when ANY of the following holds:
410
+
411
+ 1. TINA4_SESSION_SECURE is truthy (explicit opt-in), OR
412
+ 2. SameSite is ``None`` — browsers reject SameSite=None without
413
+ Secure, OR
414
+ 3. the request is really on https — detected proxy-aware via
415
+ ``request.is_secure_scheme()`` (honours x-forwarded-proto, first
416
+ hop of a comma chain), so a session behind a TLS-terminating
417
+ proxy still ships Secure (#95).
418
+
419
+ Plain HTTP with no proxy signal and no TLS stays non-Secure: a Secure
420
+ cookie over plain http is undeliverable and would silently break the
421
+ session. Pass ``request`` so the scheme can be detected; omit it and
422
+ only the env/SameSite signals apply.
393
423
  """
394
424
  if cookie_name is None:
395
- cookie_name = os.environ.get("TINA4_SESSION_NAME", "tina4_session")
425
+ cookie_name = session_cookie_name()
396
426
  samesite = os.environ.get("TINA4_SESSION_SAMESITE", "Lax")
397
427
  # Default true for HttpOnly (matches v2 behaviour) — only drop the
398
428
  # flag when the operator explicitly opts out.
399
429
  httponly = str(os.environ.get("TINA4_SESSION_HTTPONLY", "true")).strip().lower() in ("true", "1", "yes", "on")
400
430
  secure = str(os.environ.get("TINA4_SESSION_SECURE", "false")).strip().lower() in ("true", "1", "yes", "on")
431
+ secure = (
432
+ secure
433
+ or samesite.strip().lower() == "none"
434
+ or (request is not None and request.is_secure_scheme())
435
+ )
401
436
 
402
437
  parts = [f"{cookie_name}={self._session_id}", "Path=/"]
403
438
  if httponly:
@@ -421,4 +456,5 @@ class Session:
421
456
  __all__ = [
422
457
  "Session", "SessionHandler",
423
458
  "FileSessionHandler", "DatabaseSessionHandler",
459
+ "session_cookie_name",
424
460
  ]
File without changes