@hyperbrowser/sdk 1.4.0 → 1.5.0

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.
@@ -1,4 +1,4 @@
1
- import { SandboxSecrets } from "../types/sandbox";
1
+ import { SandboxOnePassword, SandboxSecrets, SandboxVaultSource } from "../types/sandbox";
2
2
  /** Names of secrets and vaults. */
3
3
  export declare const SECRET_NAME_PATTERN: RegExp;
4
4
  /** The largest version number the API stores. */
@@ -8,12 +8,34 @@ export declare const isSecretEnvName: (name: unknown) => name is string;
8
8
  export interface WireSecretReference {
9
9
  name: string;
10
10
  version?: number | "latest";
11
+ unwrapped?: string;
12
+ }
13
+ export interface WireVaultReference {
14
+ name: string;
15
+ unwrapped: Record<string, {
16
+ version: number;
17
+ unwrapped: string;
18
+ }>;
19
+ }
20
+ /** A vault as the API takes it: its name, or its name with unwrapped entries. */
21
+ export type WireVaultSource = string | WireVaultReference;
22
+ export interface WireOnePassword extends Omit<SandboxOnePassword, "token"> {
23
+ token: string | WireSecretReference;
11
24
  }
12
25
  /**
13
26
  * Check secret references locally and convert them to the API's
14
- * `{ name, version? }` shape. Returns undefined when there are none.
27
+ * `{ name, version?, unwrapped? }` shape. Returns undefined when there are none.
15
28
  */
16
29
  export declare const serializeSecretReferences: (secrets: SandboxSecrets | undefined) => Record<string, WireSecretReference> | undefined;
30
+ /**
31
+ * Check a vault source and convert it to the API's shape: a name is passed
32
+ * as is, and a `VaultReference` keeps only its name and unwrapped entries.
33
+ */
34
+ export declare const serializeVaultSource: (vault: SandboxVaultSource | undefined) => WireVaultSource | undefined;
35
+ /**
36
+ * Check a 1Password source's token, the one member that can be a secret
37
+ * reference; the API checks the rest.
38
+ */
39
+ export declare const serializeOnePassword: <T extends Pick<SandboxOnePassword, "token">>(onePassword: T | undefined) => (Omit<T, "token"> & Pick<WireOnePassword, "token">) | undefined;
17
40
  /** An environment variable must come from either `env` or `secrets`, not both. */
18
41
  export declare const assertNoEnvSecretOverlap: (env: Record<string, string> | undefined, secrets: Record<string, WireSecretReference>) => void;
19
- export type { SandboxOnePassword as WireOnePassword } from "../types/sandbox";
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.assertNoEnvSecretOverlap = exports.serializeSecretReferences = exports.isSecretEnvName = exports.MAX_SECRET_VERSION = exports.SECRET_NAME_PATTERN = void 0;
3
+ exports.assertNoEnvSecretOverlap = exports.serializeOnePassword = exports.serializeVaultSource = exports.serializeSecretReferences = exports.isSecretEnvName = exports.MAX_SECRET_VERSION = exports.SECRET_NAME_PATTERN = void 0;
4
4
  const error_1 = require("../error");
5
+ const secrets_envelope_1 = require("../secrets-envelope");
5
6
  const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
7
  /** The longest environment variable name the API stores. */
7
8
  const MAX_ENV_NAME_LENGTH = 256;
@@ -10,15 +11,44 @@ exports.SECRET_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
10
11
  /** The largest version number the API stores. */
11
12
  exports.MAX_SECRET_VERSION = 2147483647;
12
13
  const MAX_SECRETS = 32;
14
+ /** The longest unwrapped envelope the API takes. */
15
+ const MAX_UNWRAPPED_LENGTH = 64 * 1024;
13
16
  /** An environment variable name the API accepts for a secret or a vault entry. */
14
17
  const isSecretEnvName = (name) => typeof name === "string" &&
15
18
  name.length <= MAX_ENV_NAME_LENGTH &&
16
19
  ENV_NAME_PATTERN.test(name) &&
17
20
  name !== "__proto__";
18
21
  exports.isSecretEnvName = isSecretEnvName;
