hyperbrowser 1.14.0__tar.gz → 1.15.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 (145) hide show
  1. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/PKG-INFO +109 -5
  2. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/README.md +108 -4
  3. hyperbrowser-1.15.0/hyperbrowser/__init__.py +13 -0
  4. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandbox.py +4 -1
  5. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +3 -2
  6. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/secret.py +202 -15
  7. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/vault.py +35 -0
  8. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/shared.py +8 -2
  9. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +4 -1
  10. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +3 -2
  11. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/secret.py +196 -15
  12. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/vault.py +35 -0
  13. hyperbrowser-1.15.0/hyperbrowser/customer_key.py +705 -0
  14. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/__init__.py +16 -0
  15. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/consts.py +2 -0
  16. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/credential.py +6 -5
  17. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/sandbox.py +8 -4
  18. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/secret.py +81 -5
  19. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/session.py +14 -0
  20. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/vault.py +89 -1
  21. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/secrets_common.py +50 -0
  22. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/__init__.py +8 -0
  23. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/credential.py +3 -3
  24. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/sandbox.py +3 -2
  25. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/secret.py +29 -3
  26. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/vault.py +18 -0
  27. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/pyproject.toml +1 -1
  28. hyperbrowser-1.14.0/hyperbrowser/__init__.py +0 -5
  29. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/LICENSE +0 -0
  30. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/build_context.py +0 -0
  31. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/_request.py +0 -0
  32. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/async_client.py +0 -0
  33. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/base.py +0 -0
  34. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  35. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  36. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  37. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  38. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  39. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
  40. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  41. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
  42. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
  43. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  44. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  45. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  46. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  47. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  48. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  49. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
  50. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
  51. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
  52. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  53. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  54. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/session_credentials.py +0 -0
  55. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  56. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
  57. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  58. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  59. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  60. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/webmcp.py +0 -0
  61. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  62. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
  63. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
  64. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/image_build.py +0 -0
  65. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/image_resolution.py +0 -0
  66. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
  67. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  68. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  69. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  70. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  71. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  72. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
  73. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  74. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
  75. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
  76. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  77. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  78. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  79. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  80. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  81. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  82. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
  83. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
  84. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
  85. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  86. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  87. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/session_credentials.py +0 -0
  88. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  89. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
  90. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  91. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  92. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  93. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/webmcp.py +0 -0
  94. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/sync.py +0 -0
  95. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/config.py +0 -0
  96. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/credentials_common.py +0 -0
  97. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/exceptions.py +0 -0
  98. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/image_builds.py +0 -0
  99. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/_parsers.py +0 -0
  100. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/browser_use.py +0 -0
  101. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  102. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/cua.py +0 -0
  103. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  104. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
  105. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  106. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
  107. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
  108. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/computer_action.py +0 -0
  109. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/crawl.py +0 -0
  110. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/extension.py +0 -0
  111. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/extract.py +0 -0
  112. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/profile.py +0 -0
  113. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/scrape.py +0 -0
  114. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/team.py +0 -0
  115. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/volume.py +0 -0
  116. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
  117. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/branding.py +0 -0
  118. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/common.py +0 -0
  119. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/crawl.py +0 -0
  120. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/fetch.py +0 -0
  121. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/search.py +0 -0
  122. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/webmcp.py +0 -0
  123. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/py.typed +0 -0
  124. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/sandbox_common.py +0 -0
  125. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/__init__.py +0 -0
  126. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/anthropic.py +0 -0
  127. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/gemini.py +0 -0
  128. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/openai.py +0 -0
  129. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/schema.py +0 -0
  130. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/transport/async_transport.py +0 -0
  131. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/transport/base.py +0 -0
  132. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/transport/sync.py +0 -0
  133. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/_json.py +0 -0
  134. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/agents.py +0 -0
  135. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/computer_action.py +0 -0
  136. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/crawl.py +0 -0
  137. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/extension.py +0 -0
  138. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/extract.py +0 -0
  139. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/profile.py +0 -0
  140. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/scrape.py +0 -0
  141. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/session.py +0 -0
  142. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/volume.py +0 -0
  143. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/web.py +0 -0
  144. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/webmcp.py +0 -0
  145. {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/vaults_common.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 1.14.0
3
+ Version: 1.15.0
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -268,7 +268,9 @@ Things to know:
268
268
  - Attaching secrets needs an API key with `secrets.use`; filling also needs
269
269
  `browser.sessions.update`. The team needs the secrets feature. Sessions with
270
270
  credentials run as Firecracker browsers with Chrome's password manager off.
271
- A vault attaches the secrets its entries hold when the session is created.
271
+ A vault is attached by reference, so each fill uses the secrets its entries
272
+ name at that moment; `session.vaults` lists the attached vaults as `id` and
273
+ `name`.
272
274
  - Fills are recorded in the session's event log as `credential_filled` and
273
275
  `credential_fill_refused`. Their metadata names the `credential` or `vault`;
274
276
  for 1Password fills, `onePasswordToken` (the op_token's name) and its
@@ -282,9 +284,12 @@ Things to know:
282
284
  (409) is the same for frames.
283
285
  - `CREDENTIAL_ORIGIN_NOT_ALLOWED` (403): the page's origin (in `details`)
284
286
  isn't allowed; nothing was typed.
285
- - `CREDENTIAL_NOT_ATTACHED` (403): the secret (or a 1Password fill's
286
- op_token) wasn't attached when the session was created, or a vault entry
287
- now points at another secret.
287
+ - `CREDENTIAL_NOT_ATTACHED` (403): the secret, vault or 1Password fill's
288
+ op_token wasn't attached to the session, directly or as an entry of an
289
+ attached vault.
290
+ - `SECRET_REQUIRES_CUSTOMER_UNWRAP` (400): a customer-protected secret
291
+ can't be typed into a page (see
292
+ [Customer-protected secrets](#customer-protected-secrets)).
288
293
  - `CREDENTIAL_NOT_READY` (409): the secret has no allowed origins or no
289
294
  value.
290
295
  - `SECRET_TYPE_MISMATCH` (409): an `op_token` can't be attached or filled
@@ -857,6 +862,105 @@ Things to know:
857
862
  `SECRET_ONEPASSWORD_FAILED` (502/503) are retryable.
858
863
  - `retryable` is the API's flag when it sends one.
859
864
 
865
+ ### Customer-protected secrets
866
+
867
+ A secret created with `protection="customer"` is also wrapped with your own
868
+ key, so the platform stores nothing it can open alone. Each version is sealed
869
+ to the platform key as usual, then wrapped locally with your key. To use one,
870
+ unwrap it locally and pass the result. Only the platform can open the
871
+ unwrapped envelope, and it checks that the envelope belongs to the version
872
+ you pinned.
873
+
874
+ ```python
875
+ import os
876
+
877
+ from hyperbrowser import Hyperbrowser, create_customer_key
878
+
879
+ client = Hyperbrowser()
880
+
881
+ # RSA (2048 bits or more), EC P-256/P-384/P-521, X25519 or X448: a
882
+ # cryptography key object, PEM/DER or a JWK dict. A public key can wrap;
883
+ # unwrapping needs the private key.
884
+ customer_key = create_customer_key(os.environ["HB_CUSTOMER_KEY_PEM"])
885
+
886
+ client.secrets.create({"name": "db-pass", "value": "hunter2", "customer_key": customer_key})
887
+ client.secrets.add_version("db-pass", "hunter3", customer_key=customer_key)
888
+
889
+ # A reference pinned to the version unwrapped: name, version and unwrapped.
890
+ db_pass = client.secrets.unwrap("db-pass", customer_key)
891
+ sandbox = client.sandboxes.create({"image_name": "my-image", "secrets": {"DB_PASS": db_pass}})
892
+ sandbox.exec("python migrate.py", secrets={"DB_PASS": db_pass})
893
+
894
+ # A vault's customer-protected entries, unwrapped together. Its other entries
895
+ # are delivered as usual.
896
+ app_env = client.vaults.unwrap("app-env", customer_key)
897
+ sandbox.processes.start("python worker.py", vault=app_env)
898
+
899
+ # A customer-protected op_token, for sandboxes and 1Password fills.
900
+ token = client.secrets.unwrap("op-token", customer_key)
901
+ sandbox.exec(
902
+ "python app.py",
903
+ one_password={
904
+ "token": token,
905
+ "vault": "Engineering",
906
+ "fields": {"DB_PASS": {"item": "Postgres", "field": "password"}},
907
+ },
908
+ )
909
+ ```
910
+
911
+ - `protection` is `"platform"` (the default) or `"customer"`, and is fixed when
912
+ the secret is created. Passing `customer_key` to `create` implies
913
+ `"customer"`. Creating one without a value gives an empty secret; add its
914
+ first version with `add_version(secret, value, customer_key=...)`.
915
+ - The key's `kid` is its RFC 7638 thumbprint unless the JWK has a `kid` or you
916
+ pass `create_customer_key(key, kid=...)`. RSA keys wrap with `RSA-OAEP-256`.
917
+ EC and OKP keys default to `ECDH-ES+A256KW`; pass `algorithm="ECDH-ES"` for
918
+ direct key agreement. To keep the private key in a KMS or HSM, implement the
919
+ `CustomerKey` protocol yourself: `kid`, `wrap(inner_envelope,
920
+ encryption_context)` and `unwrap(wrapped_envelope)` produce and open a
921
+ compact JWE with protected header `{"alg", "enc": "A256GCM", "kid", "ctx"}`.
922
+ `ctx` must be the secret's `encryption_context`. With `AsyncHyperbrowser`,
923
+ either method may be a coroutine; the sync client refuses one.
924
+ - `unwrap(secret, keys, version=...)` unwraps the latest version by default.
925
+ `vaults.unwrap(vault, keys)` unwraps the versions the vault's entries
926
+ resolve to now. Both take a list of keys, such as during a rotation, and use
927
+ the one whose `kid` wrapped each version (`customer_key_mismatch` if none
928
+ did). The unwrapped envelope is checked against the version's
929
+ `inner_digest` locally, and again by the API.
930
+ - An unwrapped reference must pin a number, never `"latest"`. A vault
931
+ reference is refused with `SECRET_VAULT_CHANGED` when an entry was repointed
932
+ or moved to a newer version since it was unwrapped; unwrap it again.
933
+ - Customer-protected secrets can't be typed into browser pages, including as
934
+ vault entries in a fill (`SECRET_REQUIRES_CUSTOMER_UNWRAP`). A
935
+ customer-protected `op_token` can still sign 1Password fills: pass the
936
+ unwrapped reference as `one_password`'s `token`.
937
+ - Rotate your key with `rewrap(secret, from_key=..., to_key=...)`. It unwraps
938
+ each version still wrapped with another key and rewraps it with `to_key`,
939
+ all locally; the value and the version numbers don't change. It skips
940
+ versions already wrapped with `to_key`, so an interrupted rotation resumes
941
+ where it stopped. `get_wrapped_version` and `replace_version_wrapping` are
942
+ the single-version steps, for wrapping done elsewhere.
943
+ - Version details include `outer_kid`, the customer key that wrapped them, and
944
+ `retires_at`. A version whose platform key is retired is refused with
945
+ `SECRET_KEY_RETIRED`: add a new version.
946
+ - Fetching wrapped versions needs `secrets.use`, and rewrapping needs
947
+ `secrets.manage`. Signed-in users must be team owners or admins.
948
+ - Error codes from the API:
949
+ - `SECRET_REQUIRES_CUSTOMER_UNWRAP` (400): a customer-protected secret was
950
+ used without its unwrapped envelope, unwrapped with `"latest"` instead of a
951
+ pinned version, or used where it can't be (a fill).
952
+ - `SECRET_PROTECTION_MISMATCH` (400): an unwrapped envelope for a secret
953
+ that isn't customer-protected, or a version uploaded in the other
954
+ protection's form.
955
+ - `SECRET_DIGEST_MISMATCH` (409): the unwrapped envelope (or a rewrap's
956
+ `inner_digest`) doesn't match the version.
957
+ - `SECRET_VERSION_DUPLICATE` (409): another version already holds this
958
+ inner envelope; rewrap that version instead.
959
+ - `SECRET_VAULT_CHANGED` (409): a vault entry resolves to another secret or
960
+ version than the one unwrapped; unwrap the vault again.
961
+ - `SECRET_KEY_RETIRED` (409): the version was sealed to a retired platform
962
+ key; add a new version.
963
+
860
964
  ### List sandboxes with filters
861
965
 
862
966
  ```python
@@ -239,7 +239,9 @@ Things to know:
239
239
  - Attaching secrets needs an API key with `secrets.use`; filling also needs
240
240
  `browser.sessions.update`. The team needs the secrets feature. Sessions with
241
241
  credentials run as Firecracker browsers with Chrome's password manager off.
242
- A vault attaches the secrets its entries hold when the session is created.
242
+ A vault is attached by reference, so each fill uses the secrets its entries
243
+ name at that moment; `session.vaults` lists the attached vaults as `id` and
244
+ `name`.
243
245
  - Fills are recorded in the session's event log as `credential_filled` and
244
246
  `credential_fill_refused`. Their metadata names the `credential` or `vault`;
245
247
  for 1Password fills, `onePasswordToken` (the op_token's name) and its
@@ -253,9 +255,12 @@ Things to know:
253
255
  (409) is the same for frames.
254
256
  - `CREDENTIAL_ORIGIN_NOT_ALLOWED` (403): the page's origin (in `details`)
255
257
  isn't allowed; nothing was typed.
256
- - `CREDENTIAL_NOT_ATTACHED` (403): the secret (or a 1Password fill's
257
- op_token) wasn't attached when the session was created, or a vault entry
258
- now points at another secret.
258
+ - `CREDENTIAL_NOT_ATTACHED` (403): the secret, vault or 1Password fill's
259
+ op_token wasn't attached to the session, directly or as an entry of an
260
+ attached vault.
261
+ - `SECRET_REQUIRES_CUSTOMER_UNWRAP` (400): a customer-protected secret
262
+ can't be typed into a page (see
263
+ [Customer-protected secrets](#customer-protected-secrets)).
259
264
  - `CREDENTIAL_NOT_READY` (409): the secret has no allowed origins or no
260
265
  value.
261
266
  - `SECRET_TYPE_MISMATCH` (409): an `op_token` can't be attached or filled
@@ -828,6 +833,105 @@ Things to know:
828
833
  `SECRET_ONEPASSWORD_FAILED` (502/503) are retryable.
829
834
  - `retryable` is the API's flag when it sends one.
830
835
 
836
+ ### Customer-protected secrets
837
+
838
+ A secret created with `protection="customer"` is also wrapped with your own
839
+ key, so the platform stores nothing it can open alone. Each version is sealed
840
+ to the platform key as usual, then wrapped locally with your key. To use one,
841
+ unwrap it locally and pass the result. Only the platform can open the
842
+ unwrapped envelope, and it checks that the envelope belongs to the version
843
+ you pinned.
844
+
845
+ ```python
846
+ import os
847
+
848
+ from hyperbrowser import Hyperbrowser, create_customer_key
849
+
850
+ client = Hyperbrowser()
851
+
852
+ # RSA (2048 bits or more), EC P-256/P-384/P-521, X25519 or X448: a
853
+ # cryptography key object, PEM/DER or a JWK dict. A public key can wrap;
854
+ # unwrapping needs the private key.
855
+ customer_key = create_customer_key(os.environ["HB_CUSTOMER_KEY_PEM"])
856
+
857
+ client.secrets.create({"name": "db-pass", "value": "hunter2", "customer_key": customer_key})
858
+ client.secrets.add_version("db-pass", "hunter3", customer_key=customer_key)
859
+
860
+ # A reference pinned to the version unwrapped: name, version and unwrapped.
861
+ db_pass = client.secrets.unwrap("db-pass", customer_key)
862
+ sandbox = client.sandboxes.create({"image_name": "my-image", "secrets": {"DB_PASS": db_pass}})
863
+ sandbox.exec("python migrate.py", secrets={"DB_PASS": db_pass})
864
+
865
+ # A vault's customer-protected entries, unwrapped together. Its other entries
866
+ # are delivered as usual.
867
+ app_env = client.vaults.unwrap("app-env", customer_key)
868
+ sandbox.processes.start("python worker.py", vault=app_env)
869
+
870
+ # A customer-protected op_token, for sandboxes and 1Password fills.
871
+ token = client.secrets.unwrap("op-token", customer_key)
872
+ sandbox.exec(
873
+ "python app.py",
874
+ one_password={
875
+ "token": token,
876
+ "vault": "Engineering",
877
+ "fields": {"DB_PASS": {"item": "Postgres", "field": "password"}},
878
+ },
879
+ )
880
+ ```
881
+
882
+ - `protection` is `"platform"` (the default) or `"customer"`, and is fixed when
883
+ the secret is created. Passing `customer_key` to `create` implies
884
+ `"customer"`. Creating one without a value gives an empty secret; add its
885
+ first version with `add_version(secret, value, customer_key=...)`.
886
+ - The key's `kid` is its RFC 7638 thumbprint unless the JWK has a `kid` or you
887
+ pass `create_customer_key(key, kid=...)`. RSA keys wrap with `RSA-OAEP-256`.
888
+ EC and OKP keys default to `ECDH-ES+A256KW`; pass `algorithm="ECDH-ES"` for
889
+ direct key agreement. To keep the private key in a KMS or HSM, implement the
890
+ `CustomerKey` protocol yourself: `kid`, `wrap(inner_envelope,
891
+ encryption_context)` and `unwrap(wrapped_envelope)` produce and open a
892
+ compact JWE with protected header `{"alg", "enc": "A256GCM", "kid", "ctx"}`.
893
+ `ctx` must be the secret's `encryption_context`. With `AsyncHyperbrowser`,
894
+ either method may be a coroutine; the sync client refuses one.
895
+ - `unwrap(secret, keys, version=...)` unwraps the latest version by default.
896
+ `vaults.unwrap(vault, keys)` unwraps the versions the vault's entries
897
+ resolve to now. Both take a list of keys, such as during a rotation, and use
898
+ the one whose `kid` wrapped each version (`customer_key_mismatch` if none
899
+ did). The unwrapped envelope is checked against the version's
900
+ `inner_digest` locally, and again by the API.
901
+ - An unwrapped reference must pin a number, never `"latest"`. A vault
902
+ reference is refused with `SECRET_VAULT_CHANGED` when an entry was repointed
903
+ or moved to a newer version since it was unwrapped; unwrap it again.
904
+ - Customer-protected secrets can't be typed into browser pages, including as
905
+ vault entries in a fill (`SECRET_REQUIRES_CUSTOMER_UNWRAP`). A
906
+ customer-protected `op_token` can still sign 1Password fills: pass the
907
+ unwrapped reference as `one_password`'s `token`.
908
+ - Rotate your key with `rewrap(secret, from_key=..., to_key=...)`. It unwraps
909
+ each version still wrapped with another key and rewraps it with `to_key`,
910
+ all locally; the value and the version numbers don't change. It skips
911
+ versions already wrapped with `to_key`, so an interrupted rotation resumes
912
+ where it stopped. `get_wrapped_version` and `replace_version_wrapping` are
913
+ the single-version steps, for wrapping done elsewhere.
914
+ - Version details include `outer_kid`, the customer key that wrapped them, and
915
+ `retires_at`. A version whose platform key is retired is refused with
916
+ `SECRET_KEY_RETIRED`: add a new version.
917
+ - Fetching wrapped versions needs `secrets.use`, and rewrapping needs
918
+ `secrets.manage`. Signed-in users must be team owners or admins.
919
+ - Error codes from the API:
920
+ - `SECRET_REQUIRES_CUSTOMER_UNWRAP` (400): a customer-protected secret was
921
+ used without its unwrapped envelope, unwrapped with `"latest"` instead of a
922
+ pinned version, or used where it can't be (a fill).
923
+ - `SECRET_PROTECTION_MISMATCH` (400): an unwrapped envelope for a secret
924
+ that isn't customer-protected, or a version uploaded in the other
925
+ protection's form.
926
+ - `SECRET_DIGEST_MISMATCH` (409): the unwrapped envelope (or a rewrap's
927
+ `inner_digest`) doesn't match the version.
928
+ - `SECRET_VERSION_DUPLICATE` (409): another version already holds this
929
+ inner envelope; rewrap that version instead.
930
+ - `SECRET_VAULT_CHANGED` (409): a vault entry resolves to another secret or
931
+ version than the one unwrapped; unwrap the vault again.
932
+ - `SECRET_KEY_RETIRED` (409): the version was sealed to a retired platform
933
+ key; add a new version.
934
+
831
935
  ### List sandboxes with filters
832
936
 
833
937
  ```python
@@ -0,0 +1,13 @@
1
+ from .client.sync import Hyperbrowser
2
+ from .client.async_client import AsyncHyperbrowser
3
+ from .config import ClientConfig
4
+ from .customer_key import CustomerKey, create_customer_key, secret_envelope_digest
5
+
6
+ __all__ = [
7
+ "Hyperbrowser",
8
+ "AsyncHyperbrowser",
9
+ "ClientConfig",
10
+ "CustomerKey",
11
+ "create_customer_key",
12
+ "secret_envelope_digest",
13
+ ]
@@ -81,6 +81,7 @@ from ..sandboxes.image_resolution import (
81
81
  )
82
82
  from ..sandboxes.shared import (
83
83
  OnePasswordSourceInput,
84
+ VaultSourceInput,
84
85
  SecretReferences,
85
86
  _build_sandbox_exposed_url,
86
87
  _copy_model,
@@ -337,7 +338,7 @@ class SandboxHandle:
337
338
  run_as: Optional[str] = None,
338
339
  max_output_bytes: int = 64 * 1024 * 1024,
339
340
  secrets: Optional[SecretReferences] = None,
340
- vault: Optional[str] = None,
341
+ vault: Optional[VaultSourceInput] = None,
341
342
  one_password: Optional[OnePasswordSourceInput] = None,
342
343
  ):
343
344
  """Run a command to completion.
@@ -345,6 +346,8 @@ class SandboxHandle:
345
346
  ``secrets`` maps environment variable names to stored secrets (a name,
346
347
  or ``{"name": ..., "version": N}``). ``vault`` names a vault whose
347
348
  entries become variables too; ``secrets`` win on a clash.
349
+ Customer-protected secrets are passed as the references
350
+ ``secrets.unwrap()`` and ``vaults.unwrap()`` return.
348
351
  ``one_password`` sets variables from fields of a 1Password vault
349
352
  (``{"token": op_token_secret, "vault": ..., "fields": {ENV: {"item",
350
353
  "section", "field"}}}``); those variables may come from nothing else.
@@ -27,6 +27,7 @@ from .....types import (
27
27
  )
28
28
  from ...sandboxes.shared import (
29
29
  OnePasswordSourceInput,
30
+ VaultSourceInput,
30
31
  SecretReferences,
31
32
  _normalize_exec_params,
32
33
  _runtime_process_payload,
@@ -355,7 +356,7 @@ class SandboxProcessesApi:
355
356
  run_as: Optional[str] = None,
356
357
  max_output_bytes: int = DEFAULT_MAX_PROCESS_OUTPUT_BYTES,
357
358
  secrets: Optional[SecretReferences] = None,
358
- vault: Optional[str] = None,
359
+ vault: Optional[VaultSourceInput] = None,
359
360
  one_password: Optional[OnePasswordSourceInput] = None,
360
361
  ) -> SandboxProcessResult:
361
362
  handle = await self.start(
@@ -388,7 +389,7 @@ class SandboxProcessesApi:
388
389
  run_as: Optional[str] = None,
389
390
  max_output_bytes: int = DEFAULT_MAX_PROCESS_OUTPUT_BYTES,
390
391
  secrets: Optional[SecretReferences] = None,
391
- vault: Optional[str] = None,
392
+ vault: Optional[VaultSourceInput] = None,
392
393
  one_password: Optional[OnePasswordSourceInput] = None,
393
394
  ) -> SandboxProcessHandle:
394
395
  """Start a process.
@@ -1,20 +1,33 @@
1
1
  import asyncio
2
- from typing import Dict, Optional, Sequence, Union
2
+ from typing import Dict, List, Literal, Optional, Sequence, Union
3
3
 
4
4
  from ..._request import coerce_request, dump_request
5
+ from ....customer_key import (
6
+ CustomerKey,
7
+ CustomerKeys,
8
+ check_customer_key,
9
+ check_customer_keys,
10
+ check_unwrapped,
11
+ customer_key_for,
12
+ secret_envelope_digest,
13
+ async_result,
14
+ )
5
15
  from ....exceptions import HyperbrowserError
6
16
  from ....models.secret import (
7
17
  CreateSecretParams,
18
+ ReplaceSecretVersionWrappingParams,
8
19
  Secret,
9
20
  SecretDeleteResult,
10
21
  SecretDetail,
11
22
  SecretListParams,
12
23
  SecretListResponse,
24
+ SecretReference,
13
25
  SecretsPublicKey,
14
26
  SecretVersion,
15
27
  SecretValue,
16
28
  SecretVersionDeleteResult,
17
29
  UpdateSecretParams,
30
+ WrappedSecretVersion,
18
31
  )
19
32
  from ....sandbox_common import (
20
33
  ensure_response_ok,
@@ -28,7 +41,11 @@ from ....secrets_common import (
28
41
  SECRET_NAME_LOOKUP_LIMIT,
29
42
  SecretsSealer,
30
43
  browser_metadata,
44
+ check_customer_protection,
31
45
  check_secret_version,
46
+ check_secret_version_or_latest,
47
+ create_secret_protection,
48
+ invalid_request,
32
49
  is_not_found,
33
50
  looks_like_secret_id,
34
51
  secret_not_found_error,
@@ -37,6 +54,9 @@ from ....secrets_common import (
37
54
  update_secret_body,
38
55
  )
39
56
  from ....types import CreateSecretParams as CreateSecretParamsDict
57
+ from ....types import (
58
+ ReplaceSecretVersionWrappingParams as ReplaceSecretVersionWrappingParamsDict,
59
+ )
40
60
  from ....types import SecretListParams as SecretListParamsDict
41
61
  from ....types import UpdateSecretParams as UpdateSecretParamsDict
42
62
 
@@ -47,6 +67,11 @@ class SecretManager:
47
67
  Values are encrypted on this machine to the platform's public key and are
48
68
  never returned by the API. Methods that take ``secret`` accept a secret's
49
69
  ID or its exact name.
70
+
71
+ A customer-protected secret's versions are also wrapped locally with your
72
+ own key (``create_customer_key()``, or your own ``CustomerKey``), so the
73
+ platform stores nothing it can open alone. Each use passes the reference
74
+ ``unwrap()`` returns.
50
75
  """
51
76
 
52
77
  def __init__(self, client):
@@ -80,10 +105,14 @@ class SecretManager:
80
105
  account token, which starts with ``ops_``). ``allowed_origins`` makes
81
106
  a ``secret`` or ``totp`` fillable into browser pages on those origins.
82
107
 
108
+ ``protection="customer"`` wraps each version with your own key as
109
+ well; passing ``customer_key`` implies it, and wraps the first version.
110
+
83
111
  The value and the public key are checked before the secret is created,
84
112
  so a bad value or an unavailable key leaves nothing behind.
85
113
  """
86
114
  normalized = coerce_request(params, CreateSecretParams)
115
+ protection = create_secret_protection(normalized)
87
116
  payload = None
88
117
  sealer = None
89
118
  if normalized.value is not None:
@@ -91,17 +120,16 @@ class SecretManager:
91
120
  normalized.type or "secret", normalized.value
92
121
  )
93
122
  sealer = SecretsSealer(await self.get_public_key())
94
- secret = Secret(
95
- **await self._request(
96
- "POST",
97
- "/secrets",
98
- data=normalized.model_dump(by_alias=True, exclude_none=True),
99
- )
100
- )
123
+ body = normalized.model_dump(by_alias=True, exclude_none=True)
124
+ if protection is not None:
125
+ body["protection"] = protection
126
+ secret = Secret(**await self._request("POST", "/secrets", data=body))
101
127
  if sealer is None or payload is None:
102
128
  return secret
103
129
  try:
104
- version = await self._add_version(secret, payload, sealer)
130
+ version = await self._add_version(
131
+ secret, payload, sealer, normalized.customer_key
132
+ )
105
133
  except Exception as error:
106
134
  raise secret_value_not_stored_error(secret.id, secret.name, error)
107
135
  return secret.model_copy(update={"latest_version": version.version})
@@ -136,17 +164,136 @@ class SecretManager:
136
164
  secret, {"metadata": browser_metadata(allowed_origins)}
137
165
  )
138
166
 
139
- async def add_version(self, secret: str, value: SecretValue) -> SecretVersion:
167
+ async def add_version(
168
+ self,
169
+ secret: str,
170
+ value: SecretValue,
171
+ *,
172
+ customer_key: Optional[CustomerKey] = None,
173
+ ) -> SecretVersion:
140
174
  """Encrypt ``value`` locally and store it as the secret's next version.
141
175
 
142
176
  The value is checked against the secret's type first: a ``totp``
143
- secret takes a base32 seed or an ``otpauth://totp/`` URI.
177
+ secret takes a base32 seed or an ``otpauth://totp/`` URI. A
178
+ customer-protected secret's value is also wrapped with
179
+ ``customer_key``.
144
180
  """
145
181
  secret_payload_bytes(None, value)
182
+ if customer_key is not None:
183
+ check_customer_key(customer_key)
146
184
  resolved = (await self._resolve(secret))[0]
185
+ check_customer_protection(resolved, customer_key)
147
186
  payload = secret_payload_bytes(resolved.type, value)
148
187
  return await self._add_version(
149
- resolved, payload, SecretsSealer(await self.get_public_key())
188
+ resolved, payload, SecretsSealer(await self.get_public_key()), customer_key
189
+ )
190
+
191
+ async def get_wrapped_version(
192
+ self, secret: str, version: Union[int, Literal["latest"]] = "latest"
193
+ ) -> WrappedSecretVersion:
194
+ """A customer-protected version's wrapped envelope, for unwrapping with
195
+ your own key. Most callers want ``unwrap()``, which also unwraps it."""
196
+ check_secret_version_or_latest(version)
197
+ secret_id = (await self._resolve(secret))[0].id
198
+ return await self._get_wrapped(secret_id, version)
199
+
200
+ async def unwrap(
201
+ self,
202
+ secret: str,
203
+ customer_key: CustomerKeys,
204
+ *,
205
+ version: Union[int, Literal["latest"]] = "latest",
206
+ ) -> SecretReference:
207
+ """Unwrap a customer-protected version locally with your own key.
208
+
209
+ Returns the reference each use passes: as a sandbox's or process's
210
+ ``secrets`` entry, or as a 1Password ``token``. It is pinned to the
211
+ version unwrapped. The unwrapped envelope is checked against the
212
+ digest stored with the version, and only the platform can open it.
213
+ ``customer_key`` may be a list, such as during a rotation; the key
214
+ whose ``kid`` wrapped the version is used.
215
+ """
216
+ check_secret_version_or_latest(version)
217
+ check_customer_keys(customer_key)
218
+ resolved = (await self._resolve(secret))[0]
219
+ wrapped = await self._get_wrapped(resolved.id, version)
220
+ inner = await self._unwrap(
221
+ wrapped,
222
+ customer_key,
223
+ f"Version {wrapped.version} of secret {resolved.name}",
224
+ )
225
+ return SecretReference(
226
+ name=resolved.name, version=wrapped.version, unwrapped=inner
227
+ )
228
+
229
+ async def rewrap(
230
+ self,
231
+ secret: str,
232
+ *,
233
+ from_key: CustomerKeys,
234
+ to_key: CustomerKey,
235
+ versions: Optional[Sequence[int]] = None,
236
+ ) -> List[WrappedSecretVersion]:
237
+ """Rewrap a customer-protected secret's versions with a new key, for
238
+ rotating your own key.
239
+
240
+ Each version is unwrapped with ``from_key`` and wrapped with
241
+ ``to_key`` locally; the value never changes and the platform never
242
+ sees it. Without ``versions``, every version not yet wrapped with
243
+ ``to_key`` is rewrapped, so an interrupted rotation can be resumed.
244
+ Returns the versions rewrapped.
245
+ """
246
+ check_customer_keys(from_key, "from_key")
247
+ check_customer_key(to_key, "to_key")
248
+ if versions is not None:
249
+ if isinstance(versions, (str, bytes)):
250
+ raise invalid_request("versions must be a list of version numbers")
251
+ for version in versions:
252
+ check_secret_version(version)
253
+ detail = await self.get(secret)
254
+ if detail.protection != "customer":
255
+ raise invalid_request(
256
+ f"Secret {detail.name} is not customer-protected; it has no wrapping"
257
+ )
258
+ wanted = (
259
+ list(versions)
260
+ if versions is not None
261
+ else [
262
+ item.version for item in detail.versions if item.outer_kid != to_key.kid
263
+ ]
264
+ )
265
+ rewrapped = []
266
+ for version in wanted:
267
+ wrapped = await self._get_wrapped(detail.id, version)
268
+ inner = await self._unwrap(
269
+ wrapped, from_key, f"Version {version} of secret {detail.name}"
270
+ )
271
+ envelope = await async_result(
272
+ to_key.wrap(inner, detail.encryption_context), "wrap"
273
+ )
274
+ rewrapped.append(
275
+ await self._replace_wrapping(
276
+ detail.id, version, envelope, wrapped.inner_digest
277
+ )
278
+ )
279
+ return rewrapped
280
+
281
+ async def replace_version_wrapping(
282
+ self,
283
+ secret: str,
284
+ version: int,
285
+ params: Union[
286
+ ReplaceSecretVersionWrappingParamsDict, ReplaceSecretVersionWrappingParams
287
+ ],
288
+ ) -> WrappedSecretVersion:
289
+ """Replace a customer-protected version's outer envelope with one you
290
+ wrapped yourself. ``inner_digest`` must match the version's: only the
291
+ wrapping changes, never the value. ``rewrap()`` does this for you."""
292
+ check_secret_version(version)
293
+ request = coerce_request(params, ReplaceSecretVersionWrappingParams)
294
+ secret_id = (await self._resolve(secret))[0].id
295
+ return await self._replace_wrapping(
296
+ secret_id, version, request.envelope, request.inner_digest
150
297
  )
151
298
 
152
299
  async def delete(self, secret: str) -> SecretDeleteResult:
@@ -169,14 +316,54 @@ class SecretManager:
169
316
  return SecretVersionDeleteResult(**payload)
170
317
 
171
318
  async def _add_version(
172
- self, secret: Secret, payload: bytes, sealer: SecretsSealer
319
+ self,
320
+ secret: Secret,
321
+ payload: bytes,
322
+ sealer: SecretsSealer,
323
+ customer_key: Optional[CustomerKey] = None,
173
324
  ) -> SecretVersion:
174
325
  # Always the context the API returned; it differs by secret type.
175
326
  envelope = sealer.seal(secret.encryption_context, payload)
327
+ data: Dict[str, object] = {"envelope": envelope}
328
+ if customer_key is not None:
329
+ data = {
330
+ "envelope": await async_result(
331
+ customer_key.wrap(envelope, secret.encryption_context), "wrap"
332
+ ),
333
+ "innerDigest": secret_envelope_digest(envelope),
334
+ "innerKid": sealer.kid,
335
+ }
336
+ response = await self._request(
337
+ "POST", f"/secrets/{secret.id}/versions", data=data
338
+ )
339
+ return SecretVersion(**response)
340
+
341
+ async def _get_wrapped(
342
+ self, secret_id: str, version: Union[int, str]
343
+ ) -> WrappedSecretVersion:
344
+ return WrappedSecretVersion(
345
+ **await self._request(
346
+ "GET", f"/secrets/{secret_id}/versions/{version}/wrapped"
347
+ )
348
+ )
349
+
350
+ async def _replace_wrapping(
351
+ self, secret_id: str, version: int, envelope: str, inner_digest: str
352
+ ) -> WrappedSecretVersion:
176
353
  payload = await self._request(
177
- "POST", f"/secrets/{secret.id}/versions", data={"envelope": envelope}
354
+ "PUT",
355
+ f"/secrets/{secret_id}/versions/{version}/wrapping",
356
+ data={"envelope": envelope, "innerDigest": inner_digest},
178
357
  )
179
- return SecretVersion(**payload)
358
+ return WrappedSecretVersion(**payload)
359
+
360
+ @staticmethod
361
+ async def _unwrap(
362
+ wrapped: WrappedSecretVersion, keys: CustomerKeys, label: str
363
+ ) -> str:
364
+ key = customer_key_for(keys, wrapped.outer_kid, label)
365
+ inner = await async_result(key.unwrap(wrapped.envelope), "unwrap")
366
+ return check_unwrapped(inner, wrapped.inner_digest, label)
180
367
 
181
368
  async def _get_by_id(self, secret_id: str) -> SecretDetail:
182
369
  return SecretDetail(**await self._request("GET", f"/secrets/{secret_id}"))