solvapay-python 0.8.0__tar.gz → 0.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. solvapay_python-0.9.1/.github/dependabot.yml +18 -0
  2. solvapay_python-0.9.1/.github/workflows/ci.yml +81 -0
  3. solvapay_python-0.9.1/.github/workflows/contract.yml +22 -0
  4. solvapay_python-0.9.1/.github/workflows/docs.yml +21 -0
  5. solvapay_python-0.9.1/.github/workflows/publish.yml +32 -0
  6. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.gitignore +2 -0
  7. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/CHANGELOG.md +49 -0
  8. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/CONTRIBUTING.md +15 -2
  9. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/PKG-INFO +69 -11
  10. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/README.md +61 -6
  11. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/SECURITY.md +15 -2
  12. solvapay_python-0.9.1/changelog.d/README.md +35 -0
  13. solvapay_python-0.9.1/docs/errors.md +50 -0
  14. solvapay_python-0.9.1/docs/guides/fastapi.md +53 -0
  15. solvapay_python-0.9.1/docs/guides/langchain.md +45 -0
  16. solvapay_python-0.9.1/docs/guides/mcp.md +50 -0
  17. solvapay_python-0.9.1/docs/idempotency.md +83 -0
  18. solvapay_python-0.9.1/docs/index.md +63 -0
  19. solvapay_python-0.9.1/docs/migration.md +49 -0
  20. solvapay_python-0.9.1/docs/reference/client.md +5 -0
  21. solvapay_python-0.9.1/docs/reference/exceptions.md +3 -0
  22. solvapay_python-0.9.1/docs/reference/models.md +3 -0
  23. solvapay_python-0.9.1/docs/reference/paywall.md +9 -0
  24. solvapay_python-0.9.1/docs/reference/webhooks.md +15 -0
  25. solvapay_python-0.9.1/docs/rfcs/0002-openapi-investigation.md +35 -0
  26. solvapay_python-0.9.1/docs/webhooks.md +79 -0
  27. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/uv.lock +128 -122
  28. solvapay_python-0.9.1/examples/multi-framework-paywall/uv.lock +1585 -0
  29. solvapay_python-0.9.1/mkdocs.yml +44 -0
  30. solvapay_python-0.9.1/pyproject.toml +132 -0
  31. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/__init__.py +1 -1
  32. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_async_client.py +21 -0
  33. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_transport/httpx_transport.py +5 -1
  34. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_transport/middleware.py +177 -1
  35. solvapay_python-0.9.1/src/solvapay/adapters/asgi.py +119 -0
  36. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/adapters/mcp.py +1 -1
  37. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/client.py +2 -0
  38. solvapay_python-0.9.1/src/solvapay/idempotency.py +37 -0
  39. solvapay_python-0.9.1/src/solvapay/webhooks/__init__.py +43 -0
  40. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/pipeline.py +3 -14
  41. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/replay.py +32 -0
  42. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/rotation.py +8 -4
  43. solvapay_python-0.9.1/src/solvapay/webhooks/sign.py +37 -0
  44. solvapay_python-0.9.1/tests/_transport/test_recording_transport.py +68 -0
  45. solvapay_python-0.9.1/tests/_transport/test_retry_transport.py +78 -0
  46. solvapay_python-0.9.1/tests/adapters/test_asgi.py +91 -0
  47. solvapay_python-0.9.1/tests/contract/README.md +38 -0
  48. solvapay_python-0.9.1/tests/contract/test_contract_ops.py +61 -0
  49. solvapay_python-0.9.1/tests/paywall/__init__.py +0 -0
  50. solvapay_python-0.9.1/tests/test_api_version.py +57 -0
  51. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_idempotency.py +33 -0
  52. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_paywall_state.py +42 -39
  53. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_redaction.py +37 -0
  54. solvapay_python-0.9.1/tests/test_webhook_timing.py +82 -0
  55. solvapay_python-0.9.1/tests/webhooks/__init__.py +0 -0
  56. solvapay_python-0.9.1/tests/webhooks/test_async_cache.py +48 -0
  57. solvapay_python-0.9.1/tests/webhooks/test_rotation.py +47 -0
  58. solvapay_python-0.9.1/tests/webhooks/test_sign.py +37 -0
  59. solvapay_python-0.9.1/tools/lint_invariants.py +208 -0
  60. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/uv.lock +1315 -32
  61. solvapay_python-0.8.0/.github/workflows/ci.yml +0 -27
  62. solvapay_python-0.8.0/.github/workflows/publish.yml +0 -15
  63. solvapay_python-0.8.0/pyproject.toml +0 -73
  64. solvapay_python-0.8.0/src/solvapay/idempotency.py +0 -16
  65. solvapay_python-0.8.0/src/solvapay/webhooks/__init__.py +0 -28
  66. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  67. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  68. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.github/ISSUE_TEMPLATE/question.yml +0 -0
  69. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  70. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/.python-version +0 -0
  71. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/CODEOWNERS +0 -0
  72. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/CODE_OF_CONDUCT.md +0 -0
  73. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/LICENSE +0 -0
  74. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/SUPPORT.md +0 -0
  75. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/assets/agent-marketplace.png +0 -0
  76. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/docs/architecture/layers.md +0 -0
  77. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/docs/rfcs/0001-spending-policy.md +0 -0
  78. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/.env.example +0 -0
  79. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/.gitignore +0 -0
  80. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/README.md +0 -0
  81. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/claim.py +0 -0
  82. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/pyproject.toml +0 -0
  83. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/fastmcp-paywall/server.py +0 -0
  84. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/.env.example +0 -0
  85. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/.gitignore +0 -0
  86. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/README.md +0 -0
  87. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/agent.py +0 -0
  88. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/langchain-paywall/pyproject.toml +0 -0
  89. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/.env.example +0 -0
  90. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/.gitignore +0 -0
  91. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/.streamlit/config.toml +0 -0
  92. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/PLAN.md +0 -0
  93. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/README.md +0 -0
  94. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/agents.py +0 -0
  95. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/app.py +0 -0
  96. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/demo_customers.py +0 -0
  97. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/requirements.txt +0 -0
  98. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/sdk_gateway.py +0 -0
  99. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/marketplace/ui_components.py +0 -0
  100. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/.env.example +0 -0
  101. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/.gitignore +0 -0
  102. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/README.md +0 -0
  103. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/agent_langchain.py +0 -0
  104. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/model.py +0 -0
  105. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/pyproject.toml +0 -0
  106. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/script_async.py +0 -0
  107. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/server_mcp.py +0 -0
  108. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/examples/multi-framework-paywall/tool.py +0 -0
  109. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_config.py +0 -0
  110. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_http.py +0 -0
  111. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_stability.py +0 -0
  112. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_transport/__init__.py +0 -0
  113. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/_transport/_recipe.py +0 -0
  114. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/adapters/__init__.py +0 -0
  115. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/adapters/langchain.py +0 -0
  116. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/events.py +0 -0
  117. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/exceptions.py +0 -0
  118. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/fastapi.py +0 -0
  119. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/langchain.py +0 -0
  120. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/models.py +0 -0
  121. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/__init__.py +0 -0
  122. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/_registry.py +0 -0
  123. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/checkout.py +0 -0
  124. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/customers.py +0 -0
  125. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/limits.py +0 -0
  126. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/merchant.py +0 -0
  127. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/plans.py +0 -0
  128. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/products.py +0 -0
  129. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/purchases.py +0 -0
  130. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/operations/usage.py +0 -0
  131. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/__init__.py +0 -0
  132. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/core.py +0 -0
  133. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/decorators.py +0 -0
  134. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/meta.py +0 -0
  135. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/policy.py +0 -0
  136. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/resolvers.py +0 -0
  137. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall/state.py +0 -0
  138. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/paywall_state.py +0 -0
  139. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/py.typed +0 -0
  140. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/envelope.py +0 -0
  141. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/src/solvapay/webhooks/verify.py +0 -0
  142. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/__init__.py +0 -0
  143. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_stability/__init__.py +0 -0
  144. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_stability/test_stable_returns_identity.py +0 -0
  145. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/__init__.py +0 -0
  146. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_aclose_cascade.py +0 -0
  147. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_error_wrapping.py +0 -0
  148. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_headers_case_insensitive.py +0 -0
  149. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_middleware_composition.py +0 -0
  150. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/_transport/test_protocol_conformance.py +0 -0
  151. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/adapters/__init__.py +0 -0
  152. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/adapters/test_langchain_protocol.py +0 -0
  153. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/adapters/test_mcp.py +0 -0
  154. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/conftest.py +0 -0
  155. {solvapay_python-0.8.0/tests/operations → solvapay_python-0.9.1/tests/contract}/__init__.py +0 -0
  156. /solvapay_python-0.8.0/tests/paywall/__init__.py → /solvapay_python-0.9.1/tests/contract/cassettes/.gitkeep +0 -0
  157. {solvapay_python-0.8.0/tests/webhooks → solvapay_python-0.9.1/tests/operations}/__init__.py +0 -0
  158. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/operations/test_namespace_api.py +0 -0
  159. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/operations/test_path_interpolation.py +0 -0
  160. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/operations/test_retry_safety_enum.py +0 -0
  161. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/paywall/test_checkout_mint_error_surfaces.py +0 -0
  162. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/paywall/test_payable_tool_meta.py +0 -0
  163. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/paywall/test_resolvers.py +0 -0
  164. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/paywall/test_split_classes.py +0 -0
  165. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_admin.py +0 -0
  166. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_async_client.py +0 -0
  167. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_checkout.py +0 -0
  168. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_config.py +0 -0
  169. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_customer.py +0 -0
  170. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_errors.py +0 -0
  171. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_http.py +0 -0
  172. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_invariants.py +0 -0
  173. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_langchain.py +0 -0
  174. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_lifecycle.py +0 -0
  175. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_limits.py +0 -0
  176. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_packaging.py +0 -0
  177. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_paywall.py +0 -0
  178. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_webhook_events.py +0 -0
  179. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/test_webhooks.py +0 -0
  180. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/webhooks/test_clock_skew_vs_replay_ttl.py +0 -0
  181. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/webhooks/test_seen_cache_atomic.py +0 -0
  182. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tests/webhooks/test_webhook_pipeline.py +0 -0
  183. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tools/api_baseline.json +0 -0
  184. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tools/api_diff.py +0 -0
  185. {solvapay_python-0.8.0 → solvapay_python-0.9.1}/tools/importlinter.cfg +0 -0
