@hyperbrowser/sdk 1.4.1 → 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.
- package/README.md +88 -5
- package/dist/customer-key.d.ts +71 -0
- package/dist/customer-key.js +394 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -1
- package/dist/sandbox/process.d.ts +3 -3
- package/dist/sandbox/process.js +2 -2
- package/dist/sandbox/secret-references.d.ts +25 -3
- package/dist/sandbox/secret-references.js +86 -14
- package/dist/secrets-envelope.d.ts +2 -1
- package/dist/secrets-envelope.js +3 -1
- package/dist/services/credentials.d.ts +5 -1
- package/dist/services/credentials.js +12 -2
- package/dist/services/sandboxes.js +2 -2
- package/dist/services/secrets.d.ts +44 -3
- package/dist/services/secrets.js +164 -11
- package/dist/services/vaults.d.ts +25 -2
- package/dist/services/vaults.js +37 -1
- package/dist/types/constants.d.ts +1 -1
- package/dist/types/credential.d.ts +14 -5
- package/dist/types/index.d.ts +6 -4
- package/dist/types/sandbox.d.ts +31 -15
- package/dist/types/secret.d.ts +80 -0
- package/dist/types/session.d.ts +8 -6
- package/dist/types/vault.d.ts +38 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/secrets-envelope.js
CHANGED
|
@@ -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(
|
|
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({
|
|
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 {
|
|
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
|
-
/**
|
|
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. */
|
package/dist/services/secrets.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
197
|
-
|
|
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(
|
|
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 {
|
|
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
|
}
|