svc-infra 0.1.597__tar.gz → 0.1.598__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.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

Files changed (274) hide show
  1. {svc_infra-0.1.597 → svc_infra-0.1.598}/PKG-INFO +1 -1
  2. {svc_infra-0.1.597 → svc_infra-0.1.598}/pyproject.toml +2 -1
  3. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/inbox.py +12 -0
  4. svc_infra-0.1.598/src/svc_infra/jobs/builtins/webhook_delivery.py +78 -0
  5. svc_infra-0.1.598/src/svc_infra/webhooks/__init__.py +1 -0
  6. svc_infra-0.1.598/src/svc_infra/webhooks/fastapi.py +37 -0
  7. svc_infra-0.1.598/src/svc_infra/webhooks/router.py +55 -0
  8. svc_infra-0.1.598/src/svc_infra/webhooks/service.py +59 -0
  9. svc_infra-0.1.598/src/svc_infra/webhooks/signing.py +30 -0
  10. svc_infra-0.1.597/src/svc_infra/jobs/builtins/webhook_delivery.py +0 -59
  11. {svc_infra-0.1.597 → svc_infra-0.1.598}/README.md +0 -0
  12. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/__init__.py +0 -0
  13. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/README.md +0 -0
  14. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/__init__.py +0 -0
  15. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/alembic.py +0 -0
  16. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/models.py +0 -0
  17. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
  18. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
  19. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/provider/base.py +0 -0
  20. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/provider/registry.py +0 -0
  21. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
  22. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/schemas.py +0 -0
  23. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/service.py +0 -0
  24. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/apf_payments/settings.py +0 -0
  25. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/__init__.py +0 -0
  26. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/__init__.py +0 -0
  27. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
  28. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
  29. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
  30. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
  31. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
  32. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/add.py +0 -0
  33. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/gaurd.py +0 -0
  34. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
  35. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
  36. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
  37. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
  38. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
  39. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
  40. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
  41. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
  42. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
  43. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
  44. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
  45. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
  46. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
  47. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
  48. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/security.py +0 -0
  49. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
  50. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
  51. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/auth/state.py +0 -0
  52. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
  53. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/cache/add.py +0 -0
  54. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
  55. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/http.py +0 -0
  56. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
  57. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
  58. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
  59. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
  60. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
  61. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
  62. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
  63. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/sql/add.py +0 -0
  64. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -0
  65. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
  66. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
  67. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
  68. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -0
  69. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
  70. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
  71. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
  72. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
  73. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
  74. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
  75. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/dual/public.py +0 -0
  76. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/dual/router.py +0 -0
  77. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
  78. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/dx.py +0 -0
  79. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/ease.py +0 -0
  80. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
  81. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
  82. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
  83. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
  84. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
  85. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
  86. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
  87. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
  88. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
  89. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
  90. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
  91. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
  92. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
  93. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/ratelimit.py +0 -0
  94. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +0 -0
  95. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
  96. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
  97. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
  98. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
  99. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
  100. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
  101. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
  102. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
  103. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
  104. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
  105. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/pagination.py +0 -0
  106. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
  107. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
  108. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
  109. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
  110. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/paths/user.py +0 -0
  111. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
  112. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
  113. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/api/fastapi/setup.py +0 -0
  114. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/app/README.md +0 -0
  115. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/app/__init__.py +0 -0
  116. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/app/env.py +0 -0
  117. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/app/logging/__init__.py +0 -0
  118. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/app/logging/add.py +0 -0
  119. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/app/logging/filter.py +0 -0
  120. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/app/logging/formats.py +0 -0
  121. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/app/root.py +0 -0
  122. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/README.md +0 -0
  123. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/__init__.py +0 -0
  124. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/backend.py +0 -0
  125. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/decorators.py +0 -0
  126. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/demo.py +0 -0
  127. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/keys.py +0 -0
  128. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/recache.py +0 -0
  129. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/resources.py +0 -0
  130. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/tags.py +0 -0
  131. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/ttl.py +0 -0
  132. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cache/utils.py +0 -0
  133. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/__init__.py +0 -0
  134. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/__main__.py +0 -0
  135. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/__init__.py +0 -0
  136. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
  137. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
  138. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
  139. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
  140. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
  141. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
  142. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
  143. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
  144. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
  145. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/help.py +0 -0
  146. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
  147. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
  148. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
  149. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
  150. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/foundation/__init__.py +0 -0
  151. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/foundation/runner.py +0 -0
  152. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
  153. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/__init__.py +0 -0
  154. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/crud_schema.py +0 -0
  155. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/__init__.py +0 -0
  156. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/base.py +0 -0
  157. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/constants.py +0 -0
  158. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/core.py +0 -0
  159. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/indexes.py +0 -0
  160. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/management.py +0 -0
  161. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/mongo/README.md +0 -0
  162. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
  163. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/mongo/client.py +0 -0
  164. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
  165. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
  166. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
  167. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
  168. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
  169. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/repository.py +0 -0
  170. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/resource.py +0 -0
  171. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/scaffold.py +0 -0
  172. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/service.py +0 -0
  173. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
  174. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/types.py +0 -0
  175. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/nosql/utils.py +0 -0
  176. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/outbox.py +0 -0
  177. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/README.md +0 -0
  178. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/__init__.py +0 -0
  179. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/apikey.py +0 -0
  180. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/authref.py +0 -0
  181. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/base.py +0 -0
  182. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/constants.py +0 -0
  183. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/core.py +0 -0
  184. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/management.py +0 -0
  185. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/repository.py +0 -0
  186. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/resource.py +0 -0
  187. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/scaffold.py +0 -0
  188. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/service.py +0 -0
  189. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
  190. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/__init__.py +0 -0
  191. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
  192. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
  193. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
  194. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
  195. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
  196. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
  197. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
  198. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
  199. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
  200. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
  201. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/types.py +0 -0
  202. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/uniq.py +0 -0
  203. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
  204. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/utils.py +0 -0
  205. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/sql/versioning.py +0 -0
  206. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/db/utils.py +0 -0
  207. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
  208. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/jobs/easy.py +0 -0
  209. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/jobs/loader.py +0 -0
  210. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/jobs/queue.py +0 -0
  211. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/jobs/redis_queue.py +0 -0
  212. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/jobs/scheduler.py +0 -0
  213. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/jobs/worker.py +0 -0
  214. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/mcp/__init__.py +0 -0
  215. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
  216. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/README.md +0 -0
  217. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/__init__.py +0 -0
  218. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/add.py +0 -0
  219. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/cloud_dash.py +0 -0
  220. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/metrics/__init__.py +0 -0
  221. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/metrics/asgi.py +0 -0
  222. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/metrics/base.py +0 -0
  223. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/metrics/http.py +0 -0
  224. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
  225. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/metrics.py +0 -0
  226. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/__init__.py +0 -0
  227. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/compose_cloud/__init__.py +0 -0
  228. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/compose_cloud/templates/__init__.py +0 -0
  229. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
  230. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
  231. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/__init__.py +0 -0
  232. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
  233. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
  234. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
  235. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
  236. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
  237. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/dashboards/__init__.py +0 -0
  238. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/templates/__init__.py +0 -0
  239. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
  240. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
  241. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/templates/provisioning/__init__.py +0 -0
  242. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
  243. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
  244. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/settings.py +0 -0
  245. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
  246. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
  247. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/__init__.py +0 -0
  248. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/compose/__init__.py +0 -0
  249. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
  250. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
  251. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/fly/__init__.py +0 -0
  252. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
  253. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
  254. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/k8s/__init__.py +0 -0
  255. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
  256. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
  257. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
  258. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
  259. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
  260. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
  261. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/py.typed +0 -0
  262. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/audit.py +0 -0
  263. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/audit_service.py +0 -0
  264. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/headers.py +0 -0
  265. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/hibp.py +0 -0
  266. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/jwt_rotation.py +0 -0
  267. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/lockout.py +0 -0
  268. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/models.py +0 -0
  269. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/org_invites.py +0 -0
  270. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/passwords.py +0 -0
  271. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/permissions.py +0 -0
  272. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/session.py +0 -0
  273. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/security/signed_cookies.py +0 -0
  274. {svc_infra-0.1.597 → svc_infra-0.1.598}/src/svc_infra/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.597
