datavalv-client 0.5.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 (31) hide show
  1. datavalv_client-0.5.0/.gitignore +51 -0
  2. datavalv_client-0.5.0/LICENSE +21 -0
  3. datavalv_client-0.5.0/PKG-INFO +442 -0
  4. datavalv_client-0.5.0/README.md +417 -0
  5. datavalv_client-0.5.0/RELEASING.md +163 -0
  6. datavalv_client-0.5.0/pyproject.toml +73 -0
  7. datavalv_client-0.5.0/src/datavalv_client/__init__.py +45 -0
  8. datavalv_client-0.5.0/src/datavalv_client/_format_constants.py +29 -0
  9. datavalv_client-0.5.0/src/datavalv_client/cli.py +537 -0
  10. datavalv_client-0.5.0/src/datavalv_client/client.py +533 -0
  11. datavalv_client-0.5.0/src/datavalv_client/conformance.py +102 -0
  12. datavalv_client-0.5.0/src/datavalv_client/crypto.py +183 -0
  13. datavalv_client-0.5.0/src/datavalv_client/errors.py +296 -0
  14. datavalv_client-0.5.0/src/datavalv_client/format.py +41 -0
  15. datavalv_client-0.5.0/src/datavalv_client/pins.py +175 -0
  16. datavalv_client-0.5.0/src/datavalv_client/tokens.py +89 -0
  17. datavalv_client-0.5.0/tests/conftest.py +247 -0
  18. datavalv_client-0.5.0/tests/test_cli.py +657 -0
  19. datavalv_client-0.5.0/tests/test_client.py +641 -0
  20. datavalv_client-0.5.0/tests/test_conformance_cli.py +85 -0
  21. datavalv_client-0.5.0/tests/test_conformance_matrix.py +17 -0
  22. datavalv_client-0.5.0/tests/test_constants_generation.py +35 -0
  23. datavalv_client-0.5.0/tests/test_crypto.py +172 -0
  24. datavalv_client-0.5.0/tests/test_format.py +46 -0
  25. datavalv_client-0.5.0/tests/test_invite_key.py +60 -0
  26. datavalv_client-0.5.0/tests/test_packaging.py +229 -0
  27. datavalv_client-0.5.0/tests/test_pins.py +225 -0
  28. datavalv_client-0.5.0/tests/test_public_sample.py +249 -0
  29. datavalv_client-0.5.0/tests/test_readme.py +303 -0
  30. datavalv_client-0.5.0/tests/test_smoke.py +5 -0
  31. datavalv_client-0.5.0/tests/test_vectors.py +34 -0