22
+ const isVersionNumber = (version) => Number.isSafeInteger(version) &&
23
+ version >= 1 &&
24
+ version <= exports.MAX_SECRET_VERSION;
25
+ const isUnwrappedEnvelope = (unwrapped) => typeof unwrapped === "string" &&
26
+ unwrapped.length >= 1 &&
27
+ unwrapped.length <= MAX_UNWRAPPED_LENGTH;
28
+ /** Check one secret reference and convert it to the API's `{ name, version?, unwrapped? }`. */
29
+ const serializeSecretReference = (reference, label) => {
30
+ const { name, version, unwrapped } = (typeof reference === "string" ? { name: reference } : (reference ?? {}));
31
+ if (typeof name !== "string" || !exports.SECRET_NAME_PATTERN.test(name)) {
32
+ throw new error_1.HyperbrowserError(`Invalid secret name for ${label}: ${String(name)}`);
33
+ }
34
+ if (version !== undefined && version !== "latest" && !isVersionNumber(version)) {
35
+ throw new error_1.HyperbrowserError(`Secret version for ${label} must be an integer from 1 to ${exports.MAX_SECRET_VERSION} or "latest"`);
36
+ }
37
+ if (unwrapped === undefined) {
38
+ return version === undefined ? { name } : { name, version: version };
39
+ }
40
+ if (!isUnwrappedEnvelope(unwrapped)) {
41
+ throw new error_1.HyperbrowserError(`The unwrapped envelope for ${label} must be a string of 1 to ${MAX_UNWRAPPED_LENGTH} characters`);
42
+ }
43
+ // An unwrapped envelope is one version's value; "latest" could move on.
44
+ if (typeof version !== "number") {
45
+ throw new error_1.HyperbrowserError(`Pin the version of ${label} to the version you unwrapped when passing "unwrapped"`, { code: "invalid_request" });
46
+ }
47
+ return { name, version, unwrapped };
48
+ };
19
49
  /**
20
50
  * Check secret references locally and convert them to the API's
21
- * `{ name, version? }` shape. Returns undefined when there are none.
51
+ * `{ name, version?, unwrapped? }` shape. Returns undefined when there are none.
22
52
  */