3
+ Version: 0.1.598
4
4
  Summary: Infrastructure for building and deploying prod-ready services
5
5
  License: MIT
6
6
  Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "svc-infra"
3
- version = "0.1.597"
3
+ version = "0.1.598"
4
4
  description = "Infrastructure for building and deploying prod-ready services"
5
5
  authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
6
6
  license = "MIT"
@@ -125,6 +125,7 @@ markers = [
125
125
  "ratelimit: Rate limiting and abuse protection tests",
126
126
  "concurrency: Idempotency and concurrency control tests",
127
127
  "jobs: Background jobs and scheduling tests",
128
+ "webhooks: Webhooks framework tests",
128
129
  ]
129
130
  filterwarnings = [
130
131
  "ignore:The `route` decorator is deprecated:DeprecationWarning:starlette.*",
@@ -13,6 +13,10 @@ class InboxStore(Protocol):
13
13
  """Optional: remove expired keys, return number purged."""
14
14
  ...
15
15
 
16
+ def is_marked(self, key: str) -> bool:
17
+ """Return True if key is already marked (not expired), without modifying it."""
18
+ ...
19
+
16
20
 
17
21
  class InMemoryInboxStore:
18
22
  def __init__(self) -> None:
@@ -33,6 +37,11 @@ class InMemoryInboxStore:
33
37
  self._keys.pop(k, None)
34
38
  return len(to_del)
35
39
 
40
+ def is_marked(self, key: str) -> bool:
41
+ now = time.time()
42
+ exp = self._keys.get(key)
43
+ return bool(exp and exp > now)
44
+
36
45
 
37
46
  class SqlInboxStore:
38
47
  """Skeleton for a SQL-backed inbox store (dedupe table).
@@ -53,3 +62,6 @@ class SqlInboxStore:
53
62
 
54
63
  def purge_expired(self) -> int: # pragma: no cover - skeleton
55
64
  raise NotImplementedError
65
+
66
+ def is_marked(self, key: str) -> bool: # pragma: no cover - skeleton
67
+ raise NotImplementedError
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from svc_infra.db.inbox import InboxStore
6
+ from svc_infra.db.outbox import OutboxStore
7
+ from svc_infra.jobs.queue import Job
8
+ from svc_infra.webhooks.signing import sign
9
+
10
+
11
+ def make_webhook_handler(
12
+ *,
13
+ outbox: OutboxStore,
14
+ inbox: InboxStore,
15
+ get_webhook_url_for_topic,
16
+ get_secret_for_topic,
17
+ header_name: str = "X-Signature",
18
+ ):
19
+ """Return an async job handler to deliver webhooks.
20
+
21
+ Expected job payload shape:
22
+ {"outbox_id": int, "topic": str, "payload": {...}}
23
+ """
24
+
25
+ async def _handler(job: Job) -> None:
26
+ data = job.payload or {}
27
+ outbox_id = data.get("outbox_id")
28
+ topic = data.get("topic")
29
+ payload = data.get("payload") or {}
30
+ if not outbox_id or not topic:
31
+ # Nothing we can do; ack to avoid poison loop
32
+ return
33
+ # dedupe marker key (marked after successful delivery)
34
+ key = f"webhook:{outbox_id}"
35
+ if inbox.is_marked(key):
36
+ # already delivered
37
+ outbox.mark_processed(int(outbox_id))
38
+ return
39
+ event = payload.get("event") if isinstance(payload, dict) else None
40
+ subscription = payload.get("subscription") if isinstance(payload, dict) else None
41
+ if event is not None and subscription is not None:
42
+ delivery_payload = event
43
+ url = subscription.get("url") or get_webhook_url_for_topic(topic)
44
+ secret = subscription.get("secret") or get_secret_for_topic(topic)
45
+ subscription_id = subscription.get("id")
46
+ else:
47
+ delivery_payload = payload
48
+ url = get_webhook_url_for_topic(topic)
49
+ secret = get_secret_for_topic(topic)
50
+ subscription_id = None
51
+ sig = sign(secret, delivery_payload)
52
+ headers = {
53
+ header_name: sig,
54
+ "X-Event-Id": str(outbox_id),
55
+ "X-Topic": str(topic),
56
+ "X-Attempt": str(job.attempts or 1),
57
+ "X-Signature-Alg": "hmac-sha256",
58
+ "X-Signature-Version": "v1",
59
+ }
60
+ if subscription_id:
61
+ headers["X-Webhook-Subscription"] = str(subscription_id)
62
+ # include event payload version if present
63
+ version = None
64
+ if isinstance(delivery_payload, dict):
65
+ version = delivery_payload.get("version")
66
+ if version is not None:
67
+ headers["X-Payload-Version"] = str(version)
68
+ async with httpx.AsyncClient(timeout=10) as client:
69
+ resp = await client.post(url, json=delivery_payload, headers=headers)
70
+ if 200 <= resp.status_code < 300:
71
+ # record delivery and mark processed
72
+ inbox.mark_if_new(key, ttl_seconds=24 * 3600)
73
+ outbox.mark_processed(int(outbox_id))
74
+ return
75
+ # allow retry on non-2xx: raise to trigger fail/backoff
76
+ raise RuntimeError(f"webhook delivery failed: {resp.status_code}")
77
+
78
+ return _handler
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Sequence
4
+
5
+ from fastapi import HTTPException, Request, status
6
+
7
+ from .signing import verify, verify_any
8
+
9
+
10
+ def require_signature(
11
+ secrets_provider: Callable[[], str | Sequence[str]],
12
+ *,
13
+ header_name: str = "X-Signature",
14
+ ):
15
+ async def _dep(request: Request):
16
+ sig = request.headers.get(header_name)
17
+ if not sig:
18
+ raise HTTPException(
19
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="missing signature"
20
+ )
21
+ try:
22
+ body = await request.json()
23
+ except Exception:
24
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid JSON body")
25
+ secrets = secrets_provider()
26
+ ok = False
27
+ if isinstance(secrets, str):
28
+ ok = verify(secrets, body, sig)
29
+ else:
30
+ ok = verify_any(secrets, body, sig)
31
+ if not ok:
32
+ raise HTTPException(
33
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid signature"
34
+ )
35
+ return body
36
+
37
+ return _dep
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException
6
+
7
+ from svc_infra.db.outbox import InMemoryOutboxStore, OutboxStore
8
+
9
+ from .service import InMemoryWebhookSubscriptions, WebhookService
10
+
11
+ router = APIRouter(prefix="/_webhooks", tags=["webhooks"])
12
+
13
+
14
+ def get_outbox() -> OutboxStore:
15
+ # For now expose an in-memory default. Apps can override via DI.
16
+ # In production, provide a proper store through dependency override.
17
+ return InMemoryOutboxStore()
18
+
19
+
20
+ def get_subs() -> InMemoryWebhookSubscriptions:
21
+ return InMemoryWebhookSubscriptions()
22
+
23
+
24
+ def get_service(
25
+ outbox: OutboxStore = Depends(get_outbox),
26
+ subs: InMemoryWebhookSubscriptions = Depends(get_subs),
27
+ ) -> WebhookService:
28
+ return WebhookService(outbox=outbox, subs=subs)
29
+
30
+
31
+ @router.post("/subscriptions")
32
+ def add_subscription(
33
+ body: Dict[str, Any],
34
+ subs: InMemoryWebhookSubscriptions = Depends(get_subs),
35
+ ):
36
+ topic = body.get("topic")
37
+ url = body.get("url")
38
+ secret = body.get("secret")
39
+ if not topic or not url or not secret:
40
+ raise HTTPException(status_code=400, detail="Missing topic/url/secret")
41
+ subs.add(topic, url, secret)
42
+ return {"ok": True}
43
+
44
+
45
+ @router.post("/test-fire")
46
+ def test_fire(
47
+ body: Dict[str, Any],
48
+ svc: WebhookService = Depends(get_service),
49
+ ):
50
+ topic = body.get("topic")
51
+ payload = body.get("payload") or {}
52
+ if not topic:
53
+ raise HTTPException(status_code=400, detail="Missing topic")
54
+ outbox_id = svc.publish(topic, payload)
55
+ return {"ok": True, "outbox_id": outbox_id}
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Dict, List
6
+
7
+ from uuid import uuid4
8
+
9
+ from svc_infra.db.outbox import OutboxStore
10
+
11
+
12
+ @dataclass
13
+ class WebhookSubscription:
14
+ topic: str
15
+ url: str
16
+ secret: str
17
+ id: str = field(default_factory=lambda: uuid4().hex)
18
+
19
+
20
+ class InMemoryWebhookSubscriptions:
21
+ def __init__(self):
22
+ self._subs: Dict[str, List[WebhookSubscription]] = {}
23
+
24
+ def add(self, topic: str, url: str, secret: str) -> None:
25
+ self._subs.setdefault(topic, []).append(WebhookSubscription(topic, url, secret))
26
+
27
+ def get_for_topic(self, topic: str) -> List[WebhookSubscription]:
28
+ return list(self._subs.get(topic, []))
29
+
30
+
31
+ class WebhookService:
32
+ def __init__(self, outbox: OutboxStore, subs: InMemoryWebhookSubscriptions):
33
+ self._outbox = outbox
34
+ self._subs = subs
35
+
36
+ def publish(self, topic: str, payload: Dict, *, version: int = 1) -> int:
37
+ created_at = datetime.now(timezone.utc).isoformat()
38
+ base_event = {
39
+ "topic": topic,
40
+ "payload": payload,
41
+ "version": version,
42
+ "created_at": created_at,
43
+ }
44
+ # For each subscription, enqueue an outbox message with subscriber identity
45
+ last_id = 0
46
+ for sub in self._subs.get_for_topic(topic):
47
+ event = dict(base_event)
48
+ msg_payload = {
49
+ "event": event,
50
+ "subscription": {
51
+ "id": sub.id,
52
+ "topic": sub.topic,
53
+ "url": sub.url,
54
+ "secret": sub.secret,
55
+ },
56
+ }
57
+ msg = self._outbox.enqueue(topic, msg_payload)
58
+ last_id = msg.id
59
+ return last_id
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import hmac
5
+ import json
6
+ from typing import Dict, Iterable
7
+
8
+
9
+ def canonical_body(payload: Dict) -> bytes:
10
+ return json.dumps(payload, separators=(",", ":"), sort_keys=True).encode()
11
+
12
+
13
+ def sign(secret: str, payload: Dict) -> str:
14
+ body = canonical_body(payload)
15
+ return hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
16
+
17
+
18
+ def verify(secret: str, payload: Dict, signature: str) -> bool:
19
+ expected = sign(secret, payload)
20
+ try:
21
+ return hmac.compare_digest(expected, signature)
22
+ except Exception:
23
+ return False
24
+
25
+
26
+ def verify_any(secrets: Iterable[str], payload: Dict, signature: str) -> bool:
27
+ for s in secrets:
28
+ if verify(s, payload, signature):
29
+ return True
30
+ return False
@@ -1,59 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import hashlib
4
- import hmac
5
- import json
6
- from typing import Dict
7
-
8
- import httpx
9
-
10
- from svc_infra.db.inbox import InboxStore
11
- from svc_infra.db.outbox import OutboxStore
12
- from svc_infra.jobs.queue import Job
13
-
14
-
15
- def _compute_signature(secret: str, payload: Dict) -> str:
16
- body = json.dumps(payload, separators=(",", ":")).encode()
17
- return hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
18
-
19
-
20
- def make_webhook_handler(
21
- *,
22
- outbox: OutboxStore,
23
- inbox: InboxStore,
24
- get_webhook_url_for_topic,
25
- get_secret_for_topic,
26
- header_name: str = "X-Signature",
27
- ):
28
- """Return an async job handler to deliver webhooks.
29
-
30
- Expected job payload shape:
31
- {"outbox_id": int, "topic": str, "payload": {...}}
32
- """
33
-
34
- async def _handler(job: Job) -> None:
35
- data = job.payload or {}
36
- outbox_id = data.get("outbox_id")
37
- topic = data.get("topic")
38
- payload = data.get("payload") or {}
39
- if not outbox_id or not topic:
40
- # Nothing we can do; ack to avoid poison loop
41
- return
42
- # dedupe by outbox_id via inbox
43
- key = f"webhook:{outbox_id}"
44
- if not inbox.mark_if_new(key, ttl_seconds=24 * 3600):
45
- # already delivered
46
- outbox.mark_processed(int(outbox_id))
47
- return
48
- url = get_webhook_url_for_topic(topic)
49
- secret = get_secret_for_topic(topic)
50
- sig = _compute_signature(secret, payload)
51
- async with httpx.AsyncClient(timeout=10) as client:
52
- resp = await client.post(url, json=payload, headers={header_name: sig})
53
- if 200 <= resp.status_code < 300:
54
- outbox.mark_processed(int(outbox_id))
55
- return
56
- # allow retry on non-2xx: raise to trigger fail/backoff
57
- raise RuntimeError(f"webhook delivery failed: {resp.status_code}")
58
-
59
- return _handler
File without changes