@@ -0,0 +1,51 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ venv/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+
13
+ # Node / web
14
+ node_modules/
15
+ web/dist/
16
+
17
+ # CDK
18
+ cdk.out/
19
+ .cdk.staging/
20
+
21
+ # Secrets & keys — never commit signing keys
22
+ *.pem
23
+ *.key
24
+ .env
25
+ .env.*
26
+
27
+ # OS
28
+ .DS_Store
29
+
30
+ # npm lockfile is kept; build output is not. `crypto/dist` and `client-ts/dist`
31
+ # are covered by the `dist/` rule above; the tarballs are what `npm pack` leaves
32
+ # at the repository root when somebody follows client-ts/README.md step 1.
33
+ *.tgz
34
+ crypto/test-results/
35
+ crypto/playwright-report/
36
+
37
+ .venv/
38
+
39
+ # uv-managed environment
40
+ infra/cdk.out/
41
+
42
+ # IDE
43
+ .idea/
44
+ .vscode/
45
+ *.swp
46
+
47
+ # The first-backup runbook has the operator save a scratch push_backup.py in
48
+ # client/. It carries their client id, key id and the path to a signing key, so
49
+ # it must not be committed -- and it is reproduced in the runbook itself, so
50
+ # there is nothing to preserve.
51
+ client/push_backup.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hypercode AB
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,442 @@
1
+ Metadata-Version: 2.5
2
+ Name: datavalv-client
3
+ Version: 0.5.0
4
+ Summary: Client library for pushing end-to-end encrypted backups to Datavalv
5
+ Project-URL: Homepage, https://datavalv.se
6
+ Project-URL: Documentation, https://datavalv.se/connect-system
7
+ Project-URL: Source, https://github.com/Hypercode-SE/datavalv-clients
8
+ Author: Hypercode AB
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backup,datavalv,encryption,end-to-end
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: POSIX
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Topic :: System :: Archiving :: Backup
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: cryptography>=44
22
+ Requires-Dist: httpx>=0.28
23
+ Requires-Dist: pyjwt[crypto]>=2.10
24
+ Description-Content-Type: text/markdown
25
+
26
+ # datavalv-client
27
+
28
+ Datavalv receives backups pushed by a server and hands them back to people in a
29
+ browser. The copy is encrypted on the machine that made it, under a key
30
+ Datavalv never holds, so neither Datavalv nor AWS can read it.
31
+
32
+ This package is the pushing half: a `datavalv` command for a scheduled job, and
33
+ a `DatavalvClient` for a program that would rather do it itself. It is the
34
+ procedure on <https://datavalv.se/connect-system> with the mechanical parts
35
+ done for you — the keypair, a fresh single-use JWT for every request, and the
36
+ four HTTP calls one backup costs, in order, with the checksum header the third
37
+ one requires.
38
+
39
+ One step is not a command, and never will be. Step 3 says why.
40
+
41
+ Python 3.13 or newer, on a POSIX system.
42
+
43
+ ---
44
+
45
+ ## Connecting a server
46
+
47
+ Six steps. The first four are once per server; the last two are what you do per
48
+ customer and per night.
49
+
50
+ ### 1. Install it
51
+
52
+ ```bash
53
+ uv pip install datavalv-client
54
+ ```
55
+
56
+ or `pip install datavalv-client` if the server has no `uv`. That is the whole
57
+ step for most people.
58
+
59
+ The source is public if you would rather read it before you run it, or build it
60
+ yourself:
61
+
62
+ ```bash
63
+ git clone https://github.com/Hypercode-SE/datavalv-clients.git
64
+ cd datavalv-clients
65
+ uv pip install ./client
66
+ ```
67
+
68
+ For a server with no clone on it, build the wheel where you do have one:
69
+
70
+ ```bash
71
+ uv build client # writes dist/datavalv_client-0.5.0-py3-none-any.whl
72
+ ```
73
+
74
+ copy that one file over, and install it there:
75
+
76
+ ```bash
77
+ pip install datavalv_client-0.5.0-py3-none-any.whl
78
+ ```
79
+
80
+ It brings `cryptography`, `httpx` and `PyJWT` with it, and installs one command,
81
+ `datavalv`.
82
+
83
+ ### 2. Make a signing keypair
84
+
85
+ Run this **on the machine that will push the backups**. A signing key that has
86
+ been copied from somewhere else has been in an `scp` buffer, a backup of a
87
+ laptop, and whatever else copied it.
88
+
89
+ ```bash
90
+ datavalv keygen
91
+ ```
92
+
93
+ It writes `datavalv-signing.pem` in the current directory — created at mode
94
+ `600`, not created and then chmodded, because a file that exists readable for a
95
+ moment has been readable — and prints the **public** half to stdout, which is
96
+ the block you paste in step 3. Everything else it says goes to stderr.
97
+
98
+ The private half is never printed, never sent anywhere, and Datavalv stores only
99
+ the public one: there is no secret to fetch back later, and nothing we hold
100
+ proves anything in your place.
101
+
102
+ `--out PATH` puts the key somewhere else. `keygen` refuses to overwrite an
103
+ existing key file, because the old one may still be registered and pushing
104
+ backups; `--force` means you know that, and the old key should be retired in the
105
+ browser afterwards.
106
+
107
+ ### 3. Register the server in the browser
108
+
109
+ Sign in at <https://datavalv.se>, open **Connected servers** in the account menu
110
+ (`/applications`), give the server a name and paste the whole public block from
111
+ step 2. Two values come back. Keep both:
112
+
113
+ | value | what it identifies |
114
+ |---|---|
115
+ | `clientId` | this server |
116
+ | `kid` | this key |
117
+
118
+ **The first time that account signs in, a recovery code comes first.** Every new
119
+ account is shown one, once, and the screen asks for two of its four groups back
120
+ before it lets you on — an account that will only ever own a server included.
121
+ Write it down and keep it off the machine, then go on to **Connected servers**.
122
+ It is what ties the account's own key to a new password if the old one is lost.
123
+
124
+ **Why this is not a command.** `POST /v1/apps` sits on the Cognito authorizer,
125
+ so it takes a signed-in person — and a command line that asked for your Datavalv
126
+ password would be asking for the one password this product is built never to
127
+ see. That password never reaches Datavalv at all: the browser turns it into a
128
+ key that unwraps a vault and sends only a value derived from it, which is why a
129
+ stolen copy of our database is unreadable. A CLI prompt would be the one place
130
+ it existed in the clear. So this client makes the keypair and prints the public
131
+ half, and a human pastes it.
132
+
133
+ The same screen is where a key is rotated — add the new one, deploy it, retire
134
+ the old one, with both working in between — and where a server is retired
135
+ altogether.
136
+
137
+ ### 4. Configure the four settings
138
+
139
+ | variable | flag | value |
140
+ |---|---|---|
141
+ | `DATAVALV_API` | `--api` | `https://api.datavalv.se` (staging is `https://api-staging.datavalv.se`, and a registration in one is unknown to the other) |
142
+ | `DATAVALV_CLIENT_ID` | `--client-id` | the `clientId` from step 3 |
143
+ | `DATAVALV_KEY_ID` | `--key-id` | the `kid` from step 3 |
144
+ | `DATAVALV_PRIVATE_KEY` | `--private-key` | the **path** to the key from step 2 |
145
+
146
+ A flag wins over the variable; a scheduled job should use the variables.
147
+
148
+ `DATAVALV_PRIVATE_KEY` is a path on purpose. A private key in an environment
149
+ variable is in `ps`, in the shell history of whatever set it, and in every crash
150
+ dump of the process that read it. The client refuses key text there rather than
151
+ being helpfully permissive about it, and does not echo the value back when it
152
+ does — the natural error message would print your private key into whatever
153
+ collects your cron output.
154
+
155
+ If the key file is readable by anyone but its owner you get a warning, not a
156
+ refusal: a permission bit is worth telling you about, and not worth turning into
157
+ a night with no backup.
158
+
159
+ ### 5. Create a vault for the customer
160
+
161
+ One vault per customer, organisation or server whose data you push. One call,
162
+ once.
163
+
164
+ ```bash
165
+ datavalv vault create --external-id kund-4711 --owner-email anna@example.se --name "Kund 4711" --retention 7
166
+ ```
167
+
168
+ `--external-id` is your own identifier for them and what every later call uses.
169
+ `--retention` is how many copies are kept (1 if you do not say). `--locale
170
+ sv|en` picks the language of the invitation the owner receives — supplied, never
171
+ detected, because no browser is involved in this call and Swedish is the
172
+ default guess.
173
+
174
+ The vault's id is printed on stdout; the rest is on stderr. The owner is
175
+ emailed, and the vault waits.
176
+
177
+ **The address is the whole of the connection.** There is no invitation link and
178
+ no code: the vault appears only to an account whose *confirmed* email address is
179
+ identical to the one you sent. A mistyped address still returns `201`, still
180
+ sends a mail, and is reported as an error to nobody. Read it back to the
181
+ customer before you run the command.
182
+
183
+ **The invitation names your system only once we have confirmed who registered
184
+ it.** Anybody can register a server under any name, and that mail goes out from
185
+ our domain — so until we have checked, it says only that a vault is waiting, and
186
+ carries neither your server's name nor the vault's. Get in touch once the server
187
+ is registered and we will do it; nothing else changes.
188
+
189
+ **Creations are counted per day.** A server may create twice as many vaults in a
190
+ day as it may hold — ten, at the default allowance of five — and deleting a vault
191
+ gives nothing back. Past that the call answers `429 quota_exceeded`, which is
192
+ exit code 5: tomorrow it works.
193
+
194
+ ### 6. Check the vault's key
195
+
196
+ Every backup is encrypted to the vault's public key, and step 1 of a push gets
197
+ that key **from us**. We are the one party this design does not trust with your
198
+ plaintext, so that key is the one thing here you should not take our word for: a
199
+ service that answered with a key of its own could read every backup sent
200
+ afterwards, and nothing in the protocol would show it.
201
+
202
+ So this client remembers. The first key it is given for a vault is written down,
203
+ as `ssh` does with `known_hosts`, and **a different one is refused before
204
+ anything is encrypted** — exit code 7. A vault's key is made once, in its first
205
+ member's browser, and never changes.
206
+
207
+ That leaves the first time, and a person closes it. Once the owner has started
208
+ the vault:
209
+
210
+ ```bash
211
+ datavalv vault key --external-id kund-4711
212
+ ```
213
+
214
+ prints the fingerprint of the key we offer, on stdout, and records nothing. The
215
+ vault's owner sees a fingerprint under their backups at datavalv.se, **worked out
216
+ in their browser from the key they hold** — ask them to read it out. If the two
217
+ are the same, push. Better still, record theirs, which makes no request and
218
+ never passes through us at all:
219
+
220
+ ```bash
221
+ datavalv vault key --external-id kund-4711 --trust de6272b8151979389437054b06df8a817a6ad62ab87faac4bbacb3360c1385d0
222
+ ```
223
+
224
+ A substituted key is then refused on the very first push. Skip both and the
225
+ first push trusts the key it is offered, prints its fingerprint once, and pins
226
+ it — better than nothing, and the check above is what makes it more than that.
227
+
228
+ The fingerprint is SHA-256 over the key's DER, so nothing of ours is needed to
229
+ compute it:
230
+
231
+ ```bash
232
+ openssl pkey -pubin -in tenant-public.pem -outform DER | openssl dgst -sha256
233
+ ```
234
+
235
+ Pins live in `~/.local/state/datavalv/known_vaults.json` (under
236
+ `$XDG_STATE_HOME` if that is set), mode 600; `DATAVALV_KNOWN_VAULTS` names
237
+ another path. The Python and TypeScript clients share the file. **A machine with
238
+ no disk to remember on** — a container that starts empty every night — sees every
239
+ key for the first time, which protects nothing: mount that path, or pass the
240
+ fingerprint in from configuration (see *The library*).
241
+
242
+ **If a push exits 7**, nothing was sent. If the vault was deleted and created
243
+ again it is a new vault with a new key: get the new fingerprint from its owner,
244
+ and if it is the one offered,
245
+
246
+ ```bash
247
+ datavalv vault key --external-id kund-4711 --forget
248
+ ```
249
+
250
+ and push again. If it is not, do not push, and tell us.
251
+
252
+ ### 7. Push a backup
253
+
254
+ ```bash
255
+ datavalv push /var/backups/kund-4711.sql.gz --external-id kund-4711 --label "nightly"
256
+ ```
257
+
258
+ That is the four calls: fetch the vault's public key, register the copy, upload
259
+ the ciphertext, confirm it. All four or none — a copy counts only once the last
260
+ one has returned, and until then the previous copy is untouched.
261
+
262
+ The backup id goes to stdout and everything else to stderr, so
263
+ `BACKUP=$(datavalv push ...)` holds an id and nothing else. A dash reads stdin,
264
+ for a dump you would rather not write to disk:
265
+
266
+ ```bash
267
+ pg_dump kund4711 | gzip | datavalv push - --external-id kund-4711
268
+ ```
269
+
270
+ The filename is encrypted and sent along, so the member who can open the backup
271
+ can see what it was called. `--name-as NAME` sends a different one and
272
+ `--no-name` sends none at all; reading from stdin sends none, because there is
273
+ no name to send and inventing one would put a guess in the vault. A path is
274
+ never sent — only its last component.
275
+
276
+ `--idempotency-key` is worth setting from a schedule that retries: without it, a
277
+ retry of a run that timed out stores the same night twice and spends the quota
278
+ twice.
279
+
280
+ **A `409` before the owner has started the vault is expected.** The vault's key
281
+ is made in the first member's browser, so until somebody signs in there is
282
+ genuinely nothing to encrypt against. `datavalv push` exits **5** for that, and
283
+ 5 means *nothing is wrong, run it again later*. A schedule that treats it as a
284
+ failure pages somebody about a server that is working perfectly. The run after
285
+ the owner signs in goes through on its own, with nobody touching the server.
286
+
287
+ ---
288
+
289
+ ## Exit codes
290
+
291
+ `datavalv` returns these so a scheduled job can branch on them without reading
292
+ prose:
293
+
294
+ | code | meaning |
295
+ |---|---|
296
+ | 0 | done |
297
+ | 1 | failed for a reason not covered below |
298
+ | 2 | the command line was wrong |
299
+ | 3 | a setting, or a file named on the command line, is missing or unusable |
300
+ | 4 | the signing key, client id or key id was refused |
301
+ | 5 | nothing is wrong; run it again later (vault not started yet, network, quota) |
302
+ | 6 | the service refused the request and a person must look at it |
303
+ | 7 | the key offered for the vault is not the one on record; nothing was sent |
304
+
305
+ 5 is the one worth a rule in your scheduler: run it again unchanged, and the run
306
+ after the vault is started goes through. 3, 4, 6 and 7 need a person — 7 most
307
+ of all, and soon: see step 6. 2 means the command line itself was wrong, so
308
+ nobody was going to be paged about it anyway.
309
+
310
+ ---
311
+
312
+ ## What the crypto does
313
+
314
+ Per backup, on your server, before anything is sent:
315
+
316
+ - A fresh **AES-256-GCM** key is drawn for this one copy and the file is
317
+ encrypted under it. The whole 21-byte format header — magic bytes, version
318
+ and algorithm ids, the IV and its length — is the additional authenticated
319
+ data, so a header edited in transit makes the copy fail to open rather than
320
+ decrypt into something else.
321
+ - That key is wrapped to the **vault's RSA-3072 public key**, RSA-OAEP with
322
+ SHA-256, and the wrapped copy travels with the backup. The vault's private
323
+ key exists only inside members' browsers, wrapped under keys derived from a
324
+ member's password and their recovery code.
325
+ - A **SHA-256 digest is taken over the ciphertext**, never over the plaintext.
326
+ It is signed into the upload URL, so storage refuses a body that disagrees
327
+ with it, and Datavalv checks it again before the copy is allowed to count.
328
+ - The filename, when you send one, is encrypted under the same key with its own
329
+ IV — never the payload's, which would reuse a nonce under one AES-GCM key.
330
+
331
+ The format is frozen and shared with the browser implementation:
332
+ `format/v1.json` in this repository is the single source of both, and a
333
+ conformance matrix runs the two against each other on live code.
334
+
335
+ Nothing here can open a backup you have pushed — not for want of code, since the
336
+ format's decryption side is in `datavalv_client.crypto` and the conformance
337
+ suite runs on it, but because a source application is never handed the key it
338
+ would need. That is what makes a compromised server leak the backups it is
339
+ about to make rather than the ones it has made.
340
+
341
+ ---
342
+
343
+ ## The library
344
+
345
+ The CLI is a thin layer over this. Everything it does is available directly:
346
+
347
+ ```python
348
+ from pathlib import Path
349
+
350
+ from datavalv_client import DatavalvClient
351
+
352
+ with DatavalvClient(
353
+ api_base="https://api.datavalv.se",
354
+ client_id=CLIENT_ID,
355
+ key_id=KEY_ID,
356
+ private_key=Path("/etc/datavalv/datavalv-signing.pem").read_text(),
357
+ ) as client:
358
+ vault = client.create_vault("kund-4711", "anna@example.se", retention_count=7)
359
+
360
+ with open("/var/backups/kund-4711.sql.gz", "rb") as dump:
361
+ pushed = client.push_backup(
362
+ "kund-4711", dump, name="kund-4711.sql.gz", label="nightly"
363
+ )
364
+
365
+ print(pushed.backup_id, pushed.size, pushed.sha256)
366
+ ```
367
+
368
+ `private_key` is PEM **text** or an already-loaded `cryptography` key object —
369
+ never a path, which is the opposite of the environment variable's rule and for
370
+ the same reason: a client that accepted both would make the two spellings
371
+ interchangeable, and that is how a key ends up in an environment variable.
372
+
373
+ `push_backup` reads the whole file into memory. The size and the digest of the
374
+ ciphertext have to be known before an upload URL can be asked for, so there is
375
+ no streaming arrangement that would not encrypt the file twice.
376
+
377
+ Failures are `DatavalvError` subclasses, each naming the step it happened at,
378
+ and each saying whether running the same thing again could work:
379
+
380
+ ```python
381
+ from datavalv_client import DatavalvError
382
+
383
+ try:
384
+ client.push_backup("kund-4711", data)
385
+ except DatavalvError as exc:
386
+ if exc.retryable: # VaultNotInitialized, QuotaExceeded, NetworkError
387
+ log.info("not yet: %s", exc)
388
+ else: # AuthError, ConfigurationError, ApiError, UploadError,
389
+ raise # VaultKeyChanged -- never retry that one; see step 6
390
+ ```
391
+
392
+ **The vault's key is pinned here too**, with no argument needed: `pins` defaults
393
+ to the same file the CLI uses, and `push_backup` raises `VaultKeyChanged` — with
394
+ `.pinned` and `.offered` — before encrypting anything if the key is not the one
395
+ on record. `pushed.key_first_seen` is true on the push that recorded it, and
396
+ `pushed.key_fingerprint` is what to compare with the owner's. On a machine with
397
+ no disk to remember on, pass the fingerprint in from configuration instead, so
398
+ that a key which is not that one is refused from the first push:
399
+
400
+ ```python
401
+ from datavalv_client import DatavalvClient, MemoryPins
402
+
403
+ pins = MemoryPins({("https://api.datavalv.se", CLIENT_ID, "kund-4711"): FINGERPRINT})
404
+ client = DatavalvClient(
405
+ api_base="https://api.datavalv.se",
406
+ client_id=CLIENT_ID,
407
+ key_id=KEY_ID,
408
+ private_key=PRIVATE_KEY_PEM,
409
+ pins=pins,
410
+ )
411
+ ```
412
+
413
+ `client.list_backups("kund-4711")` says what the service holds for a vault: ids,
414
+ sizes, digests, timestamps, labels and the encrypted filename. No key material —
415
+ the member listing is a different model in the API for exactly that reason.
416
+
417
+ ---
418
+
419
+ ## What is not here
420
+
421
+ - **Restoring is the browser's.** A backup is decrypted inside a member's
422
+ browser with a key this package is never given. Signing in at datavalv.se is
423
+ the whole of the restore path.
424
+ - **Registering a server is the browser's**, for the reason in step 3.
425
+ - **Changing retention and deleting a vault** are API routes
426
+ (`PATCH` and `DELETE /v1/tenants/{externalId}`) that this client does not wrap
427
+ yet. Retention is set when the vault is created.
428
+
429
+ The protocol underneath, if you would rather implement it yourself, is
430
+ <https://datavalv.se/developers> — and the four calls with their fields are on
431
+ <https://datavalv.se/connect-system>, which this README follows step for step.
432
+
433
+ ---
434
+
435
+ ## Developing and releasing
436
+
437
+ The package lives in `client/` of the Datavalv monorepo. Its tests run with
438
+ `cd client && uv run pytest`, and `make check` from the repository root runs
439
+ those plus the format governance the crypto depends on.
440
+
441
+ `RELEASING.md` is the publish procedure. It has not been run: nothing has been
442
+ uploaded to PyPI under this name by this project.