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.
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/PKG-INFO +109 -5
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/README.md +108 -4
- hyperbrowser-1.15.0/hyperbrowser/__init__.py +13 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandbox.py +4 -1
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +3 -2
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/secret.py +202 -15
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/vault.py +35 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/shared.py +8 -2
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +4 -1
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +3 -2
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/secret.py +196 -15
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/vault.py +35 -0
- hyperbrowser-1.15.0/hyperbrowser/customer_key.py +705 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/__init__.py +16 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/consts.py +2 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/credential.py +6 -5
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/sandbox.py +8 -4
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/secret.py +81 -5
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/session.py +14 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/vault.py +89 -1
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/secrets_common.py +50 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/__init__.py +8 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/credential.py +3 -3
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/sandbox.py +3 -2
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/secret.py +29 -3
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/vault.py +18 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/pyproject.toml +1 -1
- hyperbrowser-1.14.0/hyperbrowser/__init__.py +0 -5
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/LICENSE +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/build_context.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/_request.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/async_client.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/session_credentials.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/webmcp.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/image_build.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/image_resolution.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/session_credentials.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/sync_manager/webmcp.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/sync.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/config.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/credentials_common.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/image_builds.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/cua.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/volume.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/models/webmcp.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/py.typed +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/sandbox_common.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/__init__.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/anthropic.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/gemini.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/openai.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/tools/schema.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/transport/async_transport.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/transport/base.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/transport/sync.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/_json.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/agents.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/computer_action.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/crawl.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/extension.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/extract.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/profile.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/scrape.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/session.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/volume.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/web.py +0 -0
- {hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/types/webmcp.py +0 -0
- {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.
|
|
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
|
|
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
|
|
286
|
-
op_token
|
|
287
|
-
|
|
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
|
|
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
|
|
257
|
-
op_token
|
|
258
|
-
|
|
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
|
+
]
|
{hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/sandbox.py
RENAMED
|
@@ -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[
|
|
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[
|
|
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[
|
|
392
|
+
vault: Optional[VaultSourceInput] = None,
|
|
392
393
|
one_password: Optional[OnePasswordSourceInput] = None,
|
|
393
394
|
) -> SandboxProcessHandle:
|
|
394
395
|
"""Start a process.
|
{hyperbrowser-1.14.0 → hyperbrowser-1.15.0}/hyperbrowser/client/managers/async_manager/secret.py
RENAMED
|
@@ -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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
"
|
|
354
|
+
"PUT",
|
|
355
|
+
f"/secrets/{secret_id}/versions/{version}/wrapping",
|
|
356
|
+
data={"envelope": envelope, "innerDigest": inner_digest},
|
|
178
357
|
)
|
|
179
|
-
return
|
|
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}"))
|