painfree 0.1.0__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 (200) hide show
  1. painfree-0.1.0/LICENSE +21 -0
  2. painfree-0.1.0/NOTICE +49 -0
  3. painfree-0.1.0/PKG-INFO +208 -0
  4. painfree-0.1.0/README.md +167 -0
  5. painfree-0.1.0/hatch_build.py +32 -0
  6. painfree-0.1.0/painfree/__init__.py +15 -0
  7. painfree-0.1.0/painfree/__main__.py +451 -0
  8. painfree-0.1.0/painfree/access.py +531 -0
  9. painfree-0.1.0/painfree/access_api.py +437 -0
  10. painfree-0.1.0/painfree/accounts.py +732 -0
  11. painfree-0.1.0/painfree/api.py +823 -0
  12. painfree-0.1.0/painfree/app.py +468 -0
  13. painfree-0.1.0/painfree/attempts.py +246 -0
  14. painfree-0.1.0/painfree/audit.py +289 -0
  15. painfree-0.1.0/painfree/authn.py +878 -0
  16. painfree-0.1.0/painfree/camt.py +331 -0
  17. painfree-0.1.0/painfree/config.py +823 -0
  18. painfree-0.1.0/painfree/connections.py +336 -0
  19. painfree-0.1.0/painfree/custody.py +124 -0
  20. painfree-0.1.0/painfree/db.py +170 -0
  21. painfree-0.1.0/painfree/dispatcher.py +461 -0
  22. painfree-0.1.0/painfree/downloader.py +360 -0
  23. painfree-0.1.0/painfree/ebics3/__init__.py +231 -0
  24. painfree-0.1.0/painfree/ebics3/btf.py +213 -0
  25. painfree-0.1.0/painfree/ebics3/canon.py +239 -0
  26. painfree-0.1.0/painfree/ebics3/certificates.py +142 -0
  27. painfree-0.1.0/painfree/ebics3/crypto.py +342 -0
  28. painfree-0.1.0/painfree/ebics3/errors.py +142 -0
  29. painfree-0.1.0/painfree/ebics3/initialisation.py +520 -0
  30. painfree-0.1.0/painfree/ebics3/keys.py +181 -0
  31. painfree-0.1.0/painfree/ebics3/orderdata.py +198 -0
  32. painfree-0.1.0/painfree/ebics3/pipeline.py +287 -0
  33. painfree-0.1.0/painfree/ebics3/requests.py +637 -0
  34. painfree-0.1.0/painfree/ebics3/responses.py +245 -0
  35. painfree-0.1.0/painfree/ebics3/returncodes.py +265 -0
  36. painfree-0.1.0/painfree/ebics3/signature.py +220 -0
  37. painfree-0.1.0/painfree/ebics3/transaction.py +356 -0
  38. painfree-0.1.0/painfree/ebics3/versions.py +82 -0
  39. painfree-0.1.0/painfree/errors.py +113 -0
  40. painfree-0.1.0/painfree/identity.py +603 -0
  41. painfree-0.1.0/painfree/initialiser.py +314 -0
  42. painfree-0.1.0/painfree/isoxml.py +191 -0
  43. painfree-0.1.0/painfree/keyjobs.py +394 -0
  44. painfree-0.1.0/painfree/keyring.py +692 -0
  45. painfree-0.1.0/painfree/logging.py +339 -0
  46. painfree-0.1.0/painfree/migrations/env.py +64 -0
  47. painfree-0.1.0/painfree/migrations/script.py.mako +24 -0
  48. painfree-0.1.0/painfree/migrations/versions/0001_audit_log.py +52 -0
  49. painfree-0.1.0/painfree/migrations/versions/0002_connections_and_keyring.py +89 -0
  50. painfree-0.1.0/painfree/migrations/versions/0003_payment_orders.py +69 -0
  51. painfree-0.1.0/painfree/migrations/versions/0004_upload_claim.py +54 -0
  52. painfree-0.1.0/painfree/migrations/versions/0005_downloads.py +132 -0
  53. painfree-0.1.0/painfree/migrations/versions/0006_webhooks.py +95 -0
  54. painfree-0.1.0/painfree/migrations/versions/0007_identity.py +61 -0
  55. painfree-0.1.0/painfree/migrations/versions/0008_key_jobs.py +55 -0
  56. painfree-0.1.0/painfree/migrations/versions/0009_status_reports.py +63 -0
  57. painfree-0.1.0/painfree/migrations/versions/0010_webhook_management.py +89 -0
  58. painfree-0.1.0/painfree/migrations/versions/0011_schedule_management.py +45 -0
  59. painfree-0.1.0/painfree/migrations/versions/0012_connection_grants.py +270 -0
  60. painfree-0.1.0/painfree/migrations/versions/0013_oversight_grants.py +63 -0
  61. painfree-0.1.0/painfree/migrations/versions/0014_basic_accounts.py +86 -0
  62. painfree-0.1.0/painfree/migrations/versions/0015_payment_schemes.py +97 -0
  63. painfree-0.1.0/painfree/oidc.py +437 -0
  64. painfree-0.1.0/painfree/orders.py +658 -0
  65. painfree-0.1.0/painfree/pain001.py +345 -0
  66. painfree-0.1.0/painfree/pain002.py +180 -0
  67. painfree-0.1.0/painfree/payments.py +202 -0
  68. painfree-0.1.0/painfree/queue.py +528 -0
  69. painfree-0.1.0/painfree/reconcile.py +530 -0
  70. painfree-0.1.0/painfree/rekey.py +316 -0
  71. painfree-0.1.0/painfree/schedule.py +915 -0
  72. painfree-0.1.0/painfree/schema.py +978 -0
  73. painfree-0.1.0/painfree/schemas/README.md +19 -0
  74. painfree-0.1.0/painfree/schemas/pain.001.001.09.xsd +1114 -0
  75. painfree-0.1.0/painfree/schemes.py +533 -0
  76. painfree-0.1.0/painfree/sealing.py +256 -0
  77. painfree-0.1.0/painfree/sps.py +375 -0
  78. painfree-0.1.0/painfree/statements.py +379 -0
  79. painfree-0.1.0/painfree/tokens.py +381 -0
  80. painfree-0.1.0/painfree/transport.py +123 -0
  81. painfree-0.1.0/painfree/ui/__init__.py +51 -0
  82. painfree-0.1.0/painfree/ui/access_views.py +255 -0
  83. painfree-0.1.0/painfree/ui/account_views.py +161 -0
  84. painfree-0.1.0/painfree/ui/i18n.py +428 -0
  85. painfree-0.1.0/painfree/ui/locales/de.json +860 -0
  86. painfree-0.1.0/painfree/ui/locales/en.json +860 -0
  87. painfree-0.1.0/painfree/ui/locales/es.json +860 -0
  88. painfree-0.1.0/painfree/ui/locales/fr.json +860 -0
  89. painfree-0.1.0/painfree/ui/locales/it.json +860 -0
  90. painfree-0.1.0/painfree/ui/locales/pl.json +888 -0
  91. painfree-0.1.0/painfree/ui/locales/review.json +77 -0
  92. painfree-0.1.0/painfree/ui/notifications.py +191 -0
  93. painfree-0.1.0/painfree/ui/reference.py +272 -0
  94. painfree-0.1.0/painfree/ui/reference_views.py +142 -0
  95. painfree-0.1.0/painfree/ui/rendering.py +312 -0
  96. painfree-0.1.0/painfree/ui/scheme_forms.py +72 -0
  97. painfree-0.1.0/painfree/ui/static/01-tokens.css +207 -0
  98. painfree-0.1.0/painfree/ui/static/02-base.css +125 -0
  99. painfree-0.1.0/painfree/ui/static/03-chrome.css +427 -0
  100. painfree-0.1.0/painfree/ui/static/04-components.css +416 -0
  101. painfree-0.1.0/painfree/ui/static/theme.js +78 -0
  102. painfree-0.1.0/painfree/ui/templates/_bank.html +27 -0
  103. painfree-0.1.0/painfree/ui/templates/_icons.html +90 -0
  104. painfree-0.1.0/painfree/ui/templates/_jobs.html +46 -0
  105. painfree-0.1.0/painfree/ui/templates/access.html +84 -0
  106. painfree-0.1.0/painfree/ui/templates/access_subject.html +90 -0
  107. painfree-0.1.0/painfree/ui/templates/accounts.html +125 -0
  108. painfree-0.1.0/painfree/ui/templates/api.html +201 -0
  109. painfree-0.1.0/painfree/ui/templates/audit.html +107 -0
  110. painfree-0.1.0/painfree/ui/templates/bank_keys.html +67 -0
  111. painfree-0.1.0/painfree/ui/templates/base.html +266 -0
  112. painfree-0.1.0/painfree/ui/templates/connection.html +119 -0
  113. painfree-0.1.0/painfree/ui/templates/connection_access.html +64 -0
  114. painfree-0.1.0/painfree/ui/templates/connection_edit.html +83 -0
  115. painfree-0.1.0/painfree/ui/templates/connection_new.html +41 -0
  116. painfree-0.1.0/painfree/ui/templates/connections.html +47 -0
  117. painfree-0.1.0/painfree/ui/templates/error.html +20 -0
  118. painfree-0.1.0/painfree/ui/templates/keys.html +146 -0
  119. painfree-0.1.0/painfree/ui/templates/letter.html +38 -0
  120. painfree-0.1.0/painfree/ui/templates/order.html +151 -0
  121. painfree-0.1.0/painfree/ui/templates/order_replay.html +33 -0
  122. painfree-0.1.0/painfree/ui/templates/orders.html +56 -0
  123. painfree-0.1.0/painfree/ui/templates/schedule.html +247 -0
  124. painfree-0.1.0/painfree/ui/templates/schedule_new.html +88 -0
  125. painfree-0.1.0/painfree/ui/templates/schedules.html +106 -0
  126. painfree-0.1.0/painfree/ui/templates/sign_in.html +33 -0
  127. painfree-0.1.0/painfree/ui/templates/signed_out.html +19 -0
  128. painfree-0.1.0/painfree/ui/templates/statement.html +33 -0
  129. painfree-0.1.0/painfree/ui/templates/statements.html +49 -0
  130. painfree-0.1.0/painfree/ui/templates/status_codes.html +37 -0
  131. painfree-0.1.0/painfree/ui/templates/webhook.html +144 -0
  132. painfree-0.1.0/painfree/ui/templates/webhook_new.html +56 -0
  133. painfree-0.1.0/painfree/ui/templates/webhook_secret.html +49 -0
  134. painfree-0.1.0/painfree/ui/templates/webhooks.html +69 -0
  135. painfree-0.1.0/painfree/ui/views.py +939 -0
  136. painfree-0.1.0/painfree/webhooks.py +966 -0
  137. painfree-0.1.0/painfree/worker.py +558 -0
  138. painfree-0.1.0/painfree/wrapping.py +267 -0
  139. painfree-0.1.0/pyproject.toml +119 -0
  140. painfree-0.1.0/tests/basic_world.py +153 -0
  141. painfree-0.1.0/tests/conftest.py +808 -0
  142. painfree-0.1.0/tests/fixtures/camt.052.001.08.xml +106 -0
  143. painfree-0.1.0/tests/fixtures/camt.053.001.08.xml +241 -0
  144. painfree-0.1.0/tests/fixtures/camt.054.001.09.xml +119 -0
  145. painfree-0.1.0/tests/fixtures/pain.002.001.10.xml +76 -0
  146. painfree-0.1.0/tests/idp.py +226 -0
  147. painfree-0.1.0/tests/schemas/README.md +38 -0
  148. painfree-0.1.0/tests/schemas/camt.052.001.08.xsd +2070 -0
  149. painfree-0.1.0/tests/schemas/camt.053.001.08.xsd +2070 -0
  150. painfree-0.1.0/tests/schemas/camt.054.001.09.xsd +2012 -0
  151. painfree-0.1.0/tests/schemas/pain.002.001.10.xsd +1185 -0
  152. painfree-0.1.0/tests/test_ebics3_initialisation.py +333 -0
  153. painfree-0.1.0/tests/test_ebics3_keys.py +236 -0
  154. painfree-0.1.0/tests/test_ebics3_pipeline.py +202 -0
  155. painfree-0.1.0/tests/test_ebics3_requests.py +471 -0
  156. painfree-0.1.0/tests/test_ebics3_returncodes.py +204 -0
  157. painfree-0.1.0/tests/test_ebics3_signature.py +223 -0
  158. painfree-0.1.0/tests/test_ebics3_transaction.py +366 -0
  159. painfree-0.1.0/tests/test_packaging.py +216 -0
  160. painfree-0.1.0/tests/test_service_access.py +791 -0
  161. painfree-0.1.0/tests/test_service_api_page.py +316 -0
  162. painfree-0.1.0/tests/test_service_app.py +305 -0
  163. painfree-0.1.0/tests/test_service_audit.py +139 -0
  164. painfree-0.1.0/tests/test_service_audit_actions.py +504 -0
  165. painfree-0.1.0/tests/test_service_audit_page.py +259 -0
  166. painfree-0.1.0/tests/test_service_authz.py +746 -0
  167. painfree-0.1.0/tests/test_service_basic_access.py +225 -0
  168. painfree-0.1.0/tests/test_service_basic_auth.py +571 -0
  169. painfree-0.1.0/tests/test_service_basic_deployment.py +486 -0
  170. painfree-0.1.0/tests/test_service_camt.py +288 -0
  171. painfree-0.1.0/tests/test_service_config.py +212 -0
  172. painfree-0.1.0/tests/test_service_connections.py +185 -0
  173. painfree-0.1.0/tests/test_service_custody.py +322 -0
  174. painfree-0.1.0/tests/test_service_db.py +278 -0
  175. painfree-0.1.0/tests/test_service_dispatcher.py +578 -0
  176. painfree-0.1.0/tests/test_service_downloader.py +323 -0
  177. painfree-0.1.0/tests/test_service_i18n.py +689 -0
  178. painfree-0.1.0/tests/test_service_keyjobs.py +384 -0
  179. painfree-0.1.0/tests/test_service_keyring.py +349 -0
  180. painfree-0.1.0/tests/test_service_logging.py +242 -0
  181. painfree-0.1.0/tests/test_service_orders.py +399 -0
  182. painfree-0.1.0/tests/test_service_oversight.py +698 -0
  183. painfree-0.1.0/tests/test_service_pain001.py +159 -0
  184. painfree-0.1.0/tests/test_service_payments_api.py +241 -0
  185. painfree-0.1.0/tests/test_service_queue.py +306 -0
  186. painfree-0.1.0/tests/test_service_reconcile.py +549 -0
  187. painfree-0.1.0/tests/test_service_rekey.py +360 -0
  188. painfree-0.1.0/tests/test_service_schedule.py +388 -0
  189. painfree-0.1.0/tests/test_service_schedule_admin.py +753 -0
  190. painfree-0.1.0/tests/test_service_scheme_fallback.py +825 -0
  191. painfree-0.1.0/tests/test_service_schemes.py +514 -0
  192. painfree-0.1.0/tests/test_service_sealing.py +116 -0
  193. painfree-0.1.0/tests/test_service_sps.py +230 -0
  194. painfree-0.1.0/tests/test_service_statements.py +312 -0
  195. painfree-0.1.0/tests/test_service_tokens.py +341 -0
  196. painfree-0.1.0/tests/test_service_ui.py +476 -0
  197. painfree-0.1.0/tests/test_service_ui_chrome.py +476 -0
  198. painfree-0.1.0/tests/test_service_webhook_admin.py +565 -0
  199. painfree-0.1.0/tests/test_service_webhooks.py +467 -0
  200. painfree-0.1.0/tests/test_service_worker.py +459 -0