23
53
  const serializeSecretReferences = (secrets) => {
24
54
  if (secrets === undefined || secrets === null) {
@@ -39,22 +69,64 @@ const serializeSecretReferences = (secrets) => {
39
69
  if (!(0, exports.isSecretEnvName)(env)) {
40
70
  throw new error_1.HyperbrowserError(`Invalid environment variable name for a secret: ${env}`);
41
71
  }
42
- const { name, version } = typeof reference === "string" ? { name: reference, version: undefined } : (reference ?? {});
43
- if (typeof name !== "string" || !exports.SECRET_NAME_PATTERN.test(name)) {
44
- throw new error_1.HyperbrowserError(`Invalid secret name for ${env}: ${String(name)}`);
45
- }
46
- if (version !== undefined &&
47
- version !== "latest" &&
48
- (!Number.isSafeInteger(version) ||
49
- version < 1 ||
50
- version > exports.MAX_SECRET_VERSION)) {
51
- throw new error_1.HyperbrowserError(`Secret version for ${env} must be an integer from 1 to ${exports.MAX_SECRET_VERSION} or "latest"`);
52
- }
53
- wire[env] = version === undefined ? { name } : { name, version };
72
+ wire[env] = serializeSecretReference(reference, env);
54
73
  }
55
74
  return wire;
56
75
  };
57
76
  exports.serializeSecretReferences = serializeSecretReferences;
77
+ /**
78
+ * Check a vault source and convert it to the API's shape: a name is passed
79
+ * as is, and a `VaultReference` keeps only its name and unwrapped entries.
80
+ */
81
+ const serializeVaultSource = (vault) => {
82
+ if (vault === undefined || typeof vault === "string") {
83
+ return vault;
84
+ }
85
+ if (!vault || typeof vault !== "object" || Array.isArray(vault)) {
86
+ throw new error_1.HyperbrowserError("vault must be a vault's name or the reference vaults.unwrap() returns");
87
+ }
88
+ const { name, unwrapped } = vault;
89
+ if (typeof name !== "string" || !exports.SECRET_NAME_PATTERN.test(name)) {
90
+ throw new error_1.HyperbrowserError(`Invalid vault name: ${String(name)}`);
91
+ }
92
+ if (!unwrapped || typeof unwrapped !== "object" || Array.isArray(unwrapped)) {
93
+ throw new error_1.HyperbrowserError(`vault.unwrapped must map vault ${name}'s variables to { version, unwrapped }`);
94
+ }
95
+ const entries = Object.entries(unwrapped);
96
+ if (entries.length > MAX_SECRETS) {
97
+ throw new error_1.HyperbrowserError(`At most ${MAX_SECRETS} unwrapped vault entries are allowed`);
98
+ }
99
+ const wire = {};
100
+ for (const [env, entry] of entries) {
101
+ if (!(0, exports.isSecretEnvName)(env)) {
102
+ throw new error_1.HyperbrowserError(`Invalid environment variable name in vault ${name}: ${env}`);
103
+ }
104
+ const { version, unwrapped: envelope } = (entry ?? {});
105
+ if (!isVersionNumber(version)) {
106
+ throw new error_1.HyperbrowserError(`The version of vault ${name}'s ${env} must be the integer it was unwrapped from`);
107
+ }
108
+ if (!isUnwrappedEnvelope(envelope)) {
109
+ throw new error_1.HyperbrowserError(`The unwrapped envelope of vault ${name}'s ${env} must be a string of 1 to ${MAX_UNWRAPPED_LENGTH} characters`);
110
+ }
111
+ wire[env] = { version, unwrapped: envelope };
112
+ }
113
+ return { name, unwrapped: wire };
114
+ };
115
+ exports.serializeVaultSource = serializeVaultSource;
116
+ /**
117
+ * Check a 1Password source's token, the one member that can be a secret
118
+ * reference; the API checks the rest.
119
+ */
120
+ const serializeOnePassword = (onePassword) => {
121
+ if (!(0, secrets_envelope_1.isPlainObject)(onePassword))
122
+ return onePassword;
123
+ const { token } = onePassword;
124
+ return {
125
+ ...onePassword,
126
+ token: typeof token === "string" ? token : serializeSecretReference(token, "the 1Password token"),
127
+ };
128
+ };
129
+ exports.serializeOnePassword = serializeOnePassword;
58
130
  /** An environment variable must come from either `env` or `secrets`, not both. */
59
131
  const assertNoEnvSecretOverlap = (env, secrets) => {
60
132
  const overlap = Object.keys(env ?? {}).filter((name) => Object.prototype.hasOwnProperty.call(secrets, name));
@@ -13,7 +13,8 @@ export declare const secretValueBytes: (value: SecretValue) => Buffer;
13
13
  export declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
14
14
  /** Encrypts values to a verified secrets public key. */
15
15
  export declare class SecretsSealer {
16
- private readonly kid;
16
+ /** The platform key's ID, which every envelope it seals names. */
17
+ readonly kid: string;
17
18
  private readonly publicKey;
18
19
  private constructor();
19
20
  /**
@@ -62,7 +62,9 @@ const rsaThumbprint = (n, e) => base64Url(crypto_1.default
62
62
  .digest());
63
63
  /** Encrypts values to a verified secrets public key. */
64
64
  class SecretsSealer {
65
- constructor(kid, publicKey) {
65
+ constructor(
66
+ /** The platform key's ID, which every envelope it seals names. */
67
+ kid, publicKey) {
66
68
  this.kid = kid;
67
69
  this.publicKey = publicKey;
68
70
  }
@@ -13,7 +13,11 @@ export declare class CredentialsService extends BaseService {
13
13
  * `op_token` secret must have been attached at session creation as
14
14
  * `{ onePassword: name }`, and the request's `allowedOrigins` are the
15
15
  * origins the values may be typed on. Each field's result carries the
16
- * `reference` read, and there is no top-level `version`.
16
+ * `reference` read, and there is no top-level `version`. A
17
+ * customer-protected token is passed as `secrets.unwrap()` returns it.
18
+ *
19
+ * A `vault` fill needs the vault attached to the session; it fills the
20
+ * entries as they are now. Customer-protected secrets can't be filled.
17
21
  *
18
22
  * API errors are `HyperbrowserError`s whose `details` is the error body; the
19
23
  * server's own details are in `details.details`, such as `details.details.pages`
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CredentialsService = void 0;
4
4
  const error_1 = require("../error");
5
+ const secret_references_1 = require("../sandbox/secret-references");
5
6
  const secrets_envelope_1 = require("../secrets-envelope");
6
7
  const base_1 = require("./base");
7
8
  /** The API gives up on a fill after about 18 seconds. */
@@ -19,7 +20,11 @@ class CredentialsService extends base_1.BaseService {
19
20
  * `op_token` secret must have been attached at session creation as
20
21
  * `{ onePassword: name }`, and the request's `allowedOrigins` are the
21
22
  * origins the values may be typed on. Each field's result carries the
22
- * `reference` read, and there is no top-level `version`.
23
+ * `reference` read, and there is no top-level `version`. A
24
+ * customer-protected token is passed as `secrets.unwrap()` returns it.
25
+ *
26
+ * A `vault` fill needs the vault attached to the session; it fills the
27
+ * entries as they are now. Customer-protected secrets can't be filled.
23
28
  *
24
29
  * API errors are `HyperbrowserError`s whose `details` is the error body; the
25
30
  * server's own details are in `details.details`, such as `details.details.pages`
@@ -38,9 +43,14 @@ class CredentialsService extends base_1.BaseService {
38
43
  }
39
44
  // Preserve request members so the API can reject unknown or conflicting options.
40
45
  const { frameUrl, ...body } = params;
46
+ const onePassword = (0, secret_references_1.serializeOnePassword)(body.onePassword);
41
47
  return this.request(`/session/${encodeURIComponent(sessionId)}/credentials/fill`, {
42
48
  method: "POST",
43
- body: JSON.stringify({ ...body, frame: frameUrl == null ? undefined : { url: frameUrl } }),
49
+ body: JSON.stringify({
50
+ ...body,
51
+ onePassword,
52
+ frame: frameUrl == null ? undefined : { url: frameUrl },
53
+ }),
44
54
  timeout: Math.max(this.timeout, MIN_FILL_TIMEOUT_MS),
45
55
  });
46
56
  }
@@ -71,8 +71,8 @@ const serializeCreateSandboxParams = (params) => {
71
71
  throw new error_1.HyperbrowserError("Unsupported sandbox runtimeClass");
72
72
  if (typeof params.imageName === "string") {
73
73
  const secrets = (0, secret_references_1.serializeSecretReferences)(params.secrets);
74
- const vault = params.vault;
75
- const onePassword = params.onePassword;
74
+ const vault = (0, secret_references_1.serializeVaultSource)(params.vault);
75
+ const onePassword = (0, secret_references_1.serializeOnePassword)(params.onePassword);
76
76
  // The startup command with secrets runs only in Firecracker sandboxes.
77
77
  if ((secrets !== undefined || vault !== undefined || onePassword !== undefined) &&
78
78
  params.runtimeClass === "gvisor-cpu")
@@ -1,9 +1,15 @@
1
- import { CreateSecretParams, Secret, SecretDeleteResult, SecretDetail, SecretListParams, SecretListResponse, SecretsPublicKey, SecretValue, SecretVersion, SecretVersionDeleteResult, UpdateSecretParams } from "../types/secret";
1
+ import { CustomerKey } from "../customer-key";
2
+ import { AddSecretVersionOptions, CreateSecretParams, ReplaceSecretVersionWrappingParams, RewrapSecretParams, Secret, SecretDeleteResult, SecretDetail, SecretListParams, SecretListResponse, SecretsPublicKey, SecretValue, SecretVersion, SecretVersionDeleteResult, UnwrappedSecretReference, UnwrapSecretOptions, UpdateSecretParams, WrappedSecretVersion } from "../types/secret";
2
3
  import { BaseService } from "./base";
3
4
  /**
4
5
  * Secrets are encrypted locally to the platform's public key; the API only
5
6
  * ever receives envelopes, and values are never returned.
6
7
  *
8
+ * A customer-protected secret's versions are also wrapped locally with your
9
+ * own key (`createCustomerKey()`, or your own `CustomerKey`), so the platform
10
+ * stores nothing it can open alone. Each use passes the reference `unwrap()`
11
+ * returns.
12
+ *
7
13
  * Methods that take `secret` accept its ID or its exact name. A UUID-shaped
8
14
  * argument is tried as an ID first and then as a name, since names may look
9
15
  * like UUIDs.
@@ -31,7 +37,36 @@ export declare class SecretsService extends BaseService {
31
37
  * value is checked against the rules of the secret's type, such as a TOTP
32
38
  * seed's format, before it is sealed.
33
39
  */
34
- addVersion(secret: string, value: SecretValue): Promise<SecretVersion>;
40
+ addVersion(secret: string, value: SecretValue, options?: AddSecretVersionOptions): Promise<SecretVersion>;
41
+ /**
42
+ * A customer-protected version's wrapped envelope, for unwrapping with
43
+ * your own key. Most callers want `unwrap()`, which also unwraps it.
44
+ */
45
+ getWrappedVersion(secret: string, version?: number | "latest"): Promise<WrappedSecretVersion>;
46
+ /**
47
+ * Unwrap a customer-protected version locally with your own key, giving
48
+ * the reference each use passes: as a sandbox's or process's `secrets`
49
+ * entry, or as a 1Password `token`. It is pinned to the version unwrapped.
50
+ * The unwrapped envelope is checked against the digest stored with the
51
+ * version, and only the platform can open it.
52
+ *
53
+ * `customerKey` may be several keys, such as during a rotation; the one
54
+ * whose `kid` wrapped the version is used.
55
+ */
56
+ unwrap(secret: string, customerKey: CustomerKey | readonly CustomerKey[], options?: UnwrapSecretOptions): Promise<UnwrappedSecretReference>;
57
+ /**
58
+ * Rewrap a customer-protected secret's versions with a new key, for
59
+ * rotating your own key. Each version is unwrapped with `from` and wrapped
60
+ * with `to` locally; the value never changes and the platform never sees
61
+ * it. Returns the versions rewrapped.
62
+ */
63
+ rewrap(secret: string, params: RewrapSecretParams): Promise<WrappedSecretVersion[]>;
64
+ /**
65
+ * Replace a customer-protected version's outer envelope with one you
66
+ * wrapped yourself. `innerDigest` must match the version's: only the
67
+ * wrapping changes, never the value. `rewrap()` does this for you.
68
+ */
69
+ replaceVersionWrapping(secret: string, version: number, params: ReplaceSecretVersionWrappingParams): Promise<WrappedSecretVersion>;
35
70
  /**
36
71
  * Replace the origins a secret may be filled into in browser sessions, or
37
72
  * clear them with `null` (it then can't be filled until origins are set
@@ -45,8 +80,14 @@ export declare class SecretsService extends BaseService {
45
80
  delete(secret: string): Promise<SecretDeleteResult>;
46
81
  /** Delete one stored version. The latest version cannot be deleted. */
47
82
  deleteVersion(secret: string, version: number): Promise<SecretVersionDeleteResult>;
48
- /** Sealed to the context the API returned for the secret; never one built here. */
83
+ /**
84
+ * Sealed to the context the API returned for the secret; never one built
85
+ * here. With a customer key, the sealed envelope is wrapped to the same
86
+ * context and the API keeps only the wrapped one and the inner digest.
87
+ */
49
88
  private uploadVersion;
89
+ private fetchWrapped;
90
+ private putWrapping;
50
91
  private resolve;
51
92
  private resolveDetail;
52
93
  /** Found by ID, the result already includes the versions. */
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SecretsService = void 0;
4
+ const customer_key_1 = require("../customer-key");
4
5
  const error_1 = require("../error");
5
6
  const secret_references_1 = require("../sandbox/secret-references");
6
7
  const secret_values_1 = require("../secret-values");
@@ -12,6 +13,17 @@ const MAX_LIST_LIMIT = 100;
12
13
  /** Teams hold at most 100 secrets, so one page of search results covers every match. */
13
14
  const NAME_LOOKUP_LIMIT = MAX_LIST_LIMIT;
14
15
  const secretPath = (id) => `/secrets/${encodeURIComponent(id)}`;
16
+ const isCustomerProtected = (secret) => secret.protection === "customer";
17
+ /** Throws unless `version` is a version number, or `"latest"` where allowed. */
18
+ const assertVersion = (version, allowLatest) => {
19
+ if (allowLatest && version === "latest")
20
+ return;
21
+ if (!Number.isSafeInteger(version) ||
22
+ version < 1 ||
23
+ version > secret_references_1.MAX_SECRET_VERSION) {
24
+ throw new error_1.HyperbrowserError(`version must be an integer from 1 to ${secret_references_1.MAX_SECRET_VERSION}${allowLatest ? ' or "latest"' : ""}`);
25
+ }
26
+ };
15
27
  const rethrow = (error, message) => {
16
28
  if (error instanceof error_1.HyperbrowserError) {
17
29
  throw error;
@@ -22,6 +34,11 @@ const rethrow = (error, message) => {
22
34
  * Secrets are encrypted locally to the platform's public key; the API only
23
35
  * ever receives envelopes, and values are never returned.
24
36
  *
37
+ * A customer-protected secret's versions are also wrapped locally with your
38
+ * own key (`createCustomerKey()`, or your own `CustomerKey`), so the platform
39
+ * stores nothing it can open alone. Each use passes the reference `unwrap()`
40
+ * returns.
41
+ *
25
42
  * Methods that take `secret` accept its ID or its exact name. A UUID-shaped
26
43
  * argument is tried as an ID first and then as a name, since names may look
27
44
  * like UUIDs.
@@ -67,13 +84,26 @@ class SecretsService extends base_1.BaseService {
67
84
  * empty secret behind.
68
85
  */
69
86
  async create(params) {
70
- const { name, description, type, allowedOrigins, value } = params ?? {};
87
+ const { name, description, type, allowedOrigins, value, customerKey } = params ?? {};
71
88
  if (typeof name !== "string" || !secret_references_1.SECRET_NAME_PATTERN.test(name)) {
72
89
  throw new error_1.HyperbrowserError("Secret name must be 1-128 letters, digits, '_' or '-', starting with a letter or digit");
73
90
  }
74
91
  if (type !== undefined && (typeof type !== "string" || !type)) {
75
92
  throw (0, error_1.invalidRequest)('type must be "secret", "totp" or "op_token"');
76
93
  }
94
+ const protection = params.protection ?? (customerKey !== undefined ? "customer" : undefined);
95
+ if (protection !== undefined && protection !== "platform" && protection !== "customer") {
96
+ throw (0, error_1.invalidRequest)('protection must be "platform" or "customer"');
97
+ }
98
+ if (customerKey !== undefined) {
99
+ if (protection !== "customer") {
100
+ throw (0, error_1.invalidRequest)('customerKey is only for secrets with protection "customer"');
101
+ }
102
+ (0, customer_key_1.assertCustomerKey)(customerKey);
103
+ }
104
+ if (protection === "customer" && value !== undefined && customerKey === undefined) {
105
+ throw (0, error_1.invalidRequest)("A customer-protected secret's value is wrapped with your own key: pass customerKey");
106
+ }
77
107
  const plaintext = value === undefined ? undefined : (0, secret_values_1.secretPlaintext)(type ?? "secret", value);
78
108
  const sealer = plaintext === undefined ? undefined : secrets_envelope_1.SecretsSealer.fromPublicKey(await this.getPublicKey());
79
109
  const metadata = allowedOrigins === undefined ? undefined : { browser: { allowedOrigins } };
@@ -81,7 +111,7 @@ class SecretsService extends base_1.BaseService {
81
111
  try {
82
112
  secret = await this.request("/secrets", {
83
113
  method: "POST",
84
- body: JSON.stringify({ name, description, type, metadata }),
114
+ body: JSON.stringify({ name, description, type, metadata, protection }),
85
115
  });
86
116
  }
87
117
  catch (error) {
@@ -91,7 +121,7 @@ class SecretsService extends base_1.BaseService {
91
121
  return secret;
92
122
  }
93
123
  try {
94
- const version = await this.uploadVersion(secret, sealer, plaintext);
124
+ const version = await this.uploadVersion(secret, sealer, plaintext, customerKey);
95
125
  return { ...secret, latestVersion: version.version };
96
126
  }
97
127
  catch (error) {
@@ -143,18 +173,122 @@ class SecretsService extends base_1.BaseService {
143
173
  * value is checked against the rules of the secret's type, such as a TOTP
144
174
  * seed's format, before it is sealed.
145
175
  */
146
- async addVersion(secret, value) {
176
+ async addVersion(secret, value, options = {}) {
147
177
  (0, secrets_envelope_1.secretValueBytes)(value);
178
+ const { customerKey } = options ?? {};
179
+ if (customerKey !== undefined)
180
+ (0, customer_key_1.assertCustomerKey)(customerKey);
148
181
  const sealer = secrets_envelope_1.SecretsSealer.fromPublicKey(await this.getPublicKey());
149
182
  try {
150
183
  const resolved = await this.resolve(secret);
184
+ const customer = isCustomerProtected(resolved);
185
+ if (customer && customerKey === undefined) {
186
+ throw (0, error_1.invalidRequest)(`Secret ${resolved.name} is customer-protected: pass customerKey to wrap the value with`);
187
+ }
188
+ if (!customer && customerKey !== undefined) {
189
+ throw (0, error_1.invalidRequest)(`Secret ${resolved.name} is not customer-protected, so its versions aren't wrapped: omit customerKey`);
190
+ }
151
191
  const plaintext = (0, secret_values_1.secretPlaintext)(resolved.type ?? "secret", value);
152
- return await this.uploadVersion(resolved, sealer, plaintext);
192
+ return await this.uploadVersion(resolved, sealer, plaintext, customerKey);
153
193
  }
154
194
  catch (error) {
155
195
  return rethrow(error, `Failed to add a version to secret ${secret}`);
156
196
  }
157
197
  }
198
+ /**
199
+ * A customer-protected version's wrapped envelope, for unwrapping with
200
+ * your own key. Most callers want `unwrap()`, which also unwraps it.
201
+ */
202
+ async getWrappedVersion(secret, version = "latest") {
203
+ assertVersion(version, true);
204
+ try {
205
+ const { id } = await this.resolve(secret);
206
+ return await this.fetchWrapped(id, version);
207
+ }
208
+ catch (error) {
209
+ return rethrow(error, `Failed to get the wrapped version of secret ${secret}`);
210
+ }
211
+ }
212
+ /**
213
+ * Unwrap a customer-protected version locally with your own key, giving
214
+ * the reference each use passes: as a sandbox's or process's `secrets`
215
+ * entry, or as a 1Password `token`. It is pinned to the version unwrapped.
216
+ * The unwrapped envelope is checked against the digest stored with the
217
+ * version, and only the platform can open it.
218
+ *
219
+ * `customerKey` may be several keys, such as during a rotation; the one
220
+ * whose `kid` wrapped the version is used.
221
+ */
222
+ async unwrap(secret, customerKey, options = {}) {
223
+ const version = options?.version ?? "latest";
224
+ assertVersion(version, true);
225
+ try {
226
+ const resolved = await this.resolve(secret);
227
+ const wrapped = await this.fetchWrapped(resolved.id, version);
228
+ const unwrapped = await (0, customer_key_1.unwrapVersion)(wrapped, customerKey, `Version ${wrapped.version} of secret ${resolved.name}`);
229
+ return { name: resolved.name, version: wrapped.version, unwrapped };
230
+ }
231
+ catch (error) {
232
+ return rethrow(error, `Failed to unwrap secret ${secret}`);
233
+ }
234
+ }
235
+ /**
236
+ * Rewrap a customer-protected secret's versions with a new key, for
237
+ * rotating your own key. Each version is unwrapped with `from` and wrapped
238
+ * with `to` locally; the value never changes and the platform never sees
239
+ * it. Returns the versions rewrapped.
240
+ */
241
+ async rewrap(secret, params) {
242
+ const { from, to, versions } = params ?? {};
243
+ (0, customer_key_1.assertCustomerKey)(to, "to");
244
+ if (versions !== undefined) {
245
+ if (!Array.isArray(versions))
246
+ throw (0, error_1.invalidRequest)("versions must be an array of version numbers");
247
+ versions.forEach((version) => assertVersion(version, false));
248
+ }
249
+ try {
250
+ const detail = await this.resolveDetail(secret);
251
+ if (!isCustomerProtected(detail)) {
252
+ throw (0, error_1.invalidRequest)(`Secret ${detail.name} is not customer-protected; it has no wrapping`);
253
+ }
254
+ const wanted = versions ??
255
+ detail.versions.filter(({ outerKid }) => outerKid !== to.kid).map(({ version }) => version);
256
+ const rewrapped = [];
257
+ for (const version of wanted) {
258
+ const wrapped = await this.fetchWrapped(detail.id, version);
259
+ const label = `Version ${version} of secret ${detail.name}`;
260
+ const inner = await (0, customer_key_1.unwrapVersion)(wrapped, from, label);
261
+ const envelope = await to.wrap(inner, detail.encryptionContext);
262
+ rewrapped.push(await this.putWrapping(detail.id, version, { envelope, innerDigest: wrapped.innerDigest }));
263
+ }
264
+ return rewrapped;
265
+ }
266
+ catch (error) {
267
+ return rethrow(error, `Failed to rewrap secret ${secret}`);
268
+ }
269
+ }
270
+ /**
271
+ * Replace a customer-protected version's outer envelope with one you
272
+ * wrapped yourself. `innerDigest` must match the version's: only the
273
+ * wrapping changes, never the value. `rewrap()` does this for you.
274
+ */
275
+ async replaceVersionWrapping(secret, version, params) {
276
+ assertVersion(version, false);
277
+ const { envelope, innerDigest } = params ?? {};
278
+ if (typeof envelope !== "string" || !envelope) {
279
+ throw (0, error_1.invalidRequest)("envelope must be the wrapped envelope");
280
+ }
281
+ if (typeof innerDigest !== "string" || !innerDigest) {
282
+ throw (0, error_1.invalidRequest)("innerDigest must be the version's innerDigest");
283
+ }
284
+ try {
285
+ const { id } = await this.resolve(secret);
286
+ return await this.putWrapping(id, version, { envelope, innerDigest });
287
+ }
288
+ catch (error) {
289
+ return rethrow(error, `Failed to replace the wrapping of version ${version} of secret ${secret}`);
290
+ }
291
+ }
158
292
  /**
159
293
  * Replace the origins a secret may be filled into in browser sessions, or
160
294
  * clear them with `null` (it then can't be filled until origins are set
@@ -180,9 +314,7 @@ class SecretsService extends base_1.BaseService {
180
314
  }
181
315
  /** Delete one stored version. The latest version cannot be deleted. */
182
316
  async deleteVersion(secret, version) {
183
- if (!Number.isSafeInteger(version) || version < 1 || version > secret_references_1.MAX_SECRET_VERSION) {
184
- throw new error_1.HyperbrowserError(`version must be an integer from 1 to ${secret_references_1.MAX_SECRET_VERSION}`);
185
- }
317
+ assertVersion(version, false);
186
318
  try {
187
319
  const { id } = await this.resolve(secret);
188
320
  return await this.request(`${secretPath(id)}/versions/${version}`, {
@@ -193,11 +325,32 @@ class SecretsService extends base_1.BaseService {
193
325
  return rethrow(error, `Failed to delete version ${version} of secret ${secret}`);
194
326
  }
195
327
  }
196
- /** Sealed to the context the API returned for the secret; never one built here. */
197
- uploadVersion(secret, sealer, plaintext) {
328
+ /**
329
+ * Sealed to the context the API returned for the secret; never one built
330
+ * here. With a customer key, the sealed envelope is wrapped to the same
331
+ * context and the API keeps only the wrapped one and the inner digest.
332
+ */
333
+ async uploadVersion(secret, sealer, plaintext, customerKey) {
334
+ const inner = sealer.seal(secret.encryptionContext, plaintext);
335
+ const body = customerKey === undefined
336
+ ? { envelope: inner }
337
+ : {
338
+ envelope: await customerKey.wrap(inner, secret.encryptionContext),
339
+ innerDigest: (0, customer_key_1.secretEnvelopeDigest)(inner),
340
+ innerKid: sealer.kid,
341
+ };
198
342
  return this.request(`${secretPath(secret.id)}/versions`, {
199
343
  method: "POST",
200
- body: JSON.stringify({ envelope: sealer.seal(secret.encryptionContext, plaintext) }),
344
+ body: JSON.stringify(body),
345
+ });
346
+ }
347
+ fetchWrapped(id, version) {
348
+ return this.request(`${secretPath(id)}/versions/${version}/wrapped`);
349
+ }
350
+ putWrapping(id, version, body) {
351
+ return this.request(`${secretPath(id)}/versions/${version}/wrapping`, {
352
+ method: "PUT",
353
+ body: JSON.stringify(body),
201
354
  });
202
355
  }
203
356
  async resolve(secret) {
@@ -1,10 +1,17 @@
1
- import { CreateVaultParams, UpdateVaultParams, Vault, VaultDeleteResult, VaultListParams, VaultListResponse } from "../types/vault";
1
+ import { CustomerKey } from "../customer-key";
2
+ import { CreateVaultParams, UpdateVaultParams, Vault, VaultDeleteResult, VaultListParams, VaultListResponse, VaultReference, WrappedVault } from "../types/vault";
2
3
  import { BaseService } from "./base";
3
4
  /**
4
5
  * Vaults name a set of environment variables whose values come from stored
5
6
  * secrets. Pass one as `vault` to a sandbox process
6
7
  * or sandbox create, or attach it to a browser session with
7
- * `credentials: [{ vault }]` and fill its entries by name.
8
+ * `credentials: [{ vault }]` and fill its entries by name. A session
9
+ * attaches the vault by reference, so each fill uses its entries as they are
10
+ * then.
11
+ *
12
+ * Entries may be customer-protected secrets. Each sandbox or process use then
13
+ * passes the vault as `unwrap()` returns it, with those entries' envelopes
14
+ * unwrapped with your own key.
8
15
  *
9
16
  * Methods that take `vault` accept its ID or its exact name. A UUID-shaped
10
17
  * argument is tried as an ID first and then as a name.
@@ -22,5 +29,21 @@ export declare class VaultsService extends BaseService {
22
29
  update(vault: string, params: UpdateVaultParams): Promise<Vault>;
23
30
  /** Delete a vault. The secrets its entries name are kept. */
24
31
  delete(vault: string): Promise<VaultDeleteResult>;
32
+ /**
33
+ * The wrapped envelopes of a vault's customer-protected entries, each at
34
+ * the version its entry resolves to now. Most callers want `unwrap()`.
35
+ */
36
+ getWrapped(vault: string): Promise<WrappedVault>;
37
+ /**
38
+ * Unwrap a vault's customer-protected entries locally with your own key,
39
+ * giving the reference to pass as a sandbox's or process's `vault`. Each
40
+ * envelope is checked against the digest stored with its version. A vault
41
+ * with no customer-protected entries needs none and can be passed by name.
42
+ *
43
+ * `customerKey` may be several keys; each entry uses the one whose `kid`
44
+ * wrapped it. If an entry changes after this, uses fail with
45
+ * `SECRET_VAULT_CHANGED`: unwrap the vault again.
46
+ */
47
+ unwrap(vault: string, customerKey: CustomerKey | readonly CustomerKey[]): Promise<VaultReference>;
25
48
  private resolve;
26
49
  }