@hyperbrowser/sdk 1.2.2 → 1.4.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 +427 -3
- package/dist/client.d.ts +2 -0
- package/dist/client.js +2 -0
- package/dist/error.d.ts +2 -0
- package/dist/error.js +4 -1
- package/dist/sandbox/process.d.ts +7 -3
- package/dist/sandbox/process.js +15 -3
- package/dist/sandbox/secret-references.d.ts +4 -0
- package/dist/sandbox/secret-references.js +11 -2
- package/dist/secret-values.d.ts +14 -0
- package/dist/secret-values.js +153 -0
- package/dist/secrets-envelope.d.ts +5 -0
- package/dist/secrets-envelope.js +17 -7
- package/dist/services/credentials.d.ts +24 -0
- package/dist/services/credentials.js +48 -0
- package/dist/services/sandboxes.d.ts +3 -2
- package/dist/services/sandboxes.js +20 -10
- package/dist/services/secrets.d.ts +19 -4
- package/dist/services/secrets.js +53 -17
- package/dist/services/sessions.d.ts +2 -0
- package/dist/services/sessions.js +2 -0
- package/dist/services/vaults.d.ts +26 -0
- package/dist/services/vaults.js +80 -0
- package/dist/tools/anthropic.d.ts +1 -0
- package/dist/tools/anthropic.js +6 -1
- package/dist/tools/gemini.d.ts +56 -0
- package/dist/tools/gemini.js +40 -0
- package/dist/tools/index.d.ts +92 -0
- package/dist/tools/index.js +81 -1
- package/dist/tools/openai.d.ts +1 -0
- package/dist/tools/openai.js +10 -1
- package/dist/tools/schema.d.ts +49 -0
- package/dist/tools/schema.js +53 -4
- package/dist/types/constants.d.ts +26 -4
- package/dist/types/credential.d.ts +120 -0
- package/dist/types/credential.js +2 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/sandbox.d.ts +89 -2
- package/dist/types/secret.d.ts +61 -3
- package/dist/types/session.d.ts +14 -0
- package/dist/types/vault.d.ts +55 -0
- package/dist/types/vault.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.secretPlaintext = exports.checkTotpSeed = void 0;
|
|
4
|
+
const secrets_envelope_1 = require("./secrets-envelope");
|
|
5
|
+
const TOTP_ALGORITHMS = new Set(["SHA1", "SHA256", "SHA512"]);
|
|
6
|
+
const MAX_TOTP_PERIOD_SECONDS = 300;
|
|
7
|
+
/**
|
|
8
|
+
* Unpadded base32 can't end with 1, 3 or 6 characters of a block, so such a
|
|
9
|
+
* seed was copied incompletely. vm-service refuses it too (older hosts
|
|
10
|
+
* dropped the extra characters and gave wrong codes).
|
|
11
|
+
*/
|
|
12
|
+
const BASE32_TAIL_LENGTHS = new Set([0, 2, 4, 5, 7]);
|
|
13
|
+
const BYTE_ORDER_MARK = "\uFEFF";
|
|
14
|
+
const INVALID_TOTP = "A TOTP value must be a base32 seed or an otpauth://totp/ URI with a base32 secret, " +
|
|
15
|
+
"and optionally algorithm SHA1, SHA256 or SHA512, digits 6 or 8, and a period of 1 to 300 seconds";
|
|
16
|
+
// vm-service parses seeds with Go: these mirror strings.TrimSpace,
|
|
17
|
+
// url.Parse and url.ParseQuery, so a value is refused here whenever it would
|
|
18
|
+
// be refused there.
|
|
19
|
+
/** Go's unicode.IsSpace: ASCII spaces, U+0085, U+00A0 and the Unicode White_Space spaces. */
|
|
20
|
+
const GO_SPACE = "\t\n\v\f\r \u0085
";
|
|
21
|
+
const trimGoSpace = (value) => {
|
|
22
|
+
let start = 0;
|
|
23
|
+
let end = value.length;
|
|
24
|
+
while (start < end && GO_SPACE.includes(value[start]))
|
|
25
|
+
start++;
|
|
26
|
+
while (end > start && GO_SPACE.includes(value[end - 1]))
|
|
27
|
+
end--;
|
|
28
|
+
return value.slice(start, end);
|
|
29
|
+
};
|
|
30
|
+
const isHex = (char) => char !== undefined && /^[0-9A-Fa-f]$/.test(char);
|
|
31
|
+
/** Every '%' starts a two-digit hex escape, as url.Parse requires outside the query. */
|
|
32
|
+
const hasValidEscapes = (text) => {
|
|
33
|
+
for (let i = 0; i < text.length; i++) {
|
|
34
|
+
if (text[i] === "%" && !(isHex(text[i + 1]) && isHex(text[i + 2])))
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
39
|
+
/** url.QueryUnescape; undefined for a bad escape. Bytes are read as Latin-1. */
|
|
40
|
+
const queryUnescape = (text) => {
|
|
41
|
+
if (!hasValidEscapes(text))
|
|
42
|
+
return undefined;
|
|
43
|
+
return text
|
|
44
|
+
.replace(/\+/g, " ")
|
|
45
|
+
.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
46
|
+
};
|
|
47
|
+
/** url.Values.Get after url.ParseQuery: pairs with bad escapes or ';' are skipped. */
|
|
48
|
+
const queryGet = (query, key) => {
|
|
49
|
+
for (const pair of query.split("&")) {
|
|
50
|
+
if (!pair || pair.includes(";"))
|
|
51
|
+
continue;
|
|
52
|
+
const separator = pair.indexOf("=");
|
|
53
|
+
const name = queryUnescape(separator < 0 ? pair : pair.slice(0, separator));
|
|
54
|
+
const value = queryUnescape(separator < 0 ? "" : pair.slice(separator + 1));
|
|
55
|
+
if (name === key && value !== undefined)
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
return "";
|
|
59
|
+
};
|
|
60
|
+
/** Characters url.Parse accepts in the userinfo before '@'. */
|
|
61
|
+
const USERINFO_PATTERN = /^[A-Za-z0-9\-._:~!$&'()*+,;=%@]*$/;
|
|
62
|
+
/**
|
|
63
|
+
* The otpauth:// URI's query, after the checks url.Parse and vm-service make.
|
|
64
|
+
* `uri` starts with "otpauth://" in any case.
|
|
65
|
+
*/
|
|
66
|
+
const otpauthQuery = (uri) => {
|
|
67
|
+
if ([...uri].some((char) => char.charCodeAt(0) < 0x20 || char.charCodeAt(0) === 0x7f)) {
|
|
68
|
+
throw (0, secrets_envelope_1.invalidSecretValue)("A TOTP otpauth:// URI must not contain control characters");
|
|
69
|
+
}
|
|
70
|
+
const hash = uri.indexOf("#");
|
|
71
|
+
const fragment = hash < 0 ? "" : uri.slice(hash + 1);
|
|
72
|
+
const beforeFragment = hash < 0 ? uri : uri.slice(0, hash);
|
|
73
|
+
const question = beforeFragment.indexOf("?");
|
|
74
|
+
const query = question < 0 ? "" : beforeFragment.slice(question + 1);
|
|
75
|
+
const rest = (question < 0 ? beforeFragment : beforeFragment.slice(0, question)).slice("otpauth://".length);
|
|
76
|
+
const slash = rest.indexOf("/");
|
|
77
|
+
const authority = slash < 0 ? rest : rest.slice(0, slash);
|
|
78
|
+
const path = slash < 0 ? "" : rest.slice(slash);
|
|
79
|
+
const at = authority.lastIndexOf("@");
|
|
80
|
+
const userinfo = at < 0 ? "" : authority.slice(0, at);
|
|
81
|
+
const host = (at < 0 ? authority : authority.slice(at + 1)).toLowerCase();
|
|
82
|
+
if (!USERINFO_PATTERN.test(userinfo) || ![userinfo, path, fragment].every(hasValidEscapes)) {
|
|
83
|
+
throw (0, secrets_envelope_1.invalidSecretValue)(INVALID_TOTP);
|
|
84
|
+
}
|
|
85
|
+
if (host === "hotp") {
|
|
86
|
+
throw (0, secrets_envelope_1.invalidSecretValue)("HOTP (otpauth://hotp/) is not supported; a TOTP value must be a base32 seed or an otpauth://totp/ URI");
|
|
87
|
+
}
|
|
88
|
+
if (host !== "totp") {
|
|
89
|
+
throw (0, secrets_envelope_1.invalidSecretValue)(INVALID_TOTP);
|
|
90
|
+
}
|
|
91
|
+
return query;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The bytes a base32 seed decodes to, read the way the platform reads it:
|
|
95
|
+
* spaces, '-', line breaks, lowercase and trailing '=' padding are accepted. Undefined
|
|
96
|
+
* when it isn't ASCII base32 or has an impossible length.
|
|
97
|
+
*/
|
|
98
|
+
const base32SeedBytes = (seed) => {
|
|
99
|
+
// Upper-cased one character at a time, as Go does, so "ß" can't turn into
|
|
100
|
+
// "SS" while "ı" still reads as "I".
|
|
101
|
+
const upper = Array.from(seed.replace(/[ \r\n-]/g, ""), (char) => {
|
|
102
|
+
const mapped = char.toUpperCase();
|
|
103
|
+
return mapped.length === 1 ? mapped : char;
|
|
104
|
+
}).join("");
|
|
105
|
+
if (!/^[A-Z2-7]+=*$/.test(upper))
|
|
106
|
+
return undefined;
|
|
107
|
+
const normalized = upper.replace(/=+$/, "");
|
|
108
|
+
return BASE32_TAIL_LENGTHS.has(normalized.length % 8)
|
|
109
|
+
? Math.floor((normalized.length * 5) / 8)
|
|
110
|
+
: undefined;
|
|
111
|
+
};
|
|
112
|
+
const isIntegerIn = (text, allowed) => /^[+-]?\d+$/.test(text) && allowed(Number(text));
|
|
113
|
+
/**
|
|
114
|
+
* Check a TOTP seed as the platform will read it at fill or process time; the
|
|
115
|
+
* API can't see values, so a bad seed would otherwise only fail then. Seeds
|
|
116
|
+
* under 10 bytes (16 base32 characters) are accepted, though they are
|
|
117
|
+
* unusually short.
|
|
118
|
+
*/
|
|
119
|
+
const checkTotpSeed = (value) => {
|
|
120
|
+
let seed = trimGoSpace(value.startsWith(BYTE_ORDER_MARK) ? value.slice(1) : value);
|
|
121
|
+
if (seed.toLowerCase().startsWith("otpauth://")) {
|
|
122
|
+
const query = otpauthQuery(seed);
|
|
123
|
+
const algorithm = queryGet(query, "algorithm");
|
|
124
|
+
const digits = queryGet(query, "digits");
|
|
125
|
+
const period = queryGet(query, "period");
|
|
126
|
+
if ((algorithm && !TOTP_ALGORITHMS.has(algorithm.toUpperCase())) ||
|
|
127
|
+
(digits && !isIntegerIn(digits, (n) => n === 6 || n === 8)) ||
|
|
128
|
+
(period && !isIntegerIn(period, (n) => n >= 1 && n <= MAX_TOTP_PERIOD_SECONDS))) {
|
|
129
|
+
throw (0, secrets_envelope_1.invalidSecretValue)(INVALID_TOTP);
|
|
130
|
+
}
|
|
131
|
+
seed = queryGet(query, "secret");
|
|
132
|
+
}
|
|
133
|
+
else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(seed)) {
|
|
134
|
+
throw (0, secrets_envelope_1.invalidSecretValue)(INVALID_TOTP);
|
|
135
|
+
}
|
|
136
|
+
if (base32SeedBytes(seed) === undefined) {
|
|
137
|
+
throw (0, secrets_envelope_1.invalidSecretValue)(INVALID_TOTP);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
exports.checkTotpSeed = checkTotpSeed;
|
|
141
|
+
/**
|
|
142
|
+
* The plaintext stored for a secret of `type`, after the platform's rules
|
|
143
|
+
* for every value (1 byte to 32 KiB of UTF-8, no NUL) and the type's own.
|
|
144
|
+
* Types this SDK doesn't know get the common rules only.
|
|
145
|
+
*/
|
|
146
|
+
const secretPlaintext = (type, value) => {
|
|
147
|
+
const bytes = (0, secrets_envelope_1.secretValueBytes)(value);
|
|
148
|
+
if (type === "totp") {
|
|
149
|
+
(0, exports.checkTotpSeed)(bytes.toString("utf8"));
|
|
150
|
+
}
|
|
151
|
+
return bytes;
|
|
152
|
+
};
|
|
153
|
+
exports.secretPlaintext = secretPlaintext;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { HyperbrowserError } from "./error";
|
|
1
2
|
import { SecretsPublicKey, SecretValue } from "./types/secret";
|
|
2
3
|
/** The largest secret value the platform accepts. */
|
|
3
4
|
export declare const MAX_SECRET_VALUE_BYTES: number;
|
|
5
|
+
/** @internal A value the platform would refuse. Messages never include the value. */
|
|
6
|
+
export declare const invalidSecretValue: (message: string) => HyperbrowserError;
|
|
4
7
|
/**
|
|
5
8
|
* Apply the platform's value rules locally, so a bad value is reported before
|
|
6
9
|
* anything is created rather than when a sandbox starts.
|
|
7
10
|
*/
|
|
8
11
|
export declare const secretValueBytes: (value: SecretValue) => Buffer;
|
|
12
|
+
/** A plain object, as opposed to an array, bytes or a class instance. */
|
|
13
|
+
export declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
|
|
9
14
|
/** Encrypts values to a verified secrets public key. */
|
|
10
15
|
export declare class SecretsSealer {
|
|
11
16
|
private readonly kid;
|
package/dist/secrets-envelope.js
CHANGED
|
@@ -3,14 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SecretsSealer = exports.secretValueBytes = exports.MAX_SECRET_VALUE_BYTES = void 0;
|
|
6
|
+
exports.SecretsSealer = exports.isPlainObject = exports.secretValueBytes = exports.invalidSecretValue = exports.MAX_SECRET_VALUE_BYTES = void 0;
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
8
|
const error_1 = require("./error");
|
|
9
9
|
/** The largest secret value the platform accepts. */
|
|
10
10
|
exports.MAX_SECRET_VALUE_BYTES = 32 * 1024;
|
|
11
11
|
const MIN_SECRETS_KEY_BITS = 3072;
|
|
12
12
|
const SECRETS_KEY_EXPONENT = 65537;
|
|
13
|
-
|
|
13
|
+
/** @internal A value the platform would refuse. Messages never include the value. */
|
|
14
|
+
const invalidSecretValue = (message) => new error_1.HyperbrowserError(message, { code: "invalid_secret_value", retryable: false });
|
|
15
|
+
exports.invalidSecretValue = invalidSecretValue;
|
|
14
16
|
const base64Url = (data) => Buffer.from(data).toString("base64url");
|
|
15
17
|
/**
|
|
16
18
|
* Apply the platform's value rules locally, so a bad value is reported before
|
|
@@ -18,27 +20,35 @@ const base64Url = (data) => Buffer.from(data).toString("base64url");
|
|
|
18
20
|
*/
|
|
19
21
|
const secretValueBytes = (value) => {
|
|
20
22
|
if (typeof value !== "string" && !(value instanceof Uint8Array)) {
|
|
21
|
-
throw
|
|
23
|
+
throw (0, exports.invalidSecretValue)("Secret value must be a string or bytes");
|
|
22
24
|
}
|
|
23
25
|
const bytes = Buffer.from(value);
|
|
24
26
|
if (bytes.length === 0) {
|
|
25
|
-
throw
|
|
27
|
+
throw (0, exports.invalidSecretValue)("Secret value is empty");
|
|
26
28
|
}
|
|
27
29
|
if (bytes.length > exports.MAX_SECRET_VALUE_BYTES) {
|
|
28
|
-
throw
|
|
30
|
+
throw (0, exports.invalidSecretValue)(`Secret value is more than ${exports.MAX_SECRET_VALUE_BYTES} bytes`);
|
|
29
31
|
}
|
|
30
32
|
if (bytes.includes(0)) {
|
|
31
|
-
throw
|
|
33
|
+
throw (0, exports.invalidSecretValue)("Secret value must not contain NUL bytes");
|
|
32
34
|
}
|
|
33
35
|
try {
|
|
34
36
|
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
35
37
|
}
|
|
36
38
|
catch {
|
|
37
|
-
throw
|
|
39
|
+
throw (0, exports.invalidSecretValue)("Secret value must be valid UTF-8");
|
|
38
40
|
}
|
|
39
41
|
return bytes;
|
|
40
42
|
};
|
|
41
43
|
exports.secretValueBytes = secretValueBytes;
|
|
44
|
+
/** A plain object, as opposed to an array, bytes or a class instance. */
|
|
45
|
+
const isPlainObject = (value) => {
|
|
46
|
+
if (value === null || typeof value !== "object")
|
|
47
|
+
return false;
|
|
48
|
+
const prototype = Object.getPrototypeOf(value);
|
|
49
|
+
return prototype === Object.prototype || prototype === null;
|
|
50
|
+
};
|
|
51
|
+
exports.isPlainObject = isPlainObject;
|
|
42
52
|
const minimalBytes = (value) => {
|
|
43
53
|
let start = 0;
|
|
44
54
|
while (start < value.length - 1 && value[start] === 0)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CredentialFillResult, FillCredentialParams } from "../types/credential";
|
|
2
|
+
import { BaseService } from "./base";
|
|
3
|
+
/** Fill secrets attached to a browser session into its pages. */
|
|
4
|
+
export declare class CredentialsService extends BaseService {
|
|
5
|
+
/**
|
|
6
|
+
* Type a secret, a vault's entries, or 1Password fields into inputs on one
|
|
7
|
+
* of the session's pages. A TOTP secret types its current code. Only types;
|
|
8
|
+
* never submits, so click the login button or press Enter afterwards.
|
|
9
|
+
* Values never leave the platform: the result lists an outcome per input.
|
|
10
|
+
* Safe to retry; it is not retried automatically.
|
|
11
|
+
*
|
|
12
|
+
* With `onePassword`, each field's `ref` names the 1Password field, the
|
|
13
|
+
* `op_token` secret must have been attached at session creation as
|
|
14
|
+
* `{ onePassword: name }`, and the request's `allowedOrigins` are the
|
|
15
|
+
* origins the values may be typed on. Each field's result carries the
|
|
16
|
+
* `reference` read, and there is no top-level `version`.
|
|
17
|
+
*
|
|
18
|
+
* API errors are `HyperbrowserError`s whose `details` is the error body; the
|
|
19
|
+
* server's own details are in `details.details`, such as `details.details.pages`
|
|
20
|
+
* for `CREDENTIAL_PAGE_AMBIGUOUS`, or `selectors` and `reasons` (by
|
|
21
|
+
* selector) for `SECRET_ONEPASSWORD_REFERENCE_FAILED`.
|
|
22
|
+
*/
|
|
23
|
+
fill(sessionId: string, params: FillCredentialParams): Promise<CredentialFillResult>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CredentialsService = void 0;
|
|
4
|
+
const error_1 = require("../error");
|
|
5
|
+
const secrets_envelope_1 = require("../secrets-envelope");
|
|
6
|
+
const base_1 = require("./base");
|
|
7
|
+
/** The API gives up on a fill after about 18 seconds. */
|
|
8
|
+
const MIN_FILL_TIMEOUT_MS = 30000;
|
|
9
|
+
/** Fill secrets attached to a browser session into its pages. */
|
|
10
|
+
class CredentialsService extends base_1.BaseService {
|
|
11
|
+
/**
|
|
12
|
+
* Type a secret, a vault's entries, or 1Password fields into inputs on one
|
|
13
|
+
* of the session's pages. A TOTP secret types its current code. Only types;
|
|
14
|
+
* never submits, so click the login button or press Enter afterwards.
|
|
15
|
+
* Values never leave the platform: the result lists an outcome per input.
|
|
16
|
+
* Safe to retry; it is not retried automatically.
|
|
17
|
+
*
|
|
18
|
+
* With `onePassword`, each field's `ref` names the 1Password field, the
|
|
19
|
+
* `op_token` secret must have been attached at session creation as
|
|
20
|
+
* `{ onePassword: name }`, and the request's `allowedOrigins` are the
|
|
21
|
+
* origins the values may be typed on. Each field's result carries the
|
|
22
|
+
* `reference` read, and there is no top-level `version`.
|
|
23
|
+
*
|
|
24
|
+
* API errors are `HyperbrowserError`s whose `details` is the error body; the
|
|
25
|
+
* server's own details are in `details.details`, such as `details.details.pages`
|
|
26
|
+
* for `CREDENTIAL_PAGE_AMBIGUOUS`, or `selectors` and `reasons` (by
|
|
27
|
+
* selector) for `SECRET_ONEPASSWORD_REFERENCE_FAILED`.
|
|
28
|
+
*/
|
|
29
|
+
async fill(sessionId, params) {
|
|
30
|
+
if (typeof sessionId !== "string" || !sessionId.trim()) {
|
|
31
|
+
throw (0, error_1.invalidRequest)("A session ID is required");
|
|
32
|
+
}
|
|
33
|
+
if (!(0, secrets_envelope_1.isPlainObject)(params)) {
|
|
34
|
+
throw (0, error_1.invalidRequest)("Fill parameters must be an object");
|
|
35
|
+
}
|
|
36
|
+
if ("frame" in params) {
|
|
37
|
+
throw (0, error_1.invalidRequest)("Use frameUrl to select an iframe");
|
|
38
|
+
}
|
|
39
|
+
// Preserve request members so the API can reject unknown or conflicting options.
|
|
40
|
+
const { frameUrl, ...body } = params;
|
|
41
|
+
return this.request(`/session/${encodeURIComponent(sessionId)}/credentials/fill`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
body: JSON.stringify({ ...body, frame: frameUrl == null ? undefined : { url: frameUrl } }),
|
|
44
|
+
timeout: Math.max(this.timeout, MIN_FILL_TIMEOUT_MS),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.CredentialsService = CredentialsService;
|
|
@@ -118,8 +118,9 @@ export declare class SandboxesService extends BaseService {
|
|
|
118
118
|
updateNetwork(id: string, policy: SandboxNetworkPolicyPatch): Promise<SandboxNetworkUpdateResult>;
|
|
119
119
|
unexpose(id: string, port: number): Promise<SandboxUnexposeResult>;
|
|
120
120
|
/**
|
|
121
|
-
* With secrets, a create whose result is unknown is never
|
|
122
|
-
* sandbox may exist and be running its startup command with
|
|
121
|
+
* With secrets, a vault or onePassword, a create whose result is unknown is never
|
|
122
|
+
* retried: the sandbox may exist and be running its startup command with
|
|
123
|
+
* the secrets.
|
|
123
124
|
*/
|
|
124
125
|
private createDetail;
|
|
125
126
|
}
|
|
@@ -71,9 +71,12 @@ 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
76
|
// The startup command with secrets runs only in Firecracker sandboxes.
|
|
75
|
-
if (secrets !== undefined
|
|
76
|
-
|
|
77
|
+
if ((secrets !== undefined || vault !== undefined || onePassword !== undefined) &&
|
|
78
|
+
params.runtimeClass === "gvisor-cpu")
|
|
79
|
+
throw new error_1.HyperbrowserError("secrets, vault and onePassword are not supported with runtimeClass gvisor-cpu");
|
|
77
80
|
return {
|
|
78
81
|
runtimeClass: params.runtimeClass,
|
|
79
82
|
imageName: params.imageName,
|
|
@@ -90,12 +93,16 @@ const serializeCreateSandboxParams = (params) => {
|
|
|
90
93
|
allowOut: params.allowOut,
|
|
91
94
|
denyOut: params.denyOut,
|
|
92
95
|
secrets,
|
|
96
|
+
vault,
|
|
97
|
+
onePassword,
|
|
93
98
|
};
|
|
94
99
|
}
|
|
95
100
|
const snapshotParams = params;
|
|
96
|
-
if (snapshotParams.secrets !== undefined
|
|
101
|
+
if (snapshotParams.secrets !== undefined ||
|
|
102
|
+
snapshotParams.vault !== undefined ||
|
|
103
|
+
snapshotParams.onePassword !== undefined) {
|
|
97
104
|
// Snapshot launches do not run a startup command.
|
|
98
|
-
throw new error_1.HyperbrowserError("secrets are only supported for image launches");
|
|
105
|
+
throw new error_1.HyperbrowserError("secrets, vault and onePassword are only supported for image launches");
|
|
99
106
|
}
|
|
100
107
|
if (snapshotParams.cpu !== undefined ||
|
|
101
108
|
snapshotParams.memoryMiB !== undefined ||
|
|
@@ -320,7 +327,9 @@ class SandboxHandle {
|
|
|
320
327
|
return this.processes.get(processId);
|
|
321
328
|
}
|
|
322
329
|
hydrate(detail) {
|
|
323
|
-
|
|
330
|
+
// Only the create response reports what the startup command received.
|
|
331
|
+
const startupSecrets = detail.startupSecrets ?? this.detail?.startupSecrets;
|
|
332
|
+
this.detail = startupSecrets === undefined ? detail : { ...detail, startupSecrets };
|
|
324
333
|
this.runtimeSession = SandboxHandle.toRuntimeSession(detail);
|
|
325
334
|
}
|
|
326
335
|
async resolveRuntimeConnection(forceRefresh = false) {
|
|
@@ -1120,24 +1129,25 @@ class SandboxesService extends base_2.BaseService {
|
|
|
1120
1129
|
}
|
|
1121
1130
|
}
|
|
1122
1131
|
/**
|
|
1123
|
-
* With secrets, a create whose result is unknown is never
|
|
1124
|
-
* sandbox may exist and be running its startup command with
|
|
1132
|
+
* With secrets, a vault or onePassword, a create whose result is unknown is never
|
|
1133
|
+
* retried: the sandbox may exist and be running its startup command with
|
|
1134
|
+
* the secrets.
|
|
1125
1135
|
*/
|
|
1126
1136
|
async createDetail(params) {
|
|
1127
1137
|
const body = serializeCreateSandboxParams(params);
|
|
1138
|
+
const withSecrets = body.secrets !== undefined || body.vault !== undefined || body.onePassword !== undefined;
|
|
1128
1139
|
try {
|
|
1129
1140
|
const detail = await this.request("/sandbox", {
|
|
1130
1141
|
method: "POST",
|
|
1131
1142
|
body: JSON.stringify(body),
|
|
1132
1143
|
});
|
|
1133
|
-
if (
|
|
1134
|
-
(typeof detail?.id !== "string" || !detail.id.trim())) {
|
|
1144
|
+
if (withSecrets && (typeof detail?.id !== "string" || !detail.id.trim())) {
|
|
1135
1145
|
throw new Error("The API accepted the create but returned no sandbox ID");
|
|
1136
1146
|
}
|
|
1137
1147
|
return normalizeSandboxDetail(detail);
|
|
1138
1148
|
}
|
|
1139
1149
|
catch (error) {
|
|
1140
|
-
if (
|
|
1150
|
+
if (withSecrets && (0, process_1.requestMayHaveArrived)(error)) {
|
|
1141
1151
|
throw new error_1.HyperbrowserError(`could not confirm the create result: ${error instanceof Error
|
|
1142
1152
|
? error.message.replace(/^\[Hyperbrowser\]: /, "")
|
|
1143
1153
|
: String(error)}; a sandbox may have been created with the secrets, so list sandboxes before retrying`, {
|
|
@@ -14,15 +14,30 @@ export declare class SecretsService extends BaseService {
|
|
|
14
14
|
list(params?: SecretListParams): Promise<SecretListResponse>;
|
|
15
15
|
/**
|
|
16
16
|
* Create a secret, and store its first version when `value` is given. The
|
|
17
|
-
* value
|
|
18
|
-
* bad value never leaves an
|
|
17
|
+
* value is checked against its type's rules, and the platform key is
|
|
18
|
+
* checked, before the secret is created, so a bad value never leaves an
|
|
19
|
+
* empty secret behind.
|
|
19
20
|
*/
|
|
20
21
|
create(params: CreateSecretParams): Promise<Secret>;
|
|
21
22
|
/** Metadata and versions; never the value. */
|
|
22
23
|
get(secret: string): Promise<SecretDetail>;
|
|
24
|
+
/**
|
|
25
|
+
* Change a secret's description or metadata. Metadata members sent replace
|
|
26
|
+
* the stored ones, `null` removes a member, and members not sent are kept.
|
|
27
|
+
*/
|
|
23
28
|
update(secret: string, params: UpdateSecretParams): Promise<Secret>;
|
|
24
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Encrypt `value` locally and store it as the secret's next version. The
|
|
31
|
+
* value is checked against the rules of the secret's type, such as a TOTP
|
|
32
|
+
* seed's format, before it is sealed.
|
|
33
|
+
*/
|
|
25
34
|
addVersion(secret: string, value: SecretValue): Promise<SecretVersion>;
|
|
35
|
+
/**
|
|
36
|
+
* Replace the origins a secret may be filled into in browser sessions, or
|
|
37
|
+
* clear them with `null` (it then can't be filled until origins are set
|
|
38
|
+
* again). Not for `op_token` secrets.
|
|
39
|
+
*/
|
|
40
|
+
setAllowedOrigins(secret: string, allowedOrigins: string[] | null): Promise<Secret>;
|
|
26
41
|
/**
|
|
27
42
|
* Permanently delete a secret and its versions. Values already delivered to
|
|
28
43
|
* sandboxes are not revoked.
|
|
@@ -30,7 +45,7 @@ export declare class SecretsService extends BaseService {
|
|
|
30
45
|
delete(secret: string): Promise<SecretDeleteResult>;
|
|
31
46
|
/** Delete one stored version. The latest version cannot be deleted. */
|
|
32
47
|
deleteVersion(secret: string, version: number): Promise<SecretVersionDeleteResult>;
|
|
33
|
-
|
|
48
|
+
/** Sealed to the context the API returned for the secret; never one built here. */
|
|
34
49
|
private uploadVersion;
|
|
35
50
|
private resolve;
|
|
36
51
|
private resolveDetail;
|
package/dist/services/secrets.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SecretsService = void 0;
|
|
4
4
|
const error_1 = require("../error");
|
|
5
5
|
const secret_references_1 = require("../sandbox/secret-references");
|
|
6
|
+
const secret_values_1 = require("../secret-values");
|
|
6
7
|
const secrets_envelope_1 = require("../secrets-envelope");
|
|
7
8
|
const base_1 = require("./base");
|
|
8
9
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -44,11 +45,15 @@ class SecretsService extends base_1.BaseService {
|
|
|
44
45
|
(!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT)) {
|
|
45
46
|
throw new error_1.HyperbrowserError(`limit must be an integer from 1 to ${MAX_LIST_LIMIT}`);
|
|
46
47
|
}
|
|
48
|
+
if (params.type !== undefined && (typeof params.type !== "string" || !params.type)) {
|
|
49
|
+
throw (0, error_1.invalidRequest)('type must be a secret type such as "secret" or "totp"');
|
|
50
|
+
}
|
|
47
51
|
try {
|
|
48
52
|
return await this.request("/secrets", undefined, {
|
|
49
53
|
search: params.search,
|
|
50
54
|
page: params.page,
|
|
51
55
|
limit: params.limit,
|
|
56
|
+
type: params.type,
|
|
52
57
|
});
|
|
53
58
|
}
|
|
54
59
|
catch (error) {
|
|
@@ -57,30 +62,36 @@ class SecretsService extends base_1.BaseService {
|
|
|
57
62
|
}
|
|
58
63
|
/**
|
|
59
64
|
* Create a secret, and store its first version when `value` is given. The
|
|
60
|
-
* value
|
|
61
|
-
* bad value never leaves an
|
|
65
|
+
* value is checked against its type's rules, and the platform key is
|
|
66
|
+
* checked, before the secret is created, so a bad value never leaves an
|
|
67
|
+
* empty secret behind.
|
|
62
68
|
*/
|
|
63
69
|
async create(params) {
|
|
64
|
-
const { name, description, value } = params ?? {};
|
|
70
|
+
const { name, description, type, allowedOrigins, value } = params ?? {};
|
|
65
71
|
if (typeof name !== "string" || !secret_references_1.SECRET_NAME_PATTERN.test(name)) {
|
|
66
72
|
throw new error_1.HyperbrowserError("Secret name must be 1-128 letters, digits, '_' or '-', starting with a letter or digit");
|
|
67
73
|
}
|
|
68
|
-
|
|
74
|
+
if (type !== undefined && (typeof type !== "string" || !type)) {
|
|
75
|
+
throw (0, error_1.invalidRequest)('type must be "secret", "totp" or "op_token"');
|
|
76
|
+
}
|
|
77
|
+
const plaintext = value === undefined ? undefined : (0, secret_values_1.secretPlaintext)(type ?? "secret", value);
|
|
78
|
+
const sealer = plaintext === undefined ? undefined : secrets_envelope_1.SecretsSealer.fromPublicKey(await this.getPublicKey());
|
|
79
|
+
const metadata = allowedOrigins === undefined ? undefined : { browser: { allowedOrigins } };
|
|
69
80
|
let secret;
|
|
70
81
|
try {
|
|
71
82
|
secret = await this.request("/secrets", {
|
|
72
83
|
method: "POST",
|
|
73
|
-
body: JSON.stringify({ name, description }),
|
|
84
|
+
body: JSON.stringify({ name, description, type, metadata }),
|
|
74
85
|
});
|
|
75
86
|
}
|
|
76
87
|
catch (error) {
|
|
77
88
|
return rethrow(error, `Failed to create secret ${name}`);
|
|
78
89
|
}
|
|
79
|
-
if (sealer === undefined) {
|
|
90
|
+
if (sealer === undefined || plaintext === undefined) {
|
|
80
91
|
return secret;
|
|
81
92
|
}
|
|
82
93
|
try {
|
|
83
|
-
const version = await this.uploadVersion(secret, sealer,
|
|
94
|
+
const version = await this.uploadVersion(secret, sealer, plaintext);
|
|
84
95
|
return { ...secret, latestVersion: version.version };
|
|
85
96
|
}
|
|
86
97
|
catch (error) {
|
|
@@ -104,28 +115,56 @@ class SecretsService extends base_1.BaseService {
|
|
|
104
115
|
return rethrow(error, `Failed to get secret ${secret}`);
|
|
105
116
|
}
|
|
106
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Change a secret's description or metadata. Metadata members sent replace
|
|
120
|
+
* the stored ones, `null` removes a member, and members not sent are kept.
|
|
121
|
+
*/
|
|
107
122
|
async update(secret, params) {
|
|
123
|
+
const { description, metadata } = params ?? {};
|
|
124
|
+
if (description === undefined && metadata === undefined) {
|
|
125
|
+
throw (0, error_1.invalidRequest)("Pass a description or metadata to update");
|
|
126
|
+
}
|
|
127
|
+
if (metadata !== undefined && !(0, secrets_envelope_1.isPlainObject)(metadata)) {
|
|
128
|
+
throw (0, error_1.invalidRequest)("Secret metadata must be an object");
|
|
129
|
+
}
|
|
108
130
|
try {
|
|
109
131
|
const { id } = await this.resolve(secret);
|
|
110
132
|
return await this.request(secretPath(id), {
|
|
111
133
|
method: "PATCH",
|
|
112
|
-
body: JSON.stringify({ description
|
|
134
|
+
body: JSON.stringify({ description, metadata }),
|
|
113
135
|
});
|
|
114
136
|
}
|
|
115
137
|
catch (error) {
|
|
116
138
|
return rethrow(error, `Failed to update secret ${secret}`);
|
|
117
139
|
}
|
|
118
140
|
}
|
|
119
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Encrypt `value` locally and store it as the secret's next version. The
|
|
143
|
+
* value is checked against the rules of the secret's type, such as a TOTP
|
|
144
|
+
* seed's format, before it is sealed.
|
|
145
|
+
*/
|
|
120
146
|
async addVersion(secret, value) {
|
|
121
|
-
|
|
147
|
+
(0, secrets_envelope_1.secretValueBytes)(value);
|
|
148
|
+
const sealer = secrets_envelope_1.SecretsSealer.fromPublicKey(await this.getPublicKey());
|
|
122
149
|
try {
|
|
123
|
-
|
|
150
|
+
const resolved = await this.resolve(secret);
|
|
151
|
+
const plaintext = (0, secret_values_1.secretPlaintext)(resolved.type ?? "secret", value);
|
|
152
|
+
return await this.uploadVersion(resolved, sealer, plaintext);
|
|
124
153
|
}
|
|
125
154
|
catch (error) {
|
|
126
155
|
return rethrow(error, `Failed to add a version to secret ${secret}`);
|
|
127
156
|
}
|
|
128
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Replace the origins a secret may be filled into in browser sessions, or
|
|
160
|
+
* clear them with `null` (it then can't be filled until origins are set
|
|
161
|
+
* again). Not for `op_token` secrets.
|
|
162
|
+
*/
|
|
163
|
+
async setAllowedOrigins(secret, allowedOrigins) {
|
|
164
|
+
return this.update(secret, {
|
|
165
|
+
metadata: { browser: allowedOrigins === null ? null : { allowedOrigins } },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
129
168
|
/**
|
|
130
169
|
* Permanently delete a secret and its versions. Values already delivered to
|
|
131
170
|
* sandboxes are not revoked.
|
|
@@ -154,14 +193,11 @@ class SecretsService extends base_1.BaseService {
|
|
|
154
193
|
return rethrow(error, `Failed to delete version ${version} of secret ${secret}`);
|
|
155
194
|
}
|
|
156
195
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return secrets_envelope_1.SecretsSealer.fromPublicKey(await this.getPublicKey());
|
|
160
|
-
}
|
|
161
|
-
uploadVersion(secret, sealer, value) {
|
|
196
|
+
/** Sealed to the context the API returned for the secret; never one built here. */
|
|
197
|
+
uploadVersion(secret, sealer, plaintext) {
|
|
162
198
|
return this.request(`${secretPath(secret.id)}/versions`, {
|
|
163
199
|
method: "POST",
|
|
164
|
-
body: JSON.stringify({ envelope: sealer.seal(secret.encryptionContext,
|
|
200
|
+
body: JSON.stringify({ envelope: sealer.seal(secret.encryptionContext, plaintext) }),
|
|
165
201
|
});
|
|
166
202
|
}
|
|
167
203
|
async resolve(secret) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BasicResponse, CaptchaEvaluationParams, CaptchaEvaluationResponse, CreateSessionParams, CreateSessionSnapshotResponse, GetActiveSessionsCountResponse, GetSessionDownloadsUrlResponse, GetSessionRecordingUrlResponse, GetSessionVideoRecordingUrlResponse, SessionDetail, SessionListParams, SessionListResponse, SessionRecording, UploadFileOptions, UploadFileResponse, SessionEventLogListParams, SessionEventLogListResponse, UpdateSessionProfileParams, UpdateSessionProxyParams, UpdateSessionScreenParams, UpdateSessionSolveCaptchasParams, UpdateSessionSolveCaptchasResponse, SessionGetParams } from "../types/session";
|
|
2
2
|
import { BaseService } from "./base";
|
|
3
3
|
import { WebMCPService } from "./webmcp";
|
|
4
|
+
import { CredentialsService } from "./credentials";
|
|
4
5
|
/**
|
|
5
6
|
* Service for managing session event logs
|
|
6
7
|
*/
|
|
@@ -15,6 +16,7 @@ declare class SessionEventLogsService extends BaseService {
|
|
|
15
16
|
export declare class SessionsService extends BaseService {
|
|
16
17
|
readonly eventLogs: SessionEventLogsService;
|
|
17
18
|
readonly webmcp: WebMCPService;
|
|
19
|
+
readonly credentials: CredentialsService;
|
|
18
20
|
constructor(apiKey: string, baseUrl: string, timeout: number);
|
|
19
21
|
/**
|
|
20
22
|
* Create a new browser session
|
|
@@ -32,6 +32,7 @@ const path = __importStar(require("path"));
|
|
|
32
32
|
const form_data_1 = __importDefault(require("form-data"));
|
|
33
33
|
const base_1 = require("./base");
|
|
34
34
|
const webmcp_1 = require("./webmcp");
|
|
35
|
+
const credentials_1 = require("./credentials");
|
|
35
36
|
const client_1 = require("../client");
|
|
36
37
|
const CAPTCHA_EVALUATION_REQUEST_TIMEOUT_MS = 185000;
|
|
37
38
|
/**
|
|
@@ -63,6 +64,7 @@ class SessionsService extends base_1.BaseService {
|
|
|
63
64
|
super(apiKey, baseUrl, timeout);
|
|
64
65
|
this.eventLogs = new SessionEventLogsService(apiKey, baseUrl, timeout);
|
|
65
66
|
this.webmcp = new webmcp_1.WebMCPService(apiKey, baseUrl, timeout);
|
|
67
|
+
this.credentials = new credentials_1.CredentialsService(apiKey, baseUrl, timeout);
|
|
66
68
|
}
|
|
67
69
|
/**
|
|
68
70
|
* Create a new browser session
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CreateVaultParams, UpdateVaultParams, Vault, VaultDeleteResult, VaultListParams, VaultListResponse } from "../types/vault";
|
|
2
|
+
import { BaseService } from "./base";
|
|
3
|
+
/**
|
|
4
|
+
* Vaults name a set of environment variables whose values come from stored
|
|
5
|
+
* secrets. Pass one as `vault` to a sandbox process
|
|
6
|
+
* or sandbox create, or attach it to a browser session with
|
|
7
|
+
* `credentials: [{ vault }]` and fill its entries by name.
|
|
8
|
+
*
|
|
9
|
+
* Methods that take `vault` accept its ID or its exact name. A UUID-shaped
|
|
10
|
+
* argument is tried as an ID first and then as a name.
|
|
11
|
+
*/
|
|
12
|
+
export declare class VaultsService extends BaseService {
|
|
13
|
+
list(params?: VaultListParams): Promise<VaultListResponse>;
|
|
14
|
+
get(vault: string): Promise<Vault>;
|
|
15
|
+
/** Create a vault. Entries name secrets, never values. */
|
|
16
|
+
create(params: CreateVaultParams): Promise<Vault>;
|
|
17
|
+
/**
|
|
18
|
+
* Change a vault's description or its entries:
|
|
19
|
+
* each entry given replaces that variable, `null` removes it, and entries
|
|
20
|
+
* not given are kept.
|
|
21
|
+
*/
|
|
22
|
+
update(vault: string, params: UpdateVaultParams): Promise<Vault>;
|
|
23
|
+
/** Delete a vault. The secrets its entries name are kept. */
|
|
24
|
+
delete(vault: string): Promise<VaultDeleteResult>;
|
|
25
|
+
private resolve;
|
|
26
|
+
}
|