painfree-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 reyemb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
painfree-0.1.0/NOTICE ADDED
@@ -0,0 +1,49 @@
1
+ painfree
2
+ Copyright (c) 2026 reyemb
3
+
4
+ This product is licensed under the MIT License. The full text is in LICENSE,
5
+ and that notice covers everything in this distribution.
6
+
7
+ --------------------------------------------------------------------------------
8
+ Third-party notices
9
+ --------------------------------------------------------------------------------
10
+
11
+ ebics-api/ebics-client-php
12
+ https://github.com/ebics-api/ebics-client-php
13
+
14
+ The EBICS 3.0 protocol engine in `painfree/ebics3/` is a Python port of
15
+ `ebics-api/ebics-client-php`. It is not a copy: the code was rewritten in
16
+ Python against a different XML and cryptography stack, and it does not
17
+ reproduce the PHP source. It does follow that project's structure and its
18
+ solutions to the parts of EBICS the specification leaves ambiguous, which is
19
+ enough that its licence travels with this distribution rather than being left
20
+ behind in a commit message.
21
+
22
+ `ebics-client-php` is MIT licensed. Its notice, reproduced in full:
23
+
24
+ MIT License
25
+
26
+ Copyright (c) 2019-2020 EBICS API
27
+
28
+ Permission is hereby granted, free of charge, to any person obtaining a copy
29
+ of this software and associated documentation files (the "Software"), to deal
30
+ in the Software without restriction, including without limitation the rights
31
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32
+ copies of the Software, and to permit persons to whom the Software is
33
+ furnished to do so, subject to the following conditions:
34
+
35
+ The above copyright notice and this permission notice shall be included in all
36
+ copies or substantial portions of the Software.
37
+
38
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
44
+ SOFTWARE.
45
+
46
+ No other project's source was read or adapted into `painfree/ebics3/`. The
47
+ Ruby `epics` and the Python `fintech` implementations are used only as
48
+ black-box oracles: they are executed and their output is compared, and neither
49
+ contributes code to this distribution.
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.5
2
+ Name: painfree
3
+ Version: 0.1.0
4
+ Summary: Self-hosted middleware that accepts JSON payment instructions and submits them to banks over EBICS 3.0
5
+ Project-URL: Homepage, https://github.com/reyemb/painfree-ebics-middleware-python
6
+ Project-URL: Source, https://github.com/reyemb/painfree-ebics-middleware-python
7
+ Project-URL: Issues, https://github.com/reyemb/painfree-ebics-middleware-python/issues
8
+ Project-URL: Changelog, https://github.com/reyemb/painfree-ebics-middleware-python/releases
9
+ Author-email: reyemb <m3y3r.bastian@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ Keywords: banking,camt,ebics,iso20022,pain.001,payments,sepa
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Financial and Insurance Industry
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Financial
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: alembic>=1.13
27
+ Requires-Dist: argon2-cffi>=23.1
28
+ Requires-Dist: cryptography>=42
29
+ Requires-Dist: fastapi>=0.115
30
+ Requires-Dist: jinja2>=3.1
31
+ Requires-Dist: lxml>=5.2
32
+ Requires-Dist: psycopg[binary]>=3.2
33
+ Requires-Dist: pydantic-settings>=2.4
34
+ Requires-Dist: pyjwt==2.11.0
35
+ Requires-Dist: sqlalchemy>=2.0
36
+ Requires-Dist: uvicorn>=0.30
37
+ Provides-Extra: dev
38
+ Requires-Dist: httpx>=0.27; extra == 'dev'
39
+ Requires-Dist: pytest>=8; extra == 'dev'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # painfree
43
+
44
+ **JSON in, EBICS out.** Self-hosted middleware that accepts authenticated HTTP
45
+ requests and submits them to your banks over EBICS 3.0, so payment files stop
46
+ being something a human uploads by hand.
47
+
48
+ ```
49
+ your system ──JSON/HTTPS──▶ painfree ──EBICS 3.0 (H005)──▶ bank A
50
+ (OIDC) │ bank B
51
+ │ bank C
52
+ └──webhook──▶ your system
53
+ ```
54
+
55
+ POST a payment as JSON. painfree validates it, builds a compliant `pain.001`,
56
+ signs and encrypts it per the EBICS protocol, uploads it to the right bank
57
+ connection, tracks the order to acknowledgement, and calls your webhook when the
58
+ status changes. It runs the other direction too: scheduled downloads of
59
+ `camt.052/053/054` and `pain.002`, normalised to JSON.
60
+
61
+ A console at `/ui` handles everything that is not a payment: bank connections,
62
+ EBICS initialisation, key management, order history, replay, audit.
63
+
64
+ ## Status
65
+
66
+ **Early.** Not in production. Not suitable for anyone else's money.
67
+
68
+ ## Run it
69
+
70
+ Four lines, against the published image. Nothing is built.
71
+
72
+ ```bash
73
+ cp deploy/production.env.example .env # pinned to ghcr.io/reyemb/painfree
74
+ deploy/init-secrets.sh # four secret files, generated once
75
+ docker compose up -d # db, api, worker, TLS proxy
76
+ docker compose exec api python -m painfree create-admin you # no OIDC only
77
+ ```
78
+
79
+ Open the address in `.env`. Set
80
+ `PAINFREE_SITE_ADDRESS=https://painfree.localhost` to run it on a machine with
81
+ no DNS: the proxy issues that certificate from its own local CA.
82
+ `podman-compose` works identically, and `deploy/build-image.sh` builds the
83
+ image yourself if you would rather not run someone else's.
84
+
85
+ Everything that is not secret lives in `.env`. Every secret is a file under
86
+ `deploy/secrets/`, generated once, mounted at runtime, and never baked into the
87
+ image or committed.
88
+
89
+ ## Submitting a payment
90
+
91
+ ```bash
92
+ curl -X POST https://painfree.localhost/v1/connections/acme-ubs/payments \
93
+ -u you:the-password-you-set \
94
+ -H 'Idempotency-Key: caller-idem-0001' \
95
+ -H 'Content-Type: application/json' -d @payment.json
96
+ ```
97
+
98
+ `202 Accepted` with an `order_id` to poll at `/v1/orders/{order_id}`. Accepted
99
+ means accepted **for processing**, not that the bank has it. The instruction is
100
+ validated against the ISO 20022 schema *and* the Swiss Payment Standards rules
101
+ before anything is signed, so a malformed QR reference comes back immediately
102
+ naming the field, rather than from the bank two hours later.
103
+
104
+ One idempotency key stays one order, and an order can end accepted at most once.
105
+
106
+ **Payment schemes.** A payment carries `normal`, `instant`, or
107
+ `instant_or_normal`, which tries instant and sends an ordinary transfer if the
108
+ bank definitively refuses. A timeout, a dropped connection or an answer that
109
+ will not parse is an unknown outcome rather than a refusal: the order is
110
+ retried carrying the message it already had, so nothing is sent twice.
111
+
112
+ `/ui/api` renders the request body, every endpoint and the privilege it demands,
113
+ generated from the router rather than written down, and links the OpenAPI
114
+ documents beside it.
115
+
116
+ ## How it works
117
+
118
+ **Two processes, one image.** `api` serves HTTP and is refused the custody
119
+ secret. `worker` holds it, and does every upload, download and key operation. An
120
+ `api` process handed the secret does not start, and production refuses to run
121
+ both halves in one process. That is a security boundary rather than a scaling
122
+ one: the process answering requests has nothing to decrypt a private key with.
123
+
124
+ **Authentication.** OIDC where you have a provider (browser SSO with PKCE, JWT
125
+ bearer for machines), and HTTP Basic against local accounts where you do not.
126
+ Production refuses to start in development mode. A process accepts one kind of
127
+ credential and never two.
128
+
129
+ **Authorisation.** Two roles: `admin`, which may grant access, and `member`,
130
+ which may not. Everything a member holds is a grant naming a subject, a bank
131
+ connection and a level. `viewer` reads; `operator` also submits payments,
132
+ replays orders and manages schedules. Grants are read from the database on
133
+ every request, so revoking somebody takes effect on their next request. A
134
+ connection a caller holds no grant on answers `404`, exactly like one that was
135
+ never registered.
136
+
137
+ **Webhooks.** At-least-once. The event is written in the same transaction as
138
+ the fact it reports, so a crash cannot lose it, and a redelivery carries the
139
+ same `event_id`: deduplicate on that. Each request is signed
140
+ `X-Painfree-Signature: v1=<hex>`, an HMAC-SHA256 over `"<timestamp>.<raw
141
+ body>"` with the subscription's own secret. Verify over the bytes you received,
142
+ not over a re-serialisation.
143
+
144
+ **Keys.** INI, HIA and HPB from the console, with a printable INI letter,
145
+ renewal and suspension. The bank's keys are trusted only once you have compared
146
+ their fingerprints against the letter the bank sent, on a page where nothing is
147
+ pre-filled and declining is as easy as accepting.
148
+
149
+ ## Configuration
150
+
151
+ Every setting is an environment variable, and any of them can be read from a
152
+ file instead by setting `PAINFREE_X_FILE`. The ones a deployment sets are in
153
+ `deploy/production.env.example`; the rest are documented where they are
154
+ defined, in `painfree/config.py`.
155
+
156
+ | Variable | Default | What |
157
+ |---|---|---|
158
+ | `PAINFREE_ENVIRONMENT` | `development` | `production` refuses SQLite, `combined`, and development auth |
159
+ | `PAINFREE_ROLE` | `combined` | `api`, `worker` or `combined` |
160
+ | `PAINFREE_DATABASE_URL` | SQLite file | PostgreSQL in production |
161
+ | `PAINFREE_KEY_ENCRYPTION_SECRET` | none | seals every stored private key; required of a worker, refused for an `api` |
162
+ | `PAINFREE_AUTH_MODE` | derived | `oidc`, `basic`, or `development`, which production refuses |
163
+ | `PAINFREE_TLS_TERMINATED_UPSTREAM` | `false` | `basic` in production will not start without it |
164
+ | `PAINFREE_OIDC_ISSUER` / `_CLIENT_ID` / `_REDIRECT_URI` | none | all three required for `oidc` |
165
+
166
+ Logs are one JSON object per line on stdout. No token, authorization code,
167
+ session id or client secret ever reaches the stream.
168
+
169
+ ## Backup, and the one thing no backup covers
170
+
171
+ `deploy/backup.sh` dumps the database and `deploy/restore.sh` restores it. That
172
+ covers the sealed keys, the order history, the idempotency ledger and the audit
173
+ trail.
174
+
175
+ It does not cover `PAINFREE_KEY_ENCRYPTION_SECRET`, which is in no backup and
176
+ which nothing recovers. Lose it and every sealed key is unreadable by anyone,
177
+ and each bank connection has to be re-keyed on paper. Rotating it is `python -m
178
+ painfree rekey`, run with both secrets present, which re-seals every row and
179
+ fails loudly on any it could not move.
180
+
181
+ ## Develop
182
+
183
+ ```bash
184
+ uv sync --extra dev
185
+ uv run python -m painfree # SQLite, migrated at startup, on :8000
186
+ uv run pytest
187
+ ```
188
+
189
+ Development mode authenticates from `X-Painfree-Dev-Principal` and
190
+ `X-Painfree-Dev-Roles`, so a checkout runs and the suite passes with no provider
191
+ to point at. It is still an authentication step, and production refuses to start
192
+ in it.
193
+
194
+ There is one importable package, `painfree`; the EBICS 3.0 engine is the
195
+ subpackage `painfree/ebics3/`, which imports nothing from the service around it
196
+ and needs only `lxml` and `cryptography`.
197
+
198
+ ## Licence
199
+
200
+ **MIT.** Copyright (c) 2026 reyemb. The full text is in
201
+ [LICENSE](https://github.com/reyemb/painfree-ebics-middleware-python/blob/main/LICENSE).
202
+
203
+ The EBICS 3.0 engine in `painfree/ebics3/` is a Python port of
204
+ [`ebics-api/ebics-client-php`](https://github.com/ebics-api/ebics-client-php),
205
+ which is MIT too. Its notice is reproduced in full in
206
+ [NOTICE](https://github.com/reyemb/painfree-ebics-middleware-python/blob/main/NOTICE),
207
+ which every copy has to keep, and both files ship inside the wheel, the
208
+ source distribution and the container image.
@@ -0,0 +1,167 @@
1
+ # painfree
2
+
3
+ **JSON in, EBICS out.** Self-hosted middleware that accepts authenticated HTTP
4
+ requests and submits them to your banks over EBICS 3.0, so payment files stop
5
+ being something a human uploads by hand.
6
+
7
+ ```
8
+ your system ──JSON/HTTPS──▶ painfree ──EBICS 3.0 (H005)──▶ bank A
9
+ (OIDC) │ bank B
10
+ │ bank C
11
+ └──webhook──▶ your system
12
+ ```
13
+
14
+ POST a payment as JSON. painfree validates it, builds a compliant `pain.001`,
15
+ signs and encrypts it per the EBICS protocol, uploads it to the right bank
16
+ connection, tracks the order to acknowledgement, and calls your webhook when the
17
+ status changes. It runs the other direction too: scheduled downloads of
18
+ `camt.052/053/054` and `pain.002`, normalised to JSON.
19
+
20
+ A console at `/ui` handles everything that is not a payment: bank connections,
21
+ EBICS initialisation, key management, order history, replay, audit.
22
+
23
+ ## Status
24
+
25
+ **Early.** Not in production. Not suitable for anyone else's money.
26
+
27
+ ## Run it
28
+
29
+ Four lines, against the published image. Nothing is built.
30
+
31
+ ```bash
32
+ cp deploy/production.env.example .env # pinned to ghcr.io/reyemb/painfree
33
+ deploy/init-secrets.sh # four secret files, generated once
34
+ docker compose up -d # db, api, worker, TLS proxy
35
+ docker compose exec api python -m painfree create-admin you # no OIDC only
36
+ ```
37
+
38
+ Open the address in `.env`. Set
39
+ `PAINFREE_SITE_ADDRESS=https://painfree.localhost` to run it on a machine with
40
+ no DNS: the proxy issues that certificate from its own local CA.
41
+ `podman-compose` works identically, and `deploy/build-image.sh` builds the
42
+ image yourself if you would rather not run someone else's.
43
+
44
+ Everything that is not secret lives in `.env`. Every secret is a file under
45
+ `deploy/secrets/`, generated once, mounted at runtime, and never baked into the
46
+ image or committed.
47
+
48
+ ## Submitting a payment
49
+
50
+ ```bash
51
+ curl -X POST https://painfree.localhost/v1/connections/acme-ubs/payments \
52
+ -u you:the-password-you-set \
53
+ -H 'Idempotency-Key: caller-idem-0001' \
54
+ -H 'Content-Type: application/json' -d @payment.json
55
+ ```
56
+
57
+ `202 Accepted` with an `order_id` to poll at `/v1/orders/{order_id}`. Accepted
58
+ means accepted **for processing**, not that the bank has it. The instruction is
59
+ validated against the ISO 20022 schema *and* the Swiss Payment Standards rules
60
+ before anything is signed, so a malformed QR reference comes back immediately
61
+ naming the field, rather than from the bank two hours later.
62
+
63
+ One idempotency key stays one order, and an order can end accepted at most once.
64
+
65
+ **Payment schemes.** A payment carries `normal`, `instant`, or
66
+ `instant_or_normal`, which tries instant and sends an ordinary transfer if the
67
+ bank definitively refuses. A timeout, a dropped connection or an answer that
68
+ will not parse is an unknown outcome rather than a refusal: the order is
69
+ retried carrying the message it already had, so nothing is sent twice.
70
+
71
+ `/ui/api` renders the request body, every endpoint and the privilege it demands,
72
+ generated from the router rather than written down, and links the OpenAPI
73
+ documents beside it.
74
+
75
+ ## How it works
76
+
77
+ **Two processes, one image.** `api` serves HTTP and is refused the custody
78
+ secret. `worker` holds it, and does every upload, download and key operation. An
79
+ `api` process handed the secret does not start, and production refuses to run
80
+ both halves in one process. That is a security boundary rather than a scaling
81
+ one: the process answering requests has nothing to decrypt a private key with.
82
+
83
+ **Authentication.** OIDC where you have a provider (browser SSO with PKCE, JWT
84
+ bearer for machines), and HTTP Basic against local accounts where you do not.
85
+ Production refuses to start in development mode. A process accepts one kind of
86
+ credential and never two.
87
+
88
+ **Authorisation.** Two roles: `admin`, which may grant access, and `member`,
89
+ which may not. Everything a member holds is a grant naming a subject, a bank
90
+ connection and a level. `viewer` reads; `operator` also submits payments,
91
+ replays orders and manages schedules. Grants are read from the database on
92
+ every request, so revoking somebody takes effect on their next request. A
93
+ connection a caller holds no grant on answers `404`, exactly like one that was
94
+ never registered.
95
+
96
+ **Webhooks.** At-least-once. The event is written in the same transaction as
97
+ the fact it reports, so a crash cannot lose it, and a redelivery carries the
98
+ same `event_id`: deduplicate on that. Each request is signed
99
+ `X-Painfree-Signature: v1=<hex>`, an HMAC-SHA256 over `"<timestamp>.<raw
100
+ body>"` with the subscription's own secret. Verify over the bytes you received,
101
+ not over a re-serialisation.
102
+
103
+ **Keys.** INI, HIA and HPB from the console, with a printable INI letter,
104
+ renewal and suspension. The bank's keys are trusted only once you have compared
105
+ their fingerprints against the letter the bank sent, on a page where nothing is
106
+ pre-filled and declining is as easy as accepting.
107
+
108
+ ## Configuration
109
+
110
+ Every setting is an environment variable, and any of them can be read from a
111
+ file instead by setting `PAINFREE_X_FILE`. The ones a deployment sets are in
112
+ `deploy/production.env.example`; the rest are documented where they are
113
+ defined, in `painfree/config.py`.
114
+
115
+ | Variable | Default | What |
116
+ |---|---|---|
117
+ | `PAINFREE_ENVIRONMENT` | `development` | `production` refuses SQLite, `combined`, and development auth |
118
+ | `PAINFREE_ROLE` | `combined` | `api`, `worker` or `combined` |
119
+ | `PAINFREE_DATABASE_URL` | SQLite file | PostgreSQL in production |
120
+ | `PAINFREE_KEY_ENCRYPTION_SECRET` | none | seals every stored private key; required of a worker, refused for an `api` |
121
+ | `PAINFREE_AUTH_MODE` | derived | `oidc`, `basic`, or `development`, which production refuses |
122
+ | `PAINFREE_TLS_TERMINATED_UPSTREAM` | `false` | `basic` in production will not start without it |
123
+ | `PAINFREE_OIDC_ISSUER` / `_CLIENT_ID` / `_REDIRECT_URI` | none | all three required for `oidc` |
124
+
125
+ Logs are one JSON object per line on stdout. No token, authorization code,
126
+ session id or client secret ever reaches the stream.
127
+
128
+ ## Backup, and the one thing no backup covers
129
+
130
+ `deploy/backup.sh` dumps the database and `deploy/restore.sh` restores it. That
131
+ covers the sealed keys, the order history, the idempotency ledger and the audit
132
+ trail.
133
+
134
+ It does not cover `PAINFREE_KEY_ENCRYPTION_SECRET`, which is in no backup and
135
+ which nothing recovers. Lose it and every sealed key is unreadable by anyone,
136
+ and each bank connection has to be re-keyed on paper. Rotating it is `python -m
137
+ painfree rekey`, run with both secrets present, which re-seals every row and
138
+ fails loudly on any it could not move.
139
+
140
+ ## Develop
141
+
142
+ ```bash
143
+ uv sync --extra dev
144
+ uv run python -m painfree # SQLite, migrated at startup, on :8000
145
+ uv run pytest
146
+ ```
147
+
148
+ Development mode authenticates from `X-Painfree-Dev-Principal` and
149
+ `X-Painfree-Dev-Roles`, so a checkout runs and the suite passes with no provider
150
+ to point at. It is still an authentication step, and production refuses to start
151
+ in it.
152
+
153
+ There is one importable package, `painfree`; the EBICS 3.0 engine is the
154
+ subpackage `painfree/ebics3/`, which imports nothing from the service around it
155
+ and needs only `lxml` and `cryptography`.
156
+
157
+ ## Licence
158
+
159
+ **MIT.** Copyright (c) 2026 reyemb. The full text is in
160
+ [LICENSE](https://github.com/reyemb/painfree-ebics-middleware-python/blob/main/LICENSE).
161
+
162
+ The EBICS 3.0 engine in `painfree/ebics3/` is a Python port of
163
+ [`ebics-api/ebics-client-php`](https://github.com/ebics-api/ebics-client-php),
164
+ which is MIT too. Its notice is reproduced in full in
165
+ [NOTICE](https://github.com/reyemb/painfree-ebics-middleware-python/blob/main/NOTICE),
166
+ which every copy has to keep, and both files ship inside the wheel, the
167
+ source distribution and the container image.
@@ -0,0 +1,32 @@
1
+ """Keep `.gitignore` out of the source distribution.
2
+
3
+ The build backend force-includes every VCS exclusion file in an sdist, and a
4
+ force-include is not subject to the target's `exclude` patterns, so this is the
5
+ only place it can be dropped.
6
+
7
+ It matters because `.gitignore` in this repository is not just build noise: it
8
+ names the working paths that are deliberately kept local, and a source
9
+ distribution published to an index is exactly where that list should not
10
+ appear. The sdist is meant to be the source a rebuild needs, and a rebuild
11
+ needs none of it.
12
+
13
+ Scoped to the sdist target, so the wheel build -- which is what the container
14
+ image runs, from a directory that holds only `pyproject.toml` and the package
15
+ -- never has to find this file.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from typing import Any
22
+
23
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
24
+
25
+
26
+ class GitignoreOutOfSdistHook(BuildHookInterface):
27
+ PLUGIN_NAME = "gitignore-out-of-sdist"
28
+
29
+ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
30
+ forced = build_data.get("force_include", {})
31
+ for source in [p for p in forced if os.path.basename(p) == ".gitignore"]:
32
+ del forced[source]
@@ -0,0 +1,15 @@
1
+ """painfree -- JSON in, EBICS out.
2
+
3
+ The service layer lives directly under this package; the EBICS 3.0 protocol
4
+ engine is the subpackage :mod:`painfree.ebics3`, which must not import from any
5
+ other part of ``painfree``. This module is deliberately empty apart from the
6
+ version, so that importing the engine does not drag FastAPI, SQLAlchemy or a
7
+ database driver in with it -- that is what keeps the engine releasable on its
8
+ own.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ __all__ = ["__version__"]