@@ -0,0 +1,18 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ open-pull-requests-limit: 5
8
+ labels:
9
+ - dependencies
10
+
11
+ - package-ecosystem: github-actions
12
+ directory: /
13
+ schedule:
14
+ interval: weekly
15
+ open-pull-requests-limit: 5
16
+ labels:
17
+ - dependencies
18
+ - ci
@@ -0,0 +1,81 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ schedule:
8
+ - cron: "0 7 * * *" # nightly security sweep
9
+
10
+ jobs:
11
+ security:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+ - uses: astral-sh/setup-uv@v7
16
+ with:
17
+ enable-cache: true
18
+ - run: uv python install 3.12
19
+ - run: uv sync --all-extras --dev
20
+ - name: bandit (high-severity, high-confidence only)
21
+ run: uv run bandit -r src/ -lll
22
+ - name: Install osv-scanner
23
+ run: |
24
+ curl -fsSL -o /tmp/osv-scanner.tar.gz \
25
+ https://github.com/google/osv-scanner/releases/download/v1.9.2/osv-scanner_linux_amd64.tar.gz
26
+ mkdir -p /tmp/osv && tar -xzf /tmp/osv-scanner.tar.gz -C /tmp/osv
27
+ sudo mv /tmp/osv/osv-scanner /usr/local/bin/osv-scanner
28
+ osv-scanner --version
29
+ - name: osv-scanner (lockfile vuln DB cross-check)
30
+ run: osv-scanner --lockfile=uv.lock
31
+
32
+
33
+ test:
34
+ strategy:
35
+ matrix:
36
+ include:
37
+ # Linux — full Python matrix
38
+ - os: ubuntu-latest
39
+ python-version: "3.10"
40
+ - os: ubuntu-latest
41
+ python-version: "3.11"
42
+ - os: ubuntu-latest
43
+ python-version: "3.12"
44
+ # macOS — 3.12 only
45
+ - os: macos-latest
46
+ python-version: "3.12"
47
+ # Windows — 3.12 only
48
+ - os: windows-latest
49
+ python-version: "3.12"
50
+ runs-on: ${{ matrix.os }}
51
+ steps:
52
+ - uses: actions/checkout@v6
53
+ with:
54
+ fetch-depth: 0
55
+ - uses: astral-sh/setup-uv@v7
56
+ with:
57
+ enable-cache: true
58
+ - name: Set Python ${{ matrix.python-version }}
59
+ run: uv python install ${{ matrix.python-version }}
60
+ - run: uv sync --all-extras --dev
61
+ - run: uv run ruff check src tests
62
+ - run: uv run ruff format --check src tests
63
+ - run: uv run mypy src
64
+ - run: uv run pytest -v
65
+ - run: uv run python tools/api_diff.py
66
+ - run: uv run lint-imports --config tools/importlinter.cfg
67
+ - run: uv run python tools/lint_invariants.py
68
+ - name: pip-audit
69
+ run: uv run pip-audit
70
+ - name: Check changelog fragment on PRs touching src/
71
+ if: github.event_name == 'pull_request'
72
+ shell: bash
73
+ run: |
74
+ git fetch origin ${{ github.base_ref }}
75
+ changed=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
76
+ if echo "$changed" | grep -q "^src/"; then
77
+ if ! echo "$changed" | grep -q "^changelog.d/"; then
78
+ echo "ERROR: src/ modified without a changelog.d/ fragment. Add one — see changelog.d/README.md"
79
+ exit 1
80
+ fi
81
+ fi
@@ -0,0 +1,22 @@
1
+ name: contract
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 2 * * *" # nightly at 02:00 UTC
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ contract:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v6
13
+ - uses: astral-sh/setup-uv@v7
14
+ with:
15
+ enable-cache: true
16
+ - run: uv sync --all-extras --dev
17
+ - name: Run contract tests
18
+ run: uv run pytest tests/contract/ -m contract -v
19
+ env:
20
+ SOLVAPAY_SANDBOX_KEY: ${{ secrets.SOLVAPAY_SANDBOX_KEY }}
21
+ SOLVAPAY_TEST_CUSTOMER_REF: ${{ secrets.SOLVAPAY_TEST_CUSTOMER_REF }}
22
+ SOLVAPAY_TEST_PRODUCT_REF: ${{ secrets.SOLVAPAY_TEST_PRODUCT_REF }}
@@ -0,0 +1,21 @@
1
+ name: docs
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v6
16
+ - uses: astral-sh/setup-uv@v7
17
+ with:
18
+ enable-cache: true
19
+ - run: uv sync --group docs
20
+ - name: Deploy to GitHub Pages
21
+ run: uv run mkdocs gh-deploy --force
@@ -0,0 +1,32 @@
1
+ name: publish
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+ jobs:
6
+ publish:
7
+ runs-on: ubuntu-latest
8
+ environment: pypi
9
+ permissions:
10
+ id-token: write
11
+ contents: write
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+ - uses: astral-sh/setup-uv@v7
15
+ - run: uv build
16
+ - name: Generate CycloneDX SBOM
17
+ run: |
18
+ # Resolve runtime + all-extras deps from the lockfile and emit a CycloneDX SBOM.
19
+ uv export --no-dev --all-extras --no-emit-project --format requirements-txt > /tmp/sbom-requirements.txt
20
+ uv tool run --from cyclonedx-bom cyclonedx-py requirements \
21
+ --pyproject pyproject.toml \
22
+ --of JSON --sv 1.6 \
23
+ -o sbom.cdx.json \
24
+ /tmp/sbom-requirements.txt
25
+ - name: Publish to PyPI (Sigstore-signed via trusted publishing)
26
+ uses: pypa/gh-action-pypi-publish@release/v1
27
+ with:
28
+ attestations: true
29
+ - name: Attach SBOM to GitHub release
30
+ env:
31
+ GH_TOKEN: ${{ github.token }}
32
+ run: gh release upload "${GITHUB_REF_NAME}" sbom.cdx.json --clobber
@@ -12,3 +12,5 @@ htmlcov/
12
12
  .env
