browse 0.10.0 → 0.11.1
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/dist/commands/cloud/secrets/create.js +33 -0
- package/dist/commands/cloud/secrets/delete.js +23 -0
- package/dist/commands/cloud/secrets/get.js +24 -0
- package/dist/commands/cloud/secrets/list.js +20 -0
- package/dist/commands/cloud/secrets/update.js +34 -0
- package/dist/commands/functions/secrets/attach.js +27 -0
- package/dist/commands/functions/secrets/detach.js +27 -0
- package/dist/commands/functions/secrets/list.js +28 -0
- package/dist/lib/cloud/api.js +3 -0
- package/dist/lib/driver/local-cdp-discovery.js +48 -24
- package/dist/lib/functions/dev.js +3 -1
- package/dist/lib/functions/init.js +62 -19
- package/dist/lib/functions/publish.js +12 -3
- package/dist/lib/secrets/api.js +60 -0
- package/dist/lib/secrets/flags.js +36 -0
- package/dist/lib/secrets/input.js +33 -0
- package/dist/lib/secrets/seal.js +30 -0
- package/dist/lib/templates/api.js +1 -0
- package/dist/lib/templates/scaffold.js +29 -16
- package/oclif.manifest.json +493 -1
- package/package.json +10 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BrowseCommand } from "../../../base.js";
|
|
3
|
+
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
|
+
import { createSecret } from "../../../lib/secrets/api.js";
|
|
5
|
+
import { readSecretValue } from "../../../lib/secrets/input.js";
|
|
6
|
+
import { secretInputFlags } from "../../../lib/secrets/flags.js";
|
|
7
|
+
import { outputJson } from "../../../lib/output.js";
|
|
8
|
+
export default class SecretsCreate extends BrowseCommand {
|
|
9
|
+
static description = "Create a project secret. Encrypts the value locally with the project public key.";
|
|
10
|
+
static examples = [
|
|
11
|
+
"browse cloud secrets create SERVICE_TOKEN",
|
|
12
|
+
"browse cloud secrets create SERVICE_TOKEN --env MY_SERVICE_TOKEN",
|
|
13
|
+
"browse cloud secrets create SERVICE_TOKEN --stdin < ./secret.txt",
|
|
14
|
+
];
|
|
15
|
+
static args = {
|
|
16
|
+
key: Args.string({
|
|
17
|
+
description: "Name exposed in the function context.secrets object.",
|
|
18
|
+
required: true,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
static flags = { ...apiCommonFlags, ...secretInputFlags };
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(SecretsCreate);
|
|
24
|
+
const options = toApiOptions(flags);
|
|
25
|
+
const value = await readSecretValue({ stdin: flags.stdin, env: flags.env });
|
|
26
|
+
try {
|
|
27
|
+
outputJson(await createSecret(options, args.key, value));
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
value.fill(0);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BrowseCommand } from "../../../base.js";
|
|
3
|
+
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
|
+
import { deleteSecret } from "../../../lib/secrets/api.js";
|
|
5
|
+
export default class SecretsDelete extends BrowseCommand {
|
|
6
|
+
static description = "Delete a project secret.";
|
|
7
|
+
static examples = [
|
|
8
|
+
"browse cloud secrets delete <secretId>",
|
|
9
|
+
"browse cloud secrets delete d2c4f48f-38e9-4b82-a36a-2b373fd14a65",
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
secretId: Args.string({
|
|
13
|
+
description: "Project secret ID (e.g. d2c4f48f-38e9-4b82-a36a-2b373fd14a65).",
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
static flags = { ...apiCommonFlags };
|
|
18
|
+
async run() {
|
|
19
|
+
const { args, flags } = await this.parse(SecretsDelete);
|
|
20
|
+
const options = toApiOptions(flags);
|
|
21
|
+
await deleteSecret(options, args.secretId);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BrowseCommand } from "../../../base.js";
|
|
3
|
+
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
|
+
import { getSecret } from "../../../lib/secrets/api.js";
|
|
5
|
+
import { outputJson } from "../../../lib/output.js";
|
|
6
|
+
export default class SecretsGet extends BrowseCommand {
|
|
7
|
+
static description = "Get project secret metadata. Does not return the secret value.";
|
|
8
|
+
static examples = [
|
|
9
|
+
"browse cloud secrets get <secretId>",
|
|
10
|
+
"browse cloud secrets get d2c4f48f-38e9-4b82-a36a-2b373fd14a65",
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
secretId: Args.string({
|
|
14
|
+
description: "Project secret ID (e.g. d2c4f48f-38e9-4b82-a36a-2b373fd14a65).",
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
static flags = { ...apiCommonFlags };
|
|
19
|
+
async run() {
|
|
20
|
+
const { args, flags } = await this.parse(SecretsGet);
|
|
21
|
+
const options = toApiOptions(flags);
|
|
22
|
+
outputJson(await getSecret(options, args.secretId));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BrowseCommand } from "../../../base.js";
|
|
2
|
+
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
3
|
+
import { listSecrets } from "../../../lib/secrets/api.js";
|
|
4
|
+
import { listSecretsFlags, toListSecretsOptions, } from "../../../lib/secrets/flags.js";
|
|
5
|
+
import { outputJson } from "../../../lib/output.js";
|
|
6
|
+
export default class SecretsList extends BrowseCommand {
|
|
7
|
+
static description = "List project secret metadata with cursor pagination.";
|
|
8
|
+
static examples = [
|
|
9
|
+
"browse cloud secrets list",
|
|
10
|
+
"browse cloud secrets list --start-at 2026-01-01T00:00:00Z",
|
|
11
|
+
"browse cloud secrets list --start-at 2026-01-01T00:00:00Z --end-at 2026-02-01T00:00:00Z",
|
|
12
|
+
"browse cloud secrets list --limit 10",
|
|
13
|
+
];
|
|
14
|
+
static flags = { ...apiCommonFlags, ...listSecretsFlags };
|
|
15
|
+
async run() {
|
|
16
|
+
const { flags } = await this.parse(SecretsList);
|
|
17
|
+
const options = toApiOptions(flags);
|
|
18
|
+
outputJson(await listSecrets(options, toListSecretsOptions(flags)));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BrowseCommand } from "../../../base.js";
|
|
3
|
+
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
|
+
import { updateSecret } from "../../../lib/secrets/api.js";
|
|
5
|
+
import { readSecretValue } from "../../../lib/secrets/input.js";
|
|
6
|
+
import { secretInputFlags } from "../../../lib/secrets/flags.js";
|
|
7
|
+
import { outputJson } from "../../../lib/output.js";
|
|
8
|
+
export default class SecretsUpdate extends BrowseCommand {
|
|
9
|
+
static description = "Replace a secret value, encrypting it locally with the current project public key.";
|
|
10
|
+
static examples = [
|
|
11
|
+
"browse cloud secrets update <secretId>",
|
|
12
|
+
"browse cloud secrets update d2c4f48f-38e9-4b82-a36a-2b373fd14a65",
|
|
13
|
+
"browse cloud secrets update <secretId> --env MY_SERVICE_TOKEN",
|
|
14
|
+
"browse cloud secrets update <secretId> --stdin < ./secret.txt",
|
|
15
|
+
];
|
|
16
|
+
static args = {
|
|
17
|
+
secretId: Args.string({
|
|
18
|
+
description: "Project secret ID (e.g. d2c4f48f-38e9-4b82-a36a-2b373fd14a65).",
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static flags = { ...apiCommonFlags, ...secretInputFlags };
|
|
23
|
+
async run() {
|
|
24
|
+
const { args, flags } = await this.parse(SecretsUpdate);
|
|
25
|
+
const options = toApiOptions(flags);
|
|
26
|
+
const value = await readSecretValue({ stdin: flags.stdin, env: flags.env });
|
|
27
|
+
try {
|
|
28
|
+
outputJson(await updateSecret(options, args.secretId, value));
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
value.fill(0);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BrowseCommand } from "../../../base.js";
|
|
3
|
+
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
|
+
import { attachFunctionSecret } from "../../../lib/secrets/api.js";
|
|
5
|
+
export default class FunctionSecretsAttach extends BrowseCommand {
|
|
6
|
+
static description = "Attach an existing project secret to a function.";
|
|
7
|
+
static examples = [
|
|
8
|
+
"browse functions secrets attach <functionId> <secretId>",
|
|
9
|
+
"browse functions secrets attach 7b6e1c42-8d93-4a15-b2f0-9c6d3e8a5041 d2c4f48f-38e9-4b82-a36a-2b373fd14a65",
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
functionId: Args.string({
|
|
13
|
+
description: "Function ID (e.g. 7b6e1c42-8d93-4a15-b2f0-9c6d3e8a5041).",
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
secretId: Args.string({
|
|
17
|
+
description: "Project secret ID (e.g. d2c4f48f-38e9-4b82-a36a-2b373fd14a65).",
|
|
18
|
+
required: true,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
static flags = { ...apiCommonFlags };
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(FunctionSecretsAttach);
|
|
24
|
+
const options = toApiOptions(flags);
|
|
25
|
+
await attachFunctionSecret(options, args.functionId, args.secretId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BrowseCommand } from "../../../base.js";
|
|
3
|
+
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
|
+
import { detachFunctionSecret } from "../../../lib/secrets/api.js";
|
|
5
|
+
export default class FunctionSecretsDetach extends BrowseCommand {
|
|
6
|
+
static description = "Detach a secret from a function without deleting the secret.";
|
|
7
|
+
static examples = [
|
|
8
|
+
"browse functions secrets detach <functionId> <secretId>",
|
|
9
|
+
"browse functions secrets detach 7b6e1c42-8d93-4a15-b2f0-9c6d3e8a5041 d2c4f48f-38e9-4b82-a36a-2b373fd14a65",
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
functionId: Args.string({
|
|
13
|
+
description: "Function ID (e.g. 7b6e1c42-8d93-4a15-b2f0-9c6d3e8a5041).",
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
secretId: Args.string({
|
|
17
|
+
description: "Project secret ID (e.g. d2c4f48f-38e9-4b82-a36a-2b373fd14a65).",
|
|
18
|
+
required: true,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
static flags = { ...apiCommonFlags };
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(FunctionSecretsDetach);
|
|
24
|
+
const options = toApiOptions(flags);
|
|
25
|
+
await detachFunctionSecret(options, args.functionId, args.secretId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BrowseCommand } from "../../../base.js";
|
|
3
|
+
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
|
+
import { listFunctionSecrets } from "../../../lib/secrets/api.js";
|
|
5
|
+
import { listSecretsFlags, toListSecretsOptions, } from "../../../lib/secrets/flags.js";
|
|
6
|
+
import { outputJson } from "../../../lib/output.js";
|
|
7
|
+
export default class FunctionSecretsList extends BrowseCommand {
|
|
8
|
+
static description = "List metadata for secrets attached to a function with cursor pagination.";
|
|
9
|
+
static examples = [
|
|
10
|
+
"browse functions secrets list <functionId>",
|
|
11
|
+
"browse functions secrets list 7b6e1c42-8d93-4a15-b2f0-9c6d3e8a5041",
|
|
12
|
+
"browse functions secrets list <functionId> --start-at 2026-01-01T00:00:00Z",
|
|
13
|
+
"browse functions secrets list <functionId> --start-at 2026-01-01T00:00:00Z --end-at 2026-02-01T00:00:00Z",
|
|
14
|
+
"browse functions secrets list <functionId> --limit 10",
|
|
15
|
+
];
|
|
16
|
+
static args = {
|
|
17
|
+
functionId: Args.string({
|
|
18
|
+
description: "Function ID (e.g. 7b6e1c42-8d93-4a15-b2f0-9c6d3e8a5041).",
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static flags = { ...apiCommonFlags, ...listSecretsFlags };
|
|
23
|
+
async run() {
|
|
24
|
+
const { args, flags } = await this.parse(FunctionSecretsList);
|
|
25
|
+
const options = toApiOptions(flags);
|
|
26
|
+
outputJson(await listFunctionSecrets(options, args.functionId, toListSecretsOptions(flags)));
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/lib/cloud/api.js
CHANGED
|
@@ -328,6 +328,9 @@ function resolveCommandFromPathname(pathname) {
|
|
|
328
328
|
if (pathname.startsWith("/v1/extensions")) {
|
|
329
329
|
return "extensions";
|
|
330
330
|
}
|
|
331
|
+
if (pathname.startsWith("/v1/secrets")) {
|
|
332
|
+
return "secrets";
|
|
333
|
+
}
|
|
331
334
|
if (pathname.startsWith("/v1/functions")) {
|
|
332
335
|
return "functions";
|
|
333
336
|
}
|
|
@@ -59,24 +59,6 @@ export async function readDevToolsActivePort(userDataDir) {
|
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
function isPortReachable(port, timeoutMs = 500) {
|
|
63
|
-
return new Promise((resolve) => {
|
|
64
|
-
const socket = net.createConnection({ host: "127.0.0.1", port });
|
|
65
|
-
const timer = setTimeout(() => {
|
|
66
|
-
socket.destroy();
|
|
67
|
-
resolve(false);
|
|
68
|
-
}, timeoutMs);
|
|
69
|
-
socket.on("connect", () => {
|
|
70
|
-
clearTimeout(timer);
|
|
71
|
-
socket.destroy();
|
|
72
|
-
resolve(true);
|
|
73
|
-
});
|
|
74
|
-
socket.on("error", () => {
|
|
75
|
-
clearTimeout(timer);
|
|
76
|
-
resolve(false);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
62
|
async function probeJsonVersion(port) {
|
|
81
63
|
const controller = new AbortController();
|
|
82
64
|
const timer = setTimeout(() => controller.abort(), 2000);
|
|
@@ -97,6 +79,46 @@ async function probeJsonVersion(port) {
|
|
|
97
79
|
clearTimeout(timer);
|
|
98
80
|
}
|
|
99
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Multiple candidates (several profile dirs, or a profile dir and a fallback
|
|
84
|
+
* port) can reference the same port within one discovery call. Memoize the
|
|
85
|
+
* live /json/version probe per port so that port is only actually hit once.
|
|
86
|
+
*/
|
|
87
|
+
function createJsonVersionProbe() {
|
|
88
|
+
const cache = new Map();
|
|
89
|
+
return (port) => {
|
|
90
|
+
let pending = cache.get(port);
|
|
91
|
+
if (!pending) {
|
|
92
|
+
pending = probeJsonVersion(port);
|
|
93
|
+
cache.set(port, pending);
|
|
94
|
+
}
|
|
95
|
+
return pending;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function wsPathname(wsUrl) {
|
|
99
|
+
try {
|
|
100
|
+
return new URL(wsUrl).pathname;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A DevToolsActivePort file only reflects the browser instance that wrote it.
|
|
108
|
+
* If a different process later binds the same port (a very likely collision,
|
|
109
|
+
* since 9222 is the near-universal default debugging port), the cached
|
|
110
|
+
* port+wsPath pair points at a target that no longer exists. Confirm the
|
|
111
|
+
* port's live /json/version still reports this exact target before trusting
|
|
112
|
+
* it. Compare pathnames only (not the full URL) since Chrome can report the
|
|
113
|
+
* host as "127.0.0.1" or "localhost" depending on version/platform.
|
|
114
|
+
*/
|
|
115
|
+
async function isDevToolsActivePortFresh(info, probe = probeJsonVersion) {
|
|
116
|
+
const liveWsUrl = await probe(info.port);
|
|
117
|
+
if (!liveWsUrl)
|
|
118
|
+
return false;
|
|
119
|
+
const expectedPathname = wsPathname(buildDevToolsWsUrl(info.port, info.wsPath));
|
|
120
|
+
return (expectedPathname !== null && wsPathname(liveWsUrl) === expectedPathname);
|
|
121
|
+
}
|
|
100
122
|
async function verifyCdpWebSocket(wsUrl) {
|
|
101
123
|
return new Promise((resolve) => {
|
|
102
124
|
const url = new URL(wsUrl);
|
|
@@ -135,12 +157,12 @@ async function verifyCdpWebSocket(wsUrl) {
|
|
|
135
157
|
});
|
|
136
158
|
});
|
|
137
159
|
}
|
|
138
|
-
async function resolveDevToolsActivePortUrl(port, userDataDirs) {
|
|
160
|
+
async function resolveDevToolsActivePortUrl(port, userDataDirs, probe) {
|
|
139
161
|
for (const dir of userDataDirs) {
|
|
140
162
|
const info = await readDevToolsActivePort(dir);
|
|
141
163
|
if (!info || info.port !== port)
|
|
142
164
|
continue;
|
|
143
|
-
if (!(await
|
|
165
|
+
if (!(await isDevToolsActivePortFresh(info, probe)))
|
|
144
166
|
continue;
|
|
145
167
|
return buildDevToolsWsUrl(info.port, info.wsPath);
|
|
146
168
|
}
|
|
@@ -148,10 +170,11 @@ async function resolveDevToolsActivePortUrl(port, userDataDirs) {
|
|
|
148
170
|
}
|
|
149
171
|
export async function resolveWsTargetFromPort(port, options = {}) {
|
|
150
172
|
const userDataDirs = options.userDataDirs ?? getChromeUserDataDirs();
|
|
151
|
-
const
|
|
173
|
+
const probe = createJsonVersionProbe();
|
|
174
|
+
const devToolsUrl = await resolveDevToolsActivePortUrl(port, userDataDirs, probe);
|
|
152
175
|
if (devToolsUrl)
|
|
153
176
|
return devToolsUrl;
|
|
154
|
-
const jsonVersionUrl = await
|
|
177
|
+
const jsonVersionUrl = await probe(port);
|
|
155
178
|
if (jsonVersionUrl)
|
|
156
179
|
return jsonVersionUrl;
|
|
157
180
|
const fallback = buildDevToolsWsUrl(port, "/devtools/browser");
|
|
@@ -162,9 +185,10 @@ export async function resolveWsTargetFromPort(port, options = {}) {
|
|
|
162
185
|
export async function discoverLocalCdp(options = {}) {
|
|
163
186
|
const candidates = [];
|
|
164
187
|
const userDataDirs = options.userDataDirs ?? getChromeUserDataDirs();
|
|
188
|
+
const probe = createJsonVersionProbe();
|
|
165
189
|
for (const dir of userDataDirs) {
|
|
166
190
|
const info = await readDevToolsActivePort(dir);
|
|
167
|
-
if (!info || !(await
|
|
191
|
+
if (!info || !(await isDevToolsActivePortFresh(info, probe)))
|
|
168
192
|
continue;
|
|
169
193
|
candidates.push({
|
|
170
194
|
source: `DevToolsActivePort:${dir}`,
|
|
@@ -172,7 +196,7 @@ export async function discoverLocalCdp(options = {}) {
|
|
|
172
196
|
});
|
|
173
197
|
}
|
|
174
198
|
for (const port of options.fallbackPorts ?? DEFAULT_FALLBACK_PORTS) {
|
|
175
|
-
const wsUrl = await
|
|
199
|
+
const wsUrl = await probe(port);
|
|
176
200
|
if (!wsUrl)
|
|
177
201
|
continue;
|
|
178
202
|
if (!candidates.some((candidate) => candidate.wsUrl === wsUrl)) {
|
|
@@ -377,7 +377,9 @@ async function routeRequest(request, response, bridge, manifestStore, sessionMan
|
|
|
377
377
|
? body.params || {}
|
|
378
378
|
: {};
|
|
379
379
|
const session = await sessionManager.createSession(manifest.config?.sessionConfig);
|
|
380
|
-
const accepted = bridge.triggerInvocation(functionName, params,
|
|
380
|
+
const accepted = bridge.triggerInvocation(functionName, params,
|
|
381
|
+
// Match production, which always sends secrets.
|
|
382
|
+
{ session, secrets: {} }, corsHeaders, response);
|
|
381
383
|
if (!accepted) {
|
|
382
384
|
await sessionManager.closeSession(session.id);
|
|
383
385
|
sendJson(response, 503, { error: "No runtime is connected yet." }, corsHeaders);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { fail } from "../errors.js";
|
|
@@ -17,24 +17,49 @@ dist/
|
|
|
17
17
|
.DS_Store
|
|
18
18
|
`;
|
|
19
19
|
const starterFunctionTemplate = `import { defineFn } from "@browserbasehq/sdk-functions";
|
|
20
|
-
import {
|
|
20
|
+
import { browserbase, Stagehand } from "@browserbasehq/stagehand";
|
|
21
|
+
import { z } from "zod/v4";
|
|
21
22
|
|
|
22
|
-
defineFn(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
defineFn(
|
|
24
|
+
"my-function",
|
|
25
|
+
async (context) => {
|
|
26
|
+
const browser = await browserbase.connect({
|
|
27
|
+
// The local dev server has no secrets, so fall back to .env.
|
|
28
|
+
apiKey:
|
|
29
|
+
context.secrets.BROWSERBASE_API_KEY ?? process.env.BROWSERBASE_API_KEY!,
|
|
30
|
+
sessionId: context.session.id,
|
|
31
|
+
});
|
|
32
|
+
// In this example, Stagehand uses the Model Gateway where Browserbase charges for the tokens
|
|
33
|
+
const stagehand = await Stagehand.create({ browser });
|
|
34
|
+
const page = (await browser.context.activePage())!;
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
await page.goto("https://news.ycombinator.com");
|
|
37
|
+
const { data } = await stagehand.extract(
|
|
38
|
+
"Extract the top 3 stories with their rank, title, and link URL.",
|
|
39
|
+
z.object({
|
|
40
|
+
stories: z
|
|
41
|
+
.array(z.object({ rank: z.number(), title: z.string(), url: z.string() }))
|
|
42
|
+
.max(3),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
await stagehand.close();
|
|
47
|
+
return {
|
|
48
|
+
message: "Successfully fetched top Hacker News stories",
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
results: data.stories,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
// Upload the Stagehand extension once, then paste its ID here:
|
|
55
|
+
// browse cloud extensions upload node_modules/@browserbasehq/stagehand/dist/assets/stagehand-extension.zip
|
|
56
|
+
sessionConfig: { extensionId: "your-extension-id" },
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
`;
|
|
60
|
+
// pnpm 11 and later fail installs when esbuild's build script is not approved.
|
|
61
|
+
const pnpmWorkspaceTemplate = `allowBuilds:
|
|
62
|
+
esbuild: true
|
|
38
63
|
`;
|
|
39
64
|
const tsconfigTemplate = `{
|
|
40
65
|
"compilerOptions": {
|
|
@@ -71,9 +96,15 @@ export async function initFunctionsProject({ packageManager, projectName, }) {
|
|
|
71
96
|
await writeFile(join(projectRoot, ".gitignore"), gitignoreTemplate);
|
|
72
97
|
await writeFile(join(projectRoot, "index.ts"), starterFunctionTemplate);
|
|
73
98
|
await writeFile(join(projectRoot, "tsconfig.json"), tsconfigTemplate);
|
|
99
|
+
if (packageManager === "pnpm") {
|
|
100
|
+
await writeFile(join(projectRoot, "pnpm-workspace.yaml"), pnpmWorkspaceTemplate);
|
|
101
|
+
}
|
|
74
102
|
const install = packageManager === "pnpm" ? ["add"] : ["install"];
|
|
75
103
|
const installDev = packageManager === "pnpm" ? ["add", "-D"] : ["install", "--save-dev"];
|
|
76
|
-
runPackageManager(packageManager, [...install, "@browserbasehq/sdk-functions", "
|
|
104
|
+
runPackageManager(packageManager, [...install, "@browserbasehq/sdk-functions", "@browserbasehq/stagehand"], projectRoot);
|
|
105
|
+
// Match Stagehand's version of zod in order to pass type checks.
|
|
106
|
+
const zodVersion = readStagehandZodVersion(projectRoot);
|
|
107
|
+
runPackageManager(packageManager, [...install, zodVersion ? `zod@${zodVersion}` : "zod"], projectRoot);
|
|
77
108
|
runPackageManager(packageManager, [...installDev, "typescript", "@types/node"], projectRoot);
|
|
78
109
|
if (!existsSync(join(projectRoot, ".git"))) {
|
|
79
110
|
spawnSync("git", ["init"], {
|
|
@@ -88,8 +119,11 @@ export async function initFunctionsProject({ packageManager, projectName, }) {
|
|
|
88
119
|
nextSteps: [
|
|
89
120
|
`cd ${projectName}`,
|
|
90
121
|
"Edit .env with your Browserbase API key",
|
|
91
|
-
|
|
92
|
-
|
|
122
|
+
"browse cloud extensions upload node_modules/@browserbasehq/stagehand/dist/assets/stagehand-extension.zip",
|
|
123
|
+
"Paste the uploaded extension ID into index.ts",
|
|
124
|
+
"browse functions dev index.ts",
|
|
125
|
+
"browse functions publish index.ts",
|
|
126
|
+
"Create a BROWSERBASE_API_KEY secret with browse cloud secrets create, then attach it with browse functions secrets attach",
|
|
93
127
|
],
|
|
94
128
|
}, null, 2));
|
|
95
129
|
}
|
|
@@ -99,6 +133,15 @@ function ensureCommand(command) {
|
|
|
99
133
|
fail(`${command} is required but was not found on PATH.`);
|
|
100
134
|
}
|
|
101
135
|
}
|
|
136
|
+
function readStagehandZodVersion(projectRoot) {
|
|
137
|
+
try {
|
|
138
|
+
const stagehandPackageJson = JSON.parse(readFileSync(join(projectRoot, "node_modules", "@browserbasehq", "stagehand", "package.json"), "utf8"));
|
|
139
|
+
return stagehandPackageJson.dependencies?.zod;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
102
145
|
function runPackageManager(packageManager, args, cwd) {
|
|
103
146
|
const result = spawnSync(packageManager, args, {
|
|
104
147
|
cwd,
|
|
@@ -122,14 +122,23 @@ function ensureArchiveLockfile(root, entries) {
|
|
|
122
122
|
}
|
|
123
123
|
const tempDir = join(tmpdir(), `bb-functions-lockgen-${randomUUID()}`);
|
|
124
124
|
mkdirSync(tempDir, { recursive: true });
|
|
125
|
-
|
|
126
|
-
const
|
|
125
|
+
// Copy the files that publish uploads, so npm resolves local dependencies the same way the build does.
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const target = join(tempDir, entry);
|
|
128
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
129
|
+
copyFileSync(join(root, entry), target);
|
|
130
|
+
}
|
|
131
|
+
// Omit resolved URLs so the builder installs from its own registry rather than the one on your machine.
|
|
132
|
+
const result = spawnSync("npm", ["install", "--package-lock-only", "--omit-lockfile-registry-resolved"], {
|
|
127
133
|
cwd: tempDir,
|
|
128
134
|
stdio: "pipe",
|
|
129
135
|
});
|
|
130
136
|
if (result.status !== 0) {
|
|
131
137
|
rmSync(tempDir, { recursive: true, force: true });
|
|
132
|
-
|
|
138
|
+
const npmOutput = result.error
|
|
139
|
+
? result.error.message
|
|
140
|
+
: result.stderr.toString().trim();
|
|
141
|
+
fail(`Failed to generate package-lock.json for the Functions build archive.${npmOutput ? `\n${npmOutput}` : ""}`);
|
|
133
142
|
}
|
|
134
143
|
return {
|
|
135
144
|
entries: [...entries, "package-lock.json"].sort(),
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { sealSecret } from "./seal.js";
|
|
2
|
+
import { requestBrowserbase, requestBrowserbaseJson } from "../cloud/api.js";
|
|
3
|
+
export function listSecrets(options, query) {
|
|
4
|
+
return requestBrowserbaseJson(options, withQuery("/v1/secrets", query));
|
|
5
|
+
}
|
|
6
|
+
function withQuery(path, query) {
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
for (const [key, value] of Object.entries(query)) {
|
|
9
|
+
if (value !== undefined)
|
|
10
|
+
params.set(key, String(value));
|
|
11
|
+
}
|
|
12
|
+
const search = params.toString();
|
|
13
|
+
return search ? `${path}?${search}` : path;
|
|
14
|
+
}
|
|
15
|
+
export function getSecret(options, secretId) {
|
|
16
|
+
return requestBrowserbaseJson(options, secretPath(secretId));
|
|
17
|
+
}
|
|
18
|
+
export async function deleteSecret(options, secretId) {
|
|
19
|
+
await requestBrowserbase(options, secretPath(secretId), { method: "DELETE" });
|
|
20
|
+
}
|
|
21
|
+
function secretPath(id) {
|
|
22
|
+
return `/v1/secrets/${encodeURIComponent(id)}`;
|
|
23
|
+
}
|
|
24
|
+
export async function createSecret(options, secretKey, value) {
|
|
25
|
+
const sealed = await encryptValue(options, value);
|
|
26
|
+
return requestBrowserbaseJson(options, "/v1/secrets", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "content-type": "application/json" },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
secretKey,
|
|
31
|
+
...sealed,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function updateSecret(options, secretId, value) {
|
|
36
|
+
const sealed = await encryptValue(options, value);
|
|
37
|
+
return requestBrowserbaseJson(options, secretPath(secretId), {
|
|
38
|
+
method: "PATCH",
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
body: JSON.stringify(sealed),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function encryptValue(options, value) {
|
|
44
|
+
const keypair = await requestBrowserbaseJson(options, "/v1/secrets/keypair");
|
|
45
|
+
const sealedSecretValue = await sealSecret(keypair.publicKey, value);
|
|
46
|
+
return { keypairId: keypair.id, sealedSecretValue };
|
|
47
|
+
}
|
|
48
|
+
export async function attachFunctionSecret(options, functionId, secretId) {
|
|
49
|
+
await requestBrowserbase(options, `/v1/functions/${encodeURIComponent(functionId)}/secrets`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ secretId }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export async function detachFunctionSecret(options, functionId, secretId) {
|
|
56
|
+
await requestBrowserbase(options, `/v1/functions/${encodeURIComponent(functionId)}/secrets/${encodeURIComponent(secretId)}`, { method: "DELETE" });
|
|
57
|
+
}
|
|
58
|
+
export function listFunctionSecrets(options, functionId, query) {
|
|
59
|
+
return requestBrowserbaseJson(options, withQuery(`/v1/functions/${encodeURIComponent(functionId)}/secrets`, query));
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
export const listSecretsFlags = {
|
|
3
|
+
limit: Flags.integer({
|
|
4
|
+
min: 1,
|
|
5
|
+
max: 1000,
|
|
6
|
+
description: "Maximum results per page (API default: 20).",
|
|
7
|
+
}),
|
|
8
|
+
cursor: Flags.string({
|
|
9
|
+
description: "nextCursor from the previous page. Keep the same filters.",
|
|
10
|
+
}),
|
|
11
|
+
"start-at": Flags.string({
|
|
12
|
+
description: "Include secrets created on or after this RFC 3339 timestamp.",
|
|
13
|
+
}),
|
|
14
|
+
"end-at": Flags.string({
|
|
15
|
+
description: "Include secrets created on or before this RFC 3339 timestamp.",
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
export function toListSecretsOptions(flags) {
|
|
19
|
+
return {
|
|
20
|
+
limit: flags.limit,
|
|
21
|
+
cursor: flags.cursor,
|
|
22
|
+
startAt: flags["start-at"],
|
|
23
|
+
endAt: flags["end-at"],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export const secretInputFlags = {
|
|
27
|
+
env: Flags.string({
|
|
28
|
+
description: "Read the secret value from the named environment variable.",
|
|
29
|
+
helpValue: "VARIABLE_NAME",
|
|
30
|
+
exclusive: ["stdin"],
|
|
31
|
+
}),
|
|
32
|
+
stdin: Flags.boolean({
|
|
33
|
+
description: "Read the exact secret value from stdin, preserving whitespace.",
|
|
34
|
+
exclusive: ["env"],
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import password from "@inquirer/password";
|
|
2
|
+
import { fail } from "../errors.js";
|
|
3
|
+
export async function readSecretValue(options) {
|
|
4
|
+
if (options.env !== undefined) {
|
|
5
|
+
if (options.stdin)
|
|
6
|
+
fail("--env and --stdin cannot be used together.");
|
|
7
|
+
if (!options.env)
|
|
8
|
+
fail("--env requires an environment variable name.");
|
|
9
|
+
const value = Object.prototype.hasOwnProperty.call(process.env, options.env)
|
|
10
|
+
? process.env[options.env]
|
|
11
|
+
: undefined;
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
fail("The environment variable selected by --env is not set.");
|
|
14
|
+
return Buffer.from(value, "utf8");
|
|
15
|
+
}
|
|
16
|
+
if (options.stdin) {
|
|
17
|
+
if (process.stdin.isTTY)
|
|
18
|
+
fail("--stdin requires piped input or file redirection.");
|
|
19
|
+
const chunks = [];
|
|
20
|
+
for await (const chunk of process.stdin) {
|
|
21
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
22
|
+
}
|
|
23
|
+
return Buffer.concat(chunks);
|
|
24
|
+
}
|
|
25
|
+
if (!process.stdin.isTTY)
|
|
26
|
+
fail("Use --stdin for piped input or --env to read an environment variable.");
|
|
27
|
+
try {
|
|
28
|
+
return Buffer.from(await password({ message: "Secret value:" }, { output: process.stderr }), "utf8");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
fail("Secret input cancelled.");
|
|
32
|
+
}
|
|
33
|
+
}
|