provance 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.
@@ -0,0 +1,80 @@
1
+ Binary files
2
+
3
+ [Bb]in/
4
+ [Oo]bj/
5
+
6
+ VS Code, Rider, and IDE files
7
+
8
+ .vs/
9
+ *.user
10
+ *.suo
11
+ *.sln.docstates
12
+ *.vshost.exe
13
+ *.vshost.exe.config
14
+ *.vshost.exe.manifest
15
+ *.testsettings
16
+ *.trx
17
+
18
+ Visual Studio / Rider cache files
19
+
20
+ .idea/
21
+ .Rider/
22
+ *.log
23
+ *.filter
24
+ *.cache
25
+ *.pdb
26
+
27
+ NuGet packages
28
+
29
+ packages/
30
+ *.nupkg
31
+
32
+ Local NuGet cache
33
+
34
+ %USERPROFILE%.nuget\packages
35
+
36
+ Test results
37
+
38
+ TestResults/
39
+ coverlet_coverage.json
40
+
41
+ Tooling
42
+
43
+ .tool-cache/
44
+
45
+ -----------------------------------------------------------------------------
46
+
47
+ Multi-Language Prep (for Java/Rust/etc.)
48
+
49
+ -----------------------------------------------------------------------------
50
+
51
+ Java/Maven/Gradle build outputs (if we add a Java project)
52
+
53
+ /target/
54
+
55
+ Rust build outputs (if we add a Rust project)
56
+
57
+ /target/
58
+
59
+ Log files and database files
60
+
61
+ *.db
62
+ *.log
63
+ # Local outputs
64
+ TestResults/
65
+ nupkg/
66
+ src/dotnet/ProvanceProtocol/Provance.Example.Api/checkpoints/
67
+
68
+ *.db
69
+ *.db-wal
70
+ *.db-shm
71
+
72
+ # Java build outputs
73
+ src/java/**/target/
74
+
75
+ # Python build outputs
76
+ src/python/dist/
77
+ __pycache__/
78
+ .pytest_cache/
79
+ *.egg-info/
80
+ .venv/
provance-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 HakimBuilds
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.
@@ -0,0 +1,304 @@
1
+ Metadata-Version: 2.5
2
+ Name: provance
3
+ Version: 0.1.0
4
+ Summary: Tamper-evident audit trail: hash chained, signed ledger in your own database, with FastAPI, Flask and Django integration. Same format as the .NET and Java libraries.
5
+ Project-URL: Homepage, https://github.com/h-ibrahim365/ProvanceProtocol
6
+ Project-URL: Documentation, https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/src/python/README.md
7
+ Project-URL: Specification, https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/docs/SPEC.md
8
+ Project-URL: Changelog, https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/h-ibrahim365/ProvanceProtocol/issues
10
+ Author: Hakim Id Brahim
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: audit,audit-log,audit-trail,compliance,django,fastapi,flask,gdpr,hash-chain,ledger,rfc8785,tamper-evident
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: FastAPI
17
+ Classifier: Framework :: Flask
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Intended Audience :: Information Technology
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Topic :: Database
24
+ Classifier: Topic :: Security
25
+ Classifier: Topic :: Security :: Cryptography
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: rfc8785>=0.1.4
29
+ Provides-Extra: mysql
30
+ Requires-Dist: pymysql>=1.1; extra == 'mysql'
31
+ Provides-Extra: postgres
32
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
33
+ Provides-Extra: sqlserver
34
+ Requires-Dist: pymssql>=2.3; extra == 'sqlserver'
35
+ Provides-Extra: test
36
+ Requires-Dist: pytest>=8; extra == 'test'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # PROVANCE for Python
40
+
41
+ Python implementation of PROVANCE, the tamper-evident audit trail. It writes the same ledger format as the
42
+ [.NET](https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/src/dotnet/README.md) and
43
+ [Java](https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/src/java/README.md) libraries
44
+ ([format specification](https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/docs/SPEC.md)) and passes the
45
+ same test vectors: a ledger written in one language can be verified, and continued, in the others.
46
+
47
+ It also ships the `provance verify` command, so an auditor or a security team can check a ledger written by any of
48
+ the three without installing .NET or Java.
49
+
50
+ Canonical JSON comes from the [`rfc8785`](https://pypi.org/project/rfc8785/) package, an RFC 8785 implementation
51
+ independent of the .NET and Java code.
52
+
53
+ Python 3.10 or later.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install provance # SQLite, in-memory store, JSON Lines exports
59
+ pip install "provance[postgres]" # + PostgreSQL (psycopg)
60
+ pip install "provance[mysql]" # + MySQL, MariaDB (PyMySQL)
61
+ pip install "provance[sqlserver]" # + SQL Server, Azure SQL (pymssql)
62
+ ```
63
+
64
+ ## Writing entries
65
+
66
+ ```python
67
+ import os
68
+ from provance import AuditedPayload, ProvanceLedger, SqlLedgerStore
69
+
70
+ ledger = ProvanceLedger(
71
+ ledger_id="orders-api-prod",
72
+ secret_key=os.environ["PROVANCE_SECRET_KEY"],
73
+ store=SqlLedgerStore.from_url("postgresql://app@db.internal/audit"),
74
+ )
75
+
76
+ ledger.add_entry("ORDER_REFUNDED", AuditedPayload(
77
+ actor_id=user_id,
78
+ description="Refund issued",
79
+ data={"orderId": order_id, "amount": "49.90"},
80
+ ))
81
+
82
+ ledger.close() # on shutdown: writes the queued entries
83
+ ```
84
+
85
+ `add_entry` returns once the entry is stored, with its sequence, hash and signature. `await ledger.add_entry_async(...)`
86
+ does the same in asyncio code, and `ledger.submit(...)` returns a `concurrent.futures.Future` without waiting.
87
+
88
+ The ledger refuses to start with a missing ledger id, an HMAC key shorter than 32 bytes or a placeholder, or no
89
+ store. There is no silent fallback to an in-memory store.
90
+
91
+ Create one `ProvanceLedger` per ledger and per process, and close it on shutdown (or use it as a context manager).
92
+ A background thread writes the entries; it does not survive `fork()`, so with gunicorn create the ledger in each
93
+ worker (in the `post_fork` hook, or lazily on first use), not before the fork with `--preload`. A ledger used after
94
+ a fork raises `LedgerUnavailableError` instead of hanging.
95
+
96
+ ### Payloads
97
+
98
+ A payload can be a `dict`, a dataclass, a Pydantic model, an `AuditedPayload` or any object with public
99
+ attributes. `None` attributes are omitted, as in .NET and Java. Enums, `datetime`, `UUID`, `Decimal` and `bytes`
100
+ (Base64) are converted; for anything else, add a `__provance_payload__()` method or pass
101
+ `payload_serializer=` to the ledger.
102
+
103
+ Every field of the payload is covered by the hash. A payload that cannot be serialized raises `PayloadError` before
104
+ anything is queued.
105
+
106
+ Numbers are stored as IEEE 754 doubles, as RFC 8785 requires: write money amounts and integers above 2^53 as
107
+ strings.
108
+
109
+ ### Settings
110
+
111
+ Settings have the same names (in snake_case) and defaults as in .NET and Java. Pass them as keyword arguments or as
112
+ a `ProvanceOptions` object.
113
+
114
+ | Setting | Default | Meaning |
115
+ | :------ | :------ | :------ |
116
+ | `ledger_id` | required | stable identifier, for example `orders-api-prod` |
117
+ | `secret_key` | required | HMAC secret of the current key, at least 32 bytes |
118
+ | `signing_key_id` | `k1` | id of the current key; change it on every rotation |
119
+ | `retired_keys` | `{}` | old keys (id to secret), to verify entries signed before a rotation |
120
+ | `queue_capacity` | `10000` | entries waiting to be written |
121
+ | `queue_full_behavior` | `WAIT` | `WAIT` blocks the caller, `FAIL_FAST` raises `LedgerOverloadedError` |
122
+ | `max_batch_size` | `256` | entries written in one transaction |
123
+ | `max_append_attempts` | `5` | attempts on a transient store error before callers get `LedgerUnavailableError` |
124
+ | `checkpoint_interval` | 5 minutes | interval of automatic checkpoints, when a sink is configured |
125
+ | `shutdown_timeout` | 30 seconds | how long `close()` waits for the queue to drain |
126
+
127
+ ### Key rotation
128
+
129
+ ```python
130
+ ledger = ProvanceLedger(
131
+ ledger_id="orders-api-prod",
132
+ signing_key_id="k2",
133
+ secret_key=os.environ["PROVANCE_KEY_K2"],
134
+ retired_keys={"k1": os.environ["PROVANCE_KEY_K1"]},
135
+ store=store,
136
+ )
137
+ ```
138
+
139
+ New entries are signed with `k2`; entries signed with `k1` still verify. To keep the key out of the application,
140
+ pass `signer=` with an object backed by your KMS: a `key_id` property, `sign(digest)` returning the signature
141
+ bytes, and `verify(key_id, digest, signature)` returning a `SignatureCheck` (see `provance.LedgerSigner`).
142
+
143
+ ## One entry per HTTP request
144
+
145
+ `provance.http` records an `HTTP_REQUEST_COMPLETED`, `HTTP_REQUEST_FAILED` or `HTTP_REQUEST_ABORTED` entry after
146
+ each response, with the method, path, status code, duration, trace id and actor. Same payload and privacy defaults
147
+ as the ASP.NET Core middleware and the Spring filter: query strings, client IPs and user agents are off unless you
148
+ enable them, and exception messages are never recorded (only the exception type). `/health`, `/healthz`,
149
+ `/metrics` and `/favicon.ico` are skipped.
150
+
151
+ FastAPI, Starlette and other ASGI applications:
152
+
153
+ ```python
154
+ from provance.http import ProvanceAsgiMiddleware
155
+
156
+ app.add_middleware(ProvanceAsgiMiddleware, ledger=ledger)
157
+ ```
158
+
159
+ Flask and other WSGI applications (the entry is written when the server closes the response, so the status code is
160
+ final; with Flask's test client, close the response or use `with client.get(...)`):
161
+
162
+ ```python
163
+ from provance.http import ProvanceWsgiMiddleware
164
+
165
+ app.wsgi_app = ProvanceWsgiMiddleware(app.wsgi_app, ledger)
166
+ ```
167
+
168
+ Django:
169
+
170
+ ```python
171
+ # myproject/audit.py
172
+ from provance.http import django_middleware
173
+ audit_middleware = django_middleware(ledger)
174
+
175
+ # settings.py, after AuthenticationMiddleware
176
+ MIDDLEWARE = [..., "myproject.audit.audit_middleware"]
177
+ ```
178
+
179
+ Options go in `HttpAuditOptions`: `include_query_string`, `include_client_ip`, `include_user_agent`,
180
+ `excluded_paths`, `should_audit(method, path)`, `actor_id_resolver`, `audit_timeout` (seconds) and `failure_mode`.
181
+ With the default `LOG_AND_CONTINUE`, a request whose entry cannot be written still gets its response and the error
182
+ is logged; `RAISE` raises after the response (it does not undo what the handler did). Record the business events
183
+ that matter with `add_entry` in the handler itself, where a failure can stop the operation.
184
+
185
+ The actor is the authenticated user: `scope["user"]` in Starlette, `REMOTE_USER` in WSGI, `request.user.pk` in
186
+ Django. Pass `actor_id_resolver` for anything else.
187
+
188
+ ## Databases
189
+
190
+ `SqlLedgerStore` uses the same table and columns as the .NET Entity Framework Core store and the Java JDBC store, so
191
+ the three libraries can write to the same ledger.
192
+
193
+ | Database | URL | Tested |
194
+ | :------- | :-- | :----- |
195
+ | SQLite | `sqlite:///audit.db` | Yes |
196
+ | PostgreSQL | `postgresql://user:pass@host/db` | Yes (CI) |
197
+ | MySQL, MariaDB | `mysql://user:pass@host/db` | No, same SQL as the Java store |
198
+ | SQL Server, Azure SQL | `mssql://user:pass@host/db` | No, same SQL as the Java store |
199
+
200
+ `from_url` opens a connection per operation. In production, pass a pooled factory instead, for example SQLAlchemy's:
201
+
202
+ ```python
203
+ store = SqlLedgerStore(engine.raw_connection, "postgresql")
204
+ ```
205
+
206
+ The table is created on first use. If your migrations create it, pass `create_table=False` and take the statements
207
+ from `store.create_table_statements()`. The unique index on `seq` is what keeps several writers consistent: do not
208
+ drop it.
209
+
210
+ `InMemoryLedgerStore` is for tests only.
211
+
212
+ ## Several instances
213
+
214
+ Several processes and machines can write to the same ledger. The store rejects a sequence that already exists; the
215
+ instance that loses reloads the head and retries. There is no lock and no leader. Entries are batched under load.
216
+
217
+ When the database fails after writing a batch but before confirming it, the writer reloads the head, recognizes its
218
+ own entries by id and continues the chain without duplicates. Transient failures are retried with backoff
219
+ (`max_append_attempts`); after that, the callers get `LedgerUnavailableError` and the writer keeps serving new
220
+ entries.
221
+
222
+ ## Verifying and checkpoints
223
+
224
+ ```python
225
+ result = ledger.verify()
226
+ print(result.valid, result.failure, result.failed_at_sequence, result.message)
227
+ ```
228
+
229
+ Verification detects a modified field, a deleted, inserted or moved entry, and hashes recomputed without the key.
230
+ Entries removed from the end, and a full rewrite by someone holding the key, are only detected against a checkpoint
231
+ stored where those people cannot write.
232
+
233
+ ```python
234
+ ledger = ProvanceLedger(..., checkpoint_directory="/mnt/immutable/provance") # or checkpoint_sinks=[my_sink]
235
+
236
+ checkpoint = LedgerCheckpoint.from_json(json_from_trusted_storage)
237
+ result = ledger.verify(checkpoint)
238
+ ```
239
+
240
+ Checkpoints are published at `checkpoint_interval` when the head has moved. A sink is any object with a
241
+ `publish(checkpoint)` method. The file sink writes `{directory}/{ledger_id}/{sequence}.json` and never overwrites a
242
+ file, like the .NET and Java file sinks; checkpoint JSON is the same in the three languages.
243
+
244
+ `ledger.redact_payload(entry_id)` removes a payload (for a GDPR erasure request) and the ledger still verifies: only
245
+ the payload hash is chained.
246
+
247
+ ## Verifying from the command line
248
+
249
+ For an auditor with read access to the database and the verification key. The HMAC secret is read from an
250
+ environment variable (default `PROVANCE_SECRET_KEY`) or a file, never from the command line, so it does not end up
251
+ in your shell history.
252
+
253
+ ```bash
254
+ export PROVANCE_SECRET_KEY="..."
255
+ provance verify --db "postgresql://auditor@db.internal/audit" --ledger-id orders-api-prod \
256
+ --checkpoint /mnt/immutable/provance
257
+ ```
258
+
259
+ ```text
260
+ OK: 18342 entries verified up to sequence 18342.
261
+ Checkpoint at sequence 18300 matched.
262
+ Head: sequence 18342, hash 4f1c...
263
+ ```
264
+
265
+ | Option | Meaning |
266
+ | :----- | :------ |
267
+ | `--db URL` | `sqlite:///audit.db` (opened read-only), `postgresql://...`, `mysql://...`, `mssql://...` |
268
+ | `--file PATH` | a JSON Lines export instead of a database |
269
+ | `--table`, `--schema` | ledger table, default `provance_ledger` |
270
+ | `--ledger-id` | the ledger id configured in the application (required) |
271
+ | `--key-id`, `--key-env`, `--key-file` | current key id (default `k1`) and where its secret is |
272
+ | `--retired-key ID=VAR` | a retired key and the environment variable holding its secret (repeatable) |
273
+ | `--checkpoint PATH` | a trusted checkpoint file, or a checkpoint directory (the latest one is used) |
274
+ | `--from-checkpoint` | verify only from the checkpoint onward |
275
+ | `--json` | machine readable output |
276
+
277
+ Exit code: `0` valid, `1` verification failed, `2` usage or connection error, so it can run on a schedule and alert
278
+ on failure. Use a database account that can only read the ledger table.
279
+
280
+ To audit offline, export the table once and verify the file anywhere:
281
+
282
+ ```bash
283
+ provance export --db "postgresql://auditor@db.internal/audit" -o ledger.jsonl
284
+ provance verify --file ledger.jsonl --ledger-id orders-api-prod --checkpoint checkpoint.json
285
+ ```
286
+
287
+ The export has one entry per line, with the field names of the .NET and Java `LedgerEntry`.
288
+
289
+ The same checks are available without a ledger instance, for an audit script:
290
+
291
+ ```python
292
+ from provance import HmacVerifier, LedgerVerifier, SqlLedgerReader, connect
293
+
294
+ connection, dialect = connect("postgresql://auditor@db.internal/audit") # read only
295
+ verifier = LedgerVerifier(SqlLedgerReader(connection, dialect),
296
+ HmacVerifier({"k2": current_secret, "k1": retired_secret}),
297
+ ledger_id="orders-api-prod")
298
+ result = verifier.verify(checkpoint=checkpoint)
299
+ ```
300
+
301
+ ## Status
302
+
303
+ Version 0.1.0, format 1. Same behavior as the .NET and Java 0.1.0 libraries. Tested on Python 3.10 to 3.13, with
304
+ SQLite and PostgreSQL.
@@ -0,0 +1,266 @@
1
+ # PROVANCE for Python
2
+
3
+ Python implementation of PROVANCE, the tamper-evident audit trail. It writes the same ledger format as the
4
+ [.NET](https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/src/dotnet/README.md) and
5
+ [Java](https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/src/java/README.md) libraries
6
+ ([format specification](https://github.com/h-ibrahim365/ProvanceProtocol/blob/main/docs/SPEC.md)) and passes the
7
+ same test vectors: a ledger written in one language can be verified, and continued, in the others.
8
+
9
+ It also ships the `provance verify` command, so an auditor or a security team can check a ledger written by any of
10
+ the three without installing .NET or Java.
11
+
12
+ Canonical JSON comes from the [`rfc8785`](https://pypi.org/project/rfc8785/) package, an RFC 8785 implementation
13
+ independent of the .NET and Java code.
14
+
15
+ Python 3.10 or later.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install provance # SQLite, in-memory store, JSON Lines exports
21
+ pip install "provance[postgres]" # + PostgreSQL (psycopg)
22
+ pip install "provance[mysql]" # + MySQL, MariaDB (PyMySQL)
23
+ pip install "provance[sqlserver]" # + SQL Server, Azure SQL (pymssql)
24
+ ```
25
+
26
+ ## Writing entries
27
+
28
+ ```python
29
+ import os
30
+ from provance import AuditedPayload, ProvanceLedger, SqlLedgerStore
31
+
32
+ ledger = ProvanceLedger(
33
+ ledger_id="orders-api-prod",
34
+ secret_key=os.environ["PROVANCE_SECRET_KEY"],
35
+ store=SqlLedgerStore.from_url("postgresql://app@db.internal/audit"),
36
+ )
37
+
38
+ ledger.add_entry("ORDER_REFUNDED", AuditedPayload(
39
+ actor_id=user_id,
40
+ description="Refund issued",
41
+ data={"orderId": order_id, "amount": "49.90"},
42
+ ))
43
+
44
+ ledger.close() # on shutdown: writes the queued entries
45
+ ```
46
+
47
+ `add_entry` returns once the entry is stored, with its sequence, hash and signature. `await ledger.add_entry_async(...)`
48
+ does the same in asyncio code, and `ledger.submit(...)` returns a `concurrent.futures.Future` without waiting.
49
+
50
+ The ledger refuses to start with a missing ledger id, an HMAC key shorter than 32 bytes or a placeholder, or no
51
+ store. There is no silent fallback to an in-memory store.
52
+
53
+ Create one `ProvanceLedger` per ledger and per process, and close it on shutdown (or use it as a context manager).
54
+ A background thread writes the entries; it does not survive `fork()`, so with gunicorn create the ledger in each
55
+ worker (in the `post_fork` hook, or lazily on first use), not before the fork with `--preload`. A ledger used after
56
+ a fork raises `LedgerUnavailableError` instead of hanging.
57
+
58
+ ### Payloads
59
+
60
+ A payload can be a `dict`, a dataclass, a Pydantic model, an `AuditedPayload` or any object with public
61
+ attributes. `None` attributes are omitted, as in .NET and Java. Enums, `datetime`, `UUID`, `Decimal` and `bytes`
62
+ (Base64) are converted; for anything else, add a `__provance_payload__()` method or pass
63
+ `payload_serializer=` to the ledger.
64
+
65
+ Every field of the payload is covered by the hash. A payload that cannot be serialized raises `PayloadError` before
66
+ anything is queued.
67
+
68
+ Numbers are stored as IEEE 754 doubles, as RFC 8785 requires: write money amounts and integers above 2^53 as
69
+ strings.
70
+
71
+ ### Settings
72
+
73
+ Settings have the same names (in snake_case) and defaults as in .NET and Java. Pass them as keyword arguments or as
74
+ a `ProvanceOptions` object.
75
+
76
+ | Setting | Default | Meaning |
77
+ | :------ | :------ | :------ |
78
+ | `ledger_id` | required | stable identifier, for example `orders-api-prod` |
79
+ | `secret_key` | required | HMAC secret of the current key, at least 32 bytes |
80
+ | `signing_key_id` | `k1` | id of the current key; change it on every rotation |
81
+ | `retired_keys` | `{}` | old keys (id to secret), to verify entries signed before a rotation |
82
+ | `queue_capacity` | `10000` | entries waiting to be written |
83
+ | `queue_full_behavior` | `WAIT` | `WAIT` blocks the caller, `FAIL_FAST` raises `LedgerOverloadedError` |
84
+ | `max_batch_size` | `256` | entries written in one transaction |
85
+ | `max_append_attempts` | `5` | attempts on a transient store error before callers get `LedgerUnavailableError` |
86
+ | `checkpoint_interval` | 5 minutes | interval of automatic checkpoints, when a sink is configured |
87
+ | `shutdown_timeout` | 30 seconds | how long `close()` waits for the queue to drain |
88
+
89
+ ### Key rotation
90
+
91
+ ```python
92
+ ledger = ProvanceLedger(
93
+ ledger_id="orders-api-prod",
94
+ signing_key_id="k2",
95
+ secret_key=os.environ["PROVANCE_KEY_K2"],
96
+ retired_keys={"k1": os.environ["PROVANCE_KEY_K1"]},
97
+ store=store,
98
+ )
99
+ ```
100
+
101
+ New entries are signed with `k2`; entries signed with `k1` still verify. To keep the key out of the application,
102
+ pass `signer=` with an object backed by your KMS: a `key_id` property, `sign(digest)` returning the signature
103
+ bytes, and `verify(key_id, digest, signature)` returning a `SignatureCheck` (see `provance.LedgerSigner`).
104
+
105
+ ## One entry per HTTP request
106
+
107
+ `provance.http` records an `HTTP_REQUEST_COMPLETED`, `HTTP_REQUEST_FAILED` or `HTTP_REQUEST_ABORTED` entry after
108
+ each response, with the method, path, status code, duration, trace id and actor. Same payload and privacy defaults
109
+ as the ASP.NET Core middleware and the Spring filter: query strings, client IPs and user agents are off unless you
110
+ enable them, and exception messages are never recorded (only the exception type). `/health`, `/healthz`,
111
+ `/metrics` and `/favicon.ico` are skipped.
112
+
113
+ FastAPI, Starlette and other ASGI applications:
114
+
115
+ ```python
116
+ from provance.http import ProvanceAsgiMiddleware
117
+
118
+ app.add_middleware(ProvanceAsgiMiddleware, ledger=ledger)
119
+ ```
120
+
121
+ Flask and other WSGI applications (the entry is written when the server closes the response, so the status code is
122
+ final; with Flask's test client, close the response or use `with client.get(...)`):
123
+
124
+ ```python
125
+ from provance.http import ProvanceWsgiMiddleware
126
+
127
+ app.wsgi_app = ProvanceWsgiMiddleware(app.wsgi_app, ledger)
128
+ ```
129
+
130
+ Django:
131
+
132
+ ```python
133
+ # myproject/audit.py
134
+ from provance.http import django_middleware
135
+ audit_middleware = django_middleware(ledger)
136
+
137
+ # settings.py, after AuthenticationMiddleware
138
+ MIDDLEWARE = [..., "myproject.audit.audit_middleware"]
139
+ ```
140
+
141
+ Options go in `HttpAuditOptions`: `include_query_string`, `include_client_ip`, `include_user_agent`,
142
+ `excluded_paths`, `should_audit(method, path)`, `actor_id_resolver`, `audit_timeout` (seconds) and `failure_mode`.
143
+ With the default `LOG_AND_CONTINUE`, a request whose entry cannot be written still gets its response and the error
144
+ is logged; `RAISE` raises after the response (it does not undo what the handler did). Record the business events
145
+ that matter with `add_entry` in the handler itself, where a failure can stop the operation.
146
+
147
+ The actor is the authenticated user: `scope["user"]` in Starlette, `REMOTE_USER` in WSGI, `request.user.pk` in
148
+ Django. Pass `actor_id_resolver` for anything else.
149
+
150
+ ## Databases
151
+
152
+ `SqlLedgerStore` uses the same table and columns as the .NET Entity Framework Core store and the Java JDBC store, so
153
+ the three libraries can write to the same ledger.
154
+
155
+ | Database | URL | Tested |
156
+ | :------- | :-- | :----- |
157
+ | SQLite | `sqlite:///audit.db` | Yes |
158
+ | PostgreSQL | `postgresql://user:pass@host/db` | Yes (CI) |
159
+ | MySQL, MariaDB | `mysql://user:pass@host/db` | No, same SQL as the Java store |
160
+ | SQL Server, Azure SQL | `mssql://user:pass@host/db` | No, same SQL as the Java store |
161
+
162
+ `from_url` opens a connection per operation. In production, pass a pooled factory instead, for example SQLAlchemy's:
163
+
164
+ ```python
165
+ store = SqlLedgerStore(engine.raw_connection, "postgresql")
166
+ ```
167
+
168
+ The table is created on first use. If your migrations create it, pass `create_table=False` and take the statements
169
+ from `store.create_table_statements()`. The unique index on `seq` is what keeps several writers consistent: do not
170
+ drop it.
171
+
172
+ `InMemoryLedgerStore` is for tests only.
173
+
174
+ ## Several instances
175
+
176
+ Several processes and machines can write to the same ledger. The store rejects a sequence that already exists; the
177
+ instance that loses reloads the head and retries. There is no lock and no leader. Entries are batched under load.
178
+
179
+ When the database fails after writing a batch but before confirming it, the writer reloads the head, recognizes its
180
+ own entries by id and continues the chain without duplicates. Transient failures are retried with backoff
181
+ (`max_append_attempts`); after that, the callers get `LedgerUnavailableError` and the writer keeps serving new
182
+ entries.
183
+
184
+ ## Verifying and checkpoints
185
+
186
+ ```python
187
+ result = ledger.verify()
188
+ print(result.valid, result.failure, result.failed_at_sequence, result.message)
189
+ ```
190
+
191
+ Verification detects a modified field, a deleted, inserted or moved entry, and hashes recomputed without the key.
192
+ Entries removed from the end, and a full rewrite by someone holding the key, are only detected against a checkpoint
193
+ stored where those people cannot write.
194
+
195
+ ```python
196
+ ledger = ProvanceLedger(..., checkpoint_directory="/mnt/immutable/provance") # or checkpoint_sinks=[my_sink]
197
+
198
+ checkpoint = LedgerCheckpoint.from_json(json_from_trusted_storage)
199
+ result = ledger.verify(checkpoint)
200
+ ```
201
+
202
+ Checkpoints are published at `checkpoint_interval` when the head has moved. A sink is any object with a
203
+ `publish(checkpoint)` method. The file sink writes `{directory}/{ledger_id}/{sequence}.json` and never overwrites a
204
+ file, like the .NET and Java file sinks; checkpoint JSON is the same in the three languages.
205
+
206
+ `ledger.redact_payload(entry_id)` removes a payload (for a GDPR erasure request) and the ledger still verifies: only
207
+ the payload hash is chained.
208
+
209
+ ## Verifying from the command line
210
+
211
+ For an auditor with read access to the database and the verification key. The HMAC secret is read from an
212
+ environment variable (default `PROVANCE_SECRET_KEY`) or a file, never from the command line, so it does not end up
213
+ in your shell history.
214
+
215
+ ```bash
216
+ export PROVANCE_SECRET_KEY="..."
217
+ provance verify --db "postgresql://auditor@db.internal/audit" --ledger-id orders-api-prod \
218
+ --checkpoint /mnt/immutable/provance
219
+ ```
220
+
221
+ ```text
222
+ OK: 18342 entries verified up to sequence 18342.
223
+ Checkpoint at sequence 18300 matched.
224
+ Head: sequence 18342, hash 4f1c...
225
+ ```
226
+
227
+ | Option | Meaning |
228
+ | :----- | :------ |
229
+ | `--db URL` | `sqlite:///audit.db` (opened read-only), `postgresql://...`, `mysql://...`, `mssql://...` |
230
+ | `--file PATH` | a JSON Lines export instead of a database |
231
+ | `--table`, `--schema` | ledger table, default `provance_ledger` |
232
+ | `--ledger-id` | the ledger id configured in the application (required) |
233
+ | `--key-id`, `--key-env`, `--key-file` | current key id (default `k1`) and where its secret is |
234
+ | `--retired-key ID=VAR` | a retired key and the environment variable holding its secret (repeatable) |
235
+ | `--checkpoint PATH` | a trusted checkpoint file, or a checkpoint directory (the latest one is used) |
236
+ | `--from-checkpoint` | verify only from the checkpoint onward |
237
+ | `--json` | machine readable output |
238
+
239
+ Exit code: `0` valid, `1` verification failed, `2` usage or connection error, so it can run on a schedule and alert
240
+ on failure. Use a database account that can only read the ledger table.
241
+
242
+ To audit offline, export the table once and verify the file anywhere:
243
+
244
+ ```bash
245
+ provance export --db "postgresql://auditor@db.internal/audit" -o ledger.jsonl
246
+ provance verify --file ledger.jsonl --ledger-id orders-api-prod --checkpoint checkpoint.json
247
+ ```
248
+
249
+ The export has one entry per line, with the field names of the .NET and Java `LedgerEntry`.
250
+
251
+ The same checks are available without a ledger instance, for an audit script:
252
+
253
+ ```python
254
+ from provance import HmacVerifier, LedgerVerifier, SqlLedgerReader, connect
255
+
256
+ connection, dialect = connect("postgresql://auditor@db.internal/audit") # read only
257
+ verifier = LedgerVerifier(SqlLedgerReader(connection, dialect),
258
+ HmacVerifier({"k2": current_secret, "k1": retired_secret}),
259
+ ledger_id="orders-api-prod")
260
+ result = verifier.verify(checkpoint=checkpoint)
261
+ ```
262
+
263
+ ## Status
264
+
265
+ Version 0.1.0, format 1. Same behavior as the .NET and Java 0.1.0 libraries. Tested on Python 3.10 to 3.13, with
266
+ SQLite and PostgreSQL.