13
13
  .env.local
14
14
  *.egg
15
+ .DS_Store
16
+ site/
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.1] — 2026-06-05
4
+
5
+ Security & supply-chain quality patch (no public API change).
6
+
7
+ ### Features
8
+
9
+ - ``publish.yml``: PyPI uploads now ship PEP 740 attestations (Sigstore-backed via OIDC trusted publishing) and attach a CycloneDX SBOM (``sbom.cdx.json``) to each GitHub release.
10
+ - CI: new ``security`` job runs ``bandit -r src/ -lll`` (high-confidence high-severity only) and ``osv-scanner --lockfile=uv.lock`` as a cross-DB check against ``pip-audit``. Runs on PR and nightly cron.
11
+ - ``tests/test_redaction.py``: property-based test with Hypothesis (100 derandomized cases) drives random API keys, hex tokens, and webhook secrets through the HTTP transport and asserts the secret never appears in ``caplog.text`` at any log level.
12
+ - ``tests/test_webhook_timing.py``: smoke test that compares ``verify_webhook`` perf-counter distributions for perfect-match vs near-miss signatures, regressing if someone swaps ``hmac.compare_digest`` for ``==``.
13
+
14
+ ### Bug Fixes
15
+
16
+ - ``pyproject.toml``: tighten ``httpx`` to ``<0.29`` (closes deviation: was unbounded). FastAPI extra remains unbounded by design — documented in-file.
17
+
18
+ ### Documentation
19
+
20
+ - ``SECURITY.md``: explicit PCI-scope section — the SDK never transmits raw cardholder data; tokenization is server-side.
21
+
22
+ ---
23
+
24
+ ## 0.9.0 — 2026-05-23
25
+
26
+ Production polish + C1 adversarial-review closure. API-version pinning, idempotency TTL, webhook secret rotation, ASGI adapter, retry/recording transports, contract tests, lint automation, doc site, supply-chain hardening.
27
+
28
+ ### Added
29
+ - **API-version pinning**: `SolvaPay(api_version="2026-05-22")` and `AsyncSolvaPay(api_version=...)` send `Solvapay-Version` header. Default pinned to `"2026-05-22"`. `api_version=None` omits the header. (HLD V1.13)
30
+ - **Idempotency time-bucket encoding**: `from_payload(*parts, time_bucket="day"|"hour"|None)`. Default `"day"` appends UTC date so keys roll at midnight, bounding replay ambiguity past server TTL. (HLD V1.14)
31
+ - **Webhook secret rotation**: `MultiSecretVerifier` full implementation — tries primary then secondary on signature mismatch; both comparisons constant-time. (HLD V1.7)
32
+ - **`AsyncInMemorySeenEventCache`**: async replay-dedup cache using `asyncio.Lock` for async webhook pipelines. (HLD V1.7)
33
+ - **`sign_webhook(body, secret, *, timestamp)`**: public helper to produce a valid `sv-signature` header for testing and outbound webhook fanout.
34
+ - **ASGI webhook adapter** (`solvapay[asgi]`): `webhook_app(pipeline, on_event, path)` returns a raw ASGI app mountable in Starlette, FastAPI, Litestar, or BlackSheep. (HLD V1.10)
35
+ - **`RetryTransport`**: middleware that retries `APIConnectionError`, `APITimeoutError`, `APIServerError`, `RateLimitError` — exponential backoff with jitter, max 3 attempts. Consults `OpSpec.retry_safety`; refuses to retry `NEVER` ops. (HLD V1.4, `solvapay[retry]`)
36
+ - **`RecordingTransport`**: records `(RequestSpec, ResponseSpec)` pairs to JSON cassettes; replays on subsequent runs. Enables offline contract tests. (HLD V1.4)
37
+ - **Sandbox contract tests** (`tests/contract/`): one test per op against real sandbox via `RecordingTransport`. Nightly CI workflow (`contract.yml`).
38
+ - **Lint invariants** (`tools/lint_invariants.py`): AST checks for 10 gotchas (future annotations, `hmac.compare_digest`, no `logging.basicConfig`, no `asyncio.run()`, alias discipline, etc.). Run in CI on every push.
39
+ - **Towncrier changelog automation**: `changelog.d/` fragments, PR gate requiring a fragment when `src/` changes, `towncrier>=23` dev dep.
40
+ - **MkDocs Material doc site**: `mkdocs.yml` + `docs/` skeleton (Quickstart, Reference, Architecture, Guides, Errors, Idempotency, Webhooks, Migration). Deploys to GitHub Pages on tag. (HLD V1.12)
41
+ - **Dependabot**: weekly Python + GitHub Actions dependency updates (`.github/dependabot.yml`).
42
+ - **`pip-audit` in CI**: dependency CVE scan on every push.
43
+ - **CI matrix expanded**: Ubuntu 3.10/3.11/3.12 + macOS 3.12 + Windows 3.12. (HLD V1.15)
44
+ - **`ResourceWarning` on unclosed `AsyncSolvaPay`**: `__del__` emits `ResourceWarning` if client is not closed and event loop is still running.
45
+ - **OpenAPI investigation RFC**: `docs/rfcs/0002-openapi-investigation.md`.
46
+
47
+ ### Changed
48
+ - `pytest.ini_options.filterwarnings`: adds `ignore::DeprecationWarning:solvapay` so tests exercising deprecated flat-shim API do not fail on expected warnings.
49
+
50
+ ---
51
+
3
52
  ## 0.8.0 — 2026-05-23
