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.
@@ -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
+ }
@@ -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 isPortReachable(info.port)))
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 devToolsUrl = await resolveDevToolsActivePortUrl(port, userDataDirs);
173
+ const probe = createJsonVersionProbe();
174
+ const devToolsUrl = await resolveDevToolsActivePortUrl(port, userDataDirs, probe);
152
175
  if (devToolsUrl)
153
176
  return devToolsUrl;
154
- const jsonVersionUrl = await probeJsonVersion(port);
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 isPortReachable(info.port)))
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 probeJsonVersion(port);
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, { session }, corsHeaders, response);
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 { chromium } from "playwright-core";
20
+ import { browserbase, Stagehand } from "@browserbasehq/stagehand";
21
+ import { z } from "zod/v4";
21
22
 
22
- defineFn("my-function", async (context) => {
23
- const browser = await chromium.connectOverCDP(context.session.connectUrl);
24
- const page = browser.contexts()[0]!.pages()[0]!;
25
-
26
- await page.goto("https://news.ycombinator.com");
27
- await page.waitForSelector(".athing", { timeout: 30_000 });
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
- const titles = await page.$$eval(".athing .titleline > a", (elements) =>
30
- elements.slice(0, 3).map((element) => element.textContent),
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
- return {
34
- message: "Fetched top Hacker News titles",
35
- titles,
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", "playwright-core"], projectRoot);
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
- packageManager === "pnpm" ? "pnpm dev" : "npm run dev",
92
- packageManager === "pnpm" ? "pnpm run deploy" : "npm run deploy",
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
- copyFileSync(join(root, "package.json"), join(tempDir, "package.json"));
126
- const result = spawnSync("npm", ["install", "--package-lock-only"], {
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
- fail("Failed to generate package-lock.json for the Functions build archive.");
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
+ }