4
53
 
5
54
  V1 architecture spine + AI-agent moat. 10 atomic commits establishing locked architecture invariants per HLD §V1.1–V1.19.
@@ -30,13 +30,26 @@ The SDK has a strict layer hierarchy — see [docs/architecture/layers.md](docs/
30
30
 
31
31
  CI fails on violations via `import-linter`.
32
32
 
33
+ ## Changelog fragment (required)
34
+
35
+ Every PR that modifies `src/` must include a towncrier fragment in `changelog.d/`.
36
+
37
+ ```bash
38
+ # Create a fragment (replace 42 with your issue/PR number)
39
+ echo "Add RetryTransport middleware for automatic retry on transient errors." > changelog.d/42.feature
40
+ ```
41
+
42
+ See `changelog.d/README.md` for naming conventions and types.
43
+
44
+ PRs that touch `src/` without a `changelog.d/` entry will fail CI.
45
+
33
46
  ## PR checklist
34
47
 
35
48
  - [ ] Tests added for new behavior
36
- - [ ] `CHANGELOG.md` updated
49
+ - [ ] `changelog.d/<issue>.<type>` fragment added (required for `src/` changes)
37
50
  - [ ] `docs/` updated if public API changed
38
51
  - [ ] Layer DAG respected (`uv run lint-imports` passes)
39
- - [ ] Stability manifest updated if new public exports added
52
+ - [ ] Stability manifest updated if new public exports added (`uv run python tools/api_diff.py`)
40
53
 
41
54
  ## Commit style
42
55
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solvapay-python
3
- Version: 0.8.0
3
+ Version: 0.9.1
4
4
  Summary: Community Python SDK for SolvaPay (agent-native payment rails)
5
5
  Project-URL: Homepage, https://github.com/dhruv-sanan/solvapay-python
6
6
  Project-URL: Issues, https://github.com/dhruv-sanan/solvapay-python/issues
@@ -21,14 +21,17 @@ Classifier: Topic :: Office/Business :: Financial
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
- Requires-Dist: httpx>=0.27
25
- Requires-Dist: pydantic<2.13,>=2.6
24
+ Requires-Dist: httpx<0.29,>=0.27
25
+ Requires-Dist: pydantic<2.14,>=2.6
26
+ Provides-Extra: asgi
26
27
  Provides-Extra: fastapi
27
28
  Requires-Dist: fastapi>=0.110; extra == 'fastapi'
28
29
  Provides-Extra: langchain
29
- Requires-Dist: langchain-core<0.4,>=0.3; extra == 'langchain'
30
+ Requires-Dist: langchain-core<1.5,>=0.3; extra == 'langchain'
30
31
  Provides-Extra: mcp
31
- Requires-Dist: fastmcp<0.5,>=0.4; extra == 'mcp'
32
+ Requires-Dist: fastmcp<4,>=3.2.0; extra == 'mcp'
33
+ Provides-Extra: retry
34
+ Requires-Dist: tenacity<10,>=8.2; extra == 'retry'
32
35
  Description-Content-Type: text/markdown
33
36
 
34
37
  # solvapay-python
@@ -135,16 +138,23 @@ if limits.within_limits:
135
138
 
136
139
  ## Idempotency
137
140
 
138
- All mutating ops accept `idempotency_key`. Use `solvapay.idempotency.from_payload` to derive deterministic keys from request content:
141
+ All mutating ops accept `idempotency_key`. Use `solvapay.idempotency.from_payload` to derive deterministic keys:
139
142
 
140
143
  ```python
141
144
  from solvapay.idempotency import from_payload
142
145
 
146
+ # Default: key includes UTC date — rolls at midnight, bounds replay past server TTL
143
147
  key = from_payload("track_usage", customer_ref, product_ref, "requests", units)
144
148
  sv.usage.track(..., idempotency_key=key) # retry-safe
149
+
150
+ # Hourly bucket (high-frequency ops)
151
+ key = from_payload("charge", customer_ref, time_bucket="hour")
152
+
153
+ # Pure payload hash — caller manages TTL externally
154
+ key = from_payload("idempotent_op", ref, time_bucket=None)
145
155
  ```
146
156
 
147
- Retried POSTs **must reuse the same key**. Key should change only when the logical request changes.
157
+ Retried POSTs **must reuse the same key**. A bucket roll (midnight / hour boundary) produces a new key — the server treats it as a new request.
148
158
 
149
159
  ---
150
160
 
@@ -171,7 +181,7 @@ except SolvaPayError as e:
171
181
  ... # catch-all
172
182
  ```
173
183
 
174
- No built-in retries by design. Layer `tenacity` manually. `solvapay[retry]` RetryTransport ships in v0.9.
184
+ No built-in retries by default. `solvapay[retry]` ships `RetryTransport` exponential backoff with jitter, 3 attempts, respects `OpSpec.retry_safety` (won't retry non-idempotent ops without an idempotency key). Or layer `tenacity` manually.
175
185
 
176
186
  ---
177
187
 
@@ -190,6 +200,29 @@ pipeline = WebhookPipeline(
190
200
  envelope = pipeline.process(body=request.body, signature=request.headers["sv-signature"])
191
201
  ```
192
202
 
203
+ **Secret rotation** — pass multiple secrets; primary tried first, secondary on mismatch:
204
+
205
+ ```python
206
+ pipeline = WebhookPipeline(["whsec_new...", "whsec_old..."])
207
+ ```
208
+
209
+ **Sign a webhook** (testing / outbound fanout):
210
+
211
+ ```python
212
+ from solvapay.webhooks import sign_webhook
213
+
214
+ header = sign_webhook(body=b'{"type":"purchase.created"}', secret="whsec_...")
215
+ # → "t=1716470000,v1=abc123..."
216
+ ```
217
+
218
+ **ASGI adapter** — mount directly in FastAPI / Starlette / Litestar:
219
+
220
+ ```python
221
+ from solvapay.adapters.asgi import webhook_app
222
+
223
+ app.mount("/webhook", webhook_app(pipeline, on_event=handle))
224
+ ```
225
+
193
226
  **Typed events** — discriminated union over 13 event types:
194
227
 
195
228
  ```python
@@ -276,6 +309,8 @@ pip install solvapay-python # core
276
309
  pip install 'solvapay-python[mcp]' # + FastMCP adapter (FastMCP ≥0.4)
277
310
  pip install 'solvapay-python[langchain]' # + LangChain adapter (langchain-core ≥0.3)
278
311
  pip install 'solvapay-python[fastapi]' # + FastAPI webhook router
312
+ pip install 'solvapay-python[asgi]' # + raw ASGI webhook adapter (no extra deps)
313
+ pip install 'solvapay-python[retry]' # + RetryTransport (tenacity)
279
314
  ```
280
315
 
281
316
  ## Environment variables
@@ -299,17 +334,40 @@ pip install 'solvapay-python[fastapi]' # + FastAPI webhook router
299
334
 
300
335
  ---
301
336
 
337
+ ## API version pinning
338
+
339
+ Pin the API version your code was written against — prevents silent breakage when the server evolves:
340
+
341
+ ```python
342
+ sv = SolvaPay(api_version="2026-05-22") # sends Solvapay-Version header
343
+ sv = SolvaPay(api_version=None) # omit header (use server default)
344
+ ```
345
+
346
+ Default is `"2026-05-22"` (v0.9 ship date). Bump only on major SDK versions.
347
+
348
+ ---
349
+
302
350
  ## Roadmap
303
351
 
304
352
  | Version | Theme |
305
353
  |---------|-------|
306
354
  | v0.8 ✅ | V1 architecture spine — Transport kernel, OpSpec registry, paywall/webhook packages, `@payable_tool`, stability manifest, layer DAG CI gate |
307
- | v0.9 | Production polish — API-version pinning, idempotency TTL, `RetryTransport`, `RecordingTransport`, contract tests, lint automation, doc site (MkDocs Material), supply-chain hygiene |
308
- | v1.0 | Gated on founder signalOpenAPI-generated models, full secret rotation, production hardening |
355
+ | v0.9 | Production polish — API-version pinning, idempotency TTL, `RetryTransport`, `RecordingTransport`, ASGI adapter, secret rotation, `sign_webhook`, contract tests, lint automation, MkDocs site, supply-chain hygiene |
356
+ | v0.9.1 | Security & supply-chain qualityPyPI attestations (PEP 740 / Sigstore), CycloneDX SBOM on releases, `bandit` + `osv-scanner` CI, Hypothesis-driven secret-redaction property tests, constant-time `verify_webhook` smoke test, explicit PCI-scope statement |
357
+ | v1.0 | Gated on founder signal — OpenAPI-generated models, WSGI/Lambda adapters, V2 planning |
309
358
 
310
359
  ---
311
360
 
312
361
  ## Status
313
362
 
314
- **v0.8.0** — V1 architecture spine + AI-agent moat. `mypy --strict` clean (43 files). 261 tests. 89% line coverage.
363
+ **v0.9.1** — Security & supply-chain quality patch. PyPI uploads now carry PEP 740 attestations (Sigstore-backed via OIDC trusted publishing); each GitHub release ships a CycloneDX SBOM (`sbom.cdx.json`). `mypy --strict` clean (45 files). 302 tests. 87% line / 85% branch coverage.
364
+
365
+ **Supply chain & security posture:**
366
+ - PyPI trusted publishing with PEP 740 attestations (Sigstore)
367
+ - CycloneDX 1.6 SBOM attached to every release
368
+ - CI runs `bandit -r src/ -lll`, `osv-scanner --lockfile=uv.lock`, and `pip-audit` (cross-DB vuln check) on PR and nightly cron
369
+ - Constant-time HMAC comparison (`hmac.compare_digest`) regression-smoked
370
+ - Hypothesis-driven property tests assert no API key, hex token, or webhook secret leaks into log output at any level
371
+ - Tightened upper bounds on volatile deps (`httpx<0.29`, `pydantic<2.14`, `langchain-core<1.5`)
372
+
315
373
  Community SDK, not official. Proposal: [solvapay/solvapay-sdk#187](https://github.com/solvapay/solvapay-sdk/issues/187).
@@ -102,16 +102,23 @@ if limits.within_limits:
102
102
 
103
103
  ## Idempotency
104
104
 
105
- All mutating ops accept `idempotency_key`. Use `solvapay.idempotency.from_payload` to derive deterministic keys from request content:
105
+ All mutating ops accept `idempotency_key`. Use `solvapay.idempotency.from_payload` to derive deterministic keys:
106
106
 
107
107
  ```python
108
108
  from solvapay.idempotency import from_payload
109
109
 
110
+ # Default: key includes UTC date — rolls at midnight, bounds replay past server TTL
110
111
  key = from_payload("track_usage", customer_ref, product_ref, "requests", units)
111
112
  sv.usage.track(..., idempotency_key=key) # retry-safe
113
+
114
+ # Hourly bucket (high-frequency ops)
115
+ key = from_payload("charge", customer_ref, time_bucket="hour")
116
+
117
+ # Pure payload hash — caller manages TTL externally
118
+ key = from_payload("idempotent_op", ref, time_bucket=None)
112
119
  ```
113
120
 
114
- Retried POSTs **must reuse the same key**. Key should change only when the logical request changes.
121
+ Retried POSTs **must reuse the same key**. A bucket roll (midnight / hour boundary) produces a new key — the server treats it as a new request.
115
122
 
116
123
  ---
117
124
 
@@ -138,7 +145,7 @@ except SolvaPayError as e:
138
145
  ... # catch-all
139
146
  ```
140
147
 
141
- No built-in retries by design. Layer `tenacity` manually. `solvapay[retry]` RetryTransport ships in v0.9.
148
+ No built-in retries by default. `solvapay[retry]` ships `RetryTransport` exponential backoff with jitter, 3 attempts, respects `OpSpec.retry_safety` (won't retry non-idempotent ops without an idempotency key). Or layer `tenacity` manually.
142
149
 
143
150
  ---
144
151
 
@@ -157,6 +164,29 @@ pipeline = WebhookPipeline(
157
164
  envelope = pipeline.process(body=request.body, signature=request.headers["sv-signature"])
158
165
  ```
159
166
 
167
+ **Secret rotation** — pass multiple secrets; primary tried first, secondary on mismatch:
168
+
169
+ ```python
170
+ pipeline = WebhookPipeline(["whsec_new...", "whsec_old..."])
171
+ ```
172
+
173
+ **Sign a webhook** (testing / outbound fanout):
174
+
175
+ ```python
176
+ from solvapay.webhooks import sign_webhook
177
+
178
+ header = sign_webhook(body=b'{"type":"purchase.created"}', secret="whsec_...")
179
+ # → "t=1716470000,v1=abc123..."
180
+ ```
181
+
182
+ **ASGI adapter** — mount directly in FastAPI / Starlette / Litestar:
183
+
184
+ ```python
185
+ from solvapay.adapters.asgi import webhook_app
186
+
187
+ app.mount("/webhook", webhook_app(pipeline, on_event=handle))
188
+ ```
189
+
160
190
  **Typed events** — discriminated union over 13 event types:
161
191
 
162
192
  ```python
@@ -243,6 +273,8 @@ pip install solvapay-python # core
243
273
  pip install 'solvapay-python[mcp]' # + FastMCP adapter (FastMCP ≥0.4)
244
274
  pip install 'solvapay-python[langchain]' # + LangChain adapter (langchain-core ≥0.3)
245
275
  pip install 'solvapay-python[fastapi]' # + FastAPI webhook router
276
+ pip install 'solvapay-python[asgi]' # + raw ASGI webhook adapter (no extra deps)
277
+ pip install 'solvapay-python[retry]' # + RetryTransport (tenacity)
246
278
  ```
247
279
 
248
280
  ## Environment variables
@@ -266,17 +298,40 @@ pip install 'solvapay-python[fastapi]' # + FastAPI webhook router
266
298
 
267
299
  ---
268
300
 
301
+ ## API version pinning
302
+
303
+ Pin the API version your code was written against — prevents silent breakage when the server evolves:
304
+
305
+ ```python
306
+ sv = SolvaPay(api_version="2026-05-22") # sends Solvapay-Version header
307
+ sv = SolvaPay(api_version=None) # omit header (use server default)
308
+ ```
309
+
310
+ Default is `"2026-05-22"` (v0.9 ship date). Bump only on major SDK versions.
311
+
312
+ ---
313
+
269
314
  ## Roadmap
270
315
 
271
316
  | Version | Theme |
272
317
  |---------|-------|
273
318
  | v0.8 ✅ | V1 architecture spine — Transport kernel, OpSpec registry, paywall/webhook packages, `@payable_tool`, stability manifest, layer DAG CI gate |
274
- | v0.9 | Production polish — API-version pinning, idempotency TTL, `RetryTransport`, `RecordingTransport`, contract tests, lint automation, doc site (MkDocs Material), supply-chain hygiene |
275
- | v1.0 | Gated on founder signalOpenAPI-generated models, full secret rotation, production hardening |
319
+ | v0.9 | Production polish — API-version pinning, idempotency TTL, `RetryTransport`, `RecordingTransport`, ASGI adapter, secret rotation, `sign_webhook`, contract tests, lint automation, MkDocs site, supply-chain hygiene |
320
+ | v0.9.1 | Security & supply-chain qualityPyPI attestations (PEP 740 / Sigstore), CycloneDX SBOM on releases, `bandit` + `osv-scanner` CI, Hypothesis-driven secret-redaction property tests, constant-time `verify_webhook` smoke test, explicit PCI-scope statement |
321
+ | v1.0 | Gated on founder signal — OpenAPI-generated models, WSGI/Lambda adapters, V2 planning |
276
322
 
277
323
  ---
278
324
 
279
325
  ## Status
280
326
 
281
- **v0.8.0** — V1 architecture spine + AI-agent moat. `mypy --strict` clean (43 files). 261 tests. 89% line coverage.
327
+ **v0.9.1** — Security & supply-chain quality patch. PyPI uploads now carry PEP 740 attestations (Sigstore-backed via OIDC trusted publishing); each GitHub release ships a CycloneDX SBOM (`sbom.cdx.json`). `mypy --strict` clean (45 files). 302 tests. 87% line / 85% branch coverage.
328
+
329
+ **Supply chain & security posture:**
330
+ - PyPI trusted publishing with PEP 740 attestations (Sigstore)
331
+ - CycloneDX 1.6 SBOM attached to every release
332
+ - CI runs `bandit -r src/ -lll`, `osv-scanner --lockfile=uv.lock`, and `pip-audit` (cross-DB vuln check) on PR and nightly cron
333
+ - Constant-time HMAC comparison (`hmac.compare_digest`) regression-smoked
334
+ - Hypothesis-driven property tests assert no API key, hex token, or webhook secret leaks into log output at any level
335
+ - Tightened upper bounds on volatile deps (`httpx<0.29`, `pydantic<2.14`, `langchain-core<1.5`)
336
+
282
337
  Community SDK, not official. Proposal: [solvapay/solvapay-sdk#187](https://github.com/solvapay/solvapay-sdk/issues/187).
@@ -13,8 +13,17 @@ This is a **community** Python SDK. It is not an official SolvaPay product.
13
13
  - SolvaPay server-side vulnerabilities
14
14
  - Issues requiring a SolvaPay account to reproduce
15
15
 
16
- **PCI scope:** The SDK never transmits raw cardholder data (PAN, CVV, expiry).
17
- It only exchanges SolvaPay API keys and customer references over HTTPS.
16
+ ## PCI Scope
17
+
18
+ The SDK **never transmits raw cardholder data (PAN, CVV, expiry, track data)**.
19
+ All tokenization happens **server-side** at the SolvaPay API; the SDK only
20
+ exchanges SolvaPay API keys, customer references, and tokenized identifiers
21
+ over HTTPS. Integrators using this SDK do not bring PAN data into their
22
+ own application scope through it.
23
+
24
+ If you discover any code path that would cause raw cardholder data to traverse
25
+ the SDK boundary, treat it as a high-severity vulnerability and report it via
26
+ the channel below.
18
27
 
19
28
  ## Reporting a Vulnerability
20
29
 
@@ -25,6 +34,9 @@ Please include:
25
34
  - Steps to reproduce
26
35
  - Any proof-of-concept code (privately)
27
36
 
37
+ Encrypt sensitive payloads with the maintainer's public key on request.
38
+ Do **not** open public GitHub issues for suspected vulnerabilities.
39
+
28
40
  **Response SLA:** Best-effort; typically within 7 days.
29
41
  **Disclosure policy:** 90-day coordinated disclosure. Public CVE filed only for confirmed SDK bugs.
30
42
 
@@ -32,5 +44,6 @@ Please include:
32
44
 
33
45
  | Version | Supported |
34
46
  |---------|-----------|
47
+ | 0.9.x | Yes |
35
48
  | 0.8.x | Yes |
36
49
  | < 0.8 | No — upgrade recommended |
@@ -0,0 +1,35 @@
1
+ # Changelog Fragments
2
+
3
+ This directory contains towncrier changelog fragments. Each file represents one change entry.
4
+
5
+ ## Fragment naming
6
+
7
+ ```
8
+ <issue_or_pr_number>.<type>
9
+ ```
10
+
11
+ **Types:** `feature`, `bugfix`, `doc`, `removal`
12
+
13
+ **Examples:**
14
+ ```
15
+ 42.feature → "Add RetryTransport middleware"
16
+ 55.bugfix → "Fix idempotency key not sent on retry"
17
+ 60.doc → "Document api_version pinning"
18
+ 99.removal → "Remove deprecated SolvaPayAPIError alias"
19
+ ```
20
+
21
+ ## Required for PRs
22
+
23
+ Any PR that touches `src/` **must** include at least one fragment. PRs without a fragment will fail CI.
24
+
25
+ Example fragment content:
26
+ ```
27
+ Add ``api_version`` kwarg to ``SolvaPay`` and ``AsyncSolvaPay`` for server-side API version pinning.
28
+ ```
29
+
30
+ ## Building the changelog
31
+
32
+ ```bash
33
+ uv run towncrier build --version 0.9.0 --draft # preview
34
+ uv run towncrier build --version 0.9.0 # write to CHANGELOG.md
35
+ ```
@@ -0,0 +1,50 @@
1
+ # Errors
2
+
3
+ ```
4
+ SolvaPayError
5
+ ├── APIError(status_code, body, request_id, error_code, error_message)
6
+ │ ├── AuthenticationError # 401
7
+ │ ├── PermissionError # 403
8
+ │ ├── NotFoundError # 404
9
+ │ ├── RateLimitError(retry_after) # 429
10
+ │ ├── InvalidRequestError # other 4xx
11
+ │ └── APIServerError # 5xx
12
+ ├── APIConnectionError # network failure
13
+ ├── APITimeoutError # timeout
14
+ └── PaywallRequired # paywall gate hit
15
+ .checkout_url: str | None
16
+ .checkout_mint_error: APIError | None
17
+ ```
18
+
19
+ ## Handling errors
20
+
21
+ ```python
22
+ from solvapay import SolvaPay
23
+ from solvapay.exceptions import (
24
+ AuthenticationError,
25
+ RateLimitError,
26
+ APIConnectionError,
27
+ PaywallRequired,
28
+ )
29
+
30
+ sv = SolvaPay()
31
+
32
+ try:
33
+ sv.limits.check(customer_ref="cus_1", product_ref="prd_1")
34
+ except AuthenticationError:
35
+ print("Invalid API key")
36
+ except RateLimitError as e:
37
+ print(f"Rate limited — retry after {e.retry_after}s")
38
+ except APIConnectionError:
39
+ print("Network failure — safe to retry with idempotency key")
40
+ except PaywallRequired as e:
41
+ print(f"Paywall hit. Checkout: {e.checkout_url}")
42
+ ```
43
+
44
+ ## `request_id`
45
+
46
+ Every `APIError` captures `request_id` from `x-request-id` or `x-correlation-id` response headers. Include this in support tickets.
47
+
48
+ ## Legacy alias
49
+
50
+ `SolvaPayAPIError` is an alias for `APIError`. It emits `DeprecationWarning` and will be removed in v2.0. Use `APIError` directly.