@haven_ai/cli 0.0.0-dev.202609051106.201fc90 → 0.0.0-dev.202609051302.7cc48d5

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 CHANGED
@@ -76,6 +76,46 @@ human goes to stderr. That holds for refusals too, which is the half a caller
76
76
  cannot work around: parse stdout, branch on the exit code, and read stderr only
77
77
  when a person is watching.
78
78
 
79
+ ### Signing in without a password (#2526)
80
+
81
+ `haven login` starts a **browser-approved** flow by default. It prints a link
82
+ and a code; a human opens the link, sees who is asking — the `client_label` the
83
+ CLI sent — and what the session may do, then approves.
84
+ There is no password anywhere in that path, which is the point: an agent
85
+ driving this CLI must never hold its user's password.
86
+
87
+ ```bash
88
+ haven login --json
89
+ # {"ok":true,"verification_url":"https://app.haven…/device?code=ABCD-2345",
90
+ # "user_code":"ABCD-2345","expires_at":"…"}
91
+ ```
92
+
93
+ Under `--json` that object is printed **before** polling begins, so an agent
94
+ can hand its user the link immediately rather than after the flow completes.
95
+ Add `--no-wait` to stop there and poll later; without it the CLI waits at the
96
+ interval the server names, widening it when the server says `slow_down`.
97
+
98
+ Exit codes carry the outcome an agent acts on: **3** when the code expired
99
+ (ask for a new one), **4** when the human denied it (stop asking).
100
+
101
+ `haven login --email <address>` keeps the password path for a human who wants
102
+ it. It is not removed — it is simply no longer what an agent gets by asking to
103
+ log in.
104
+
105
+ **What the approved session can do.** Create and manage agents, set up a
106
+ connection, and read your account. **What it cannot:** sign anything, approve a
107
+ budget, change signers, move funds, change your credentials, or rotate an
108
+ agent's keys — neither the delegate key (`/agents/:id/rekey/*`) nor the API key
109
+ (`/agents/:id/rotate-key`). `haven agents rotate-key` therefore needs an
110
+ ordinary session (`haven login --email`), not a device-code one: issuing a
111
+ fresh credential is a change of authority, and the human keeps those. The allow-list lives in
112
+ `packages/backend/src/middleware/owner-cli.ts`; a route that is not on it
113
+ refuses, because #1640 already refuses every purpose-carrying token everywhere
114
+ and this is a single opt-in exception. A census test measures what the
115
+ enforcement actually answers for every registered route, refuses an entry whose
116
+ route does not exist or is not behind `authMiddleware`, and holds the list
117
+ against an independent opinion about which path shapes are authority.
118
+
79
119
  ```bash
80
120
  haven agents list --json # success: the payload, unchanged
81
121
  haven agents show missing --json # failure: one object, still parseable
package/dist/cli.cjs CHANGED
@@ -22,13 +22,14 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
22
22
  ]);
23
23
  function parseArgs(argv) {
24
24
  const positionals = [];
25
- const flags = { json: false, help: false, version: false, yes: false };
25
+ const flags = { json: false, help: false, version: false, yes: false, noWait: false };
26
26
  for (let i = 0; i < argv.length; i += 1) {
27
27
  const arg = argv[i];
28
28
  if (arg === "--json") flags.json = true;
29
29
  else if (arg === "--help" || arg === "-h") flags.help = true;
30
30
  else if (arg === "--version" || arg === "-v") flags.version = true;
31
31
  else if (arg === "--yes" || arg === "-y") flags.yes = true;
32
+ else if (arg === "--no-wait") flags.noWait = true;
32
33
  else if (VALUE_FLAGS.has(arg)) {
33
34
  const value = argv[++i];
34
35
  if (value === void 0 || value.startsWith("--")) {
@@ -116,10 +117,20 @@ function helpText() {
116
117
  // src/api.ts
117
118
  var CliApiError = class extends Error {
118
119
  status;
119
- constructor(message, status) {
120
+ /**
121
+ * The parsed response body, when there was one (#2526).
122
+ *
123
+ * The message is built FROM `body.error` for humans, and the device flow
124
+ * needs the same value as DATA: `authorization_pending` and `slow_down` are
125
+ * control signals in a poll loop, and branching on them by matching the
126
+ * message string would make the loop depend on prose that is free to change.
127
+ */
128
+ body;
129
+ constructor(message, status, body) {
120
130
  super(message);
121
131
  this.name = "CliApiError";
122
132
  this.status = status;
133
+ this.body = body;
123
134
  }
124
135
  };
125
136
  function createCliApi({ baseUrl, token, fetchImpl = fetch }) {
@@ -148,7 +159,7 @@ function createCliApi({ baseUrl, token, fetchImpl = fetch }) {
148
159
  const payload = text ? safeParse(text) : void 0;
149
160
  if (!res.ok) {
150
161
  const message = (payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string" ? payload.error : null) ?? `Request failed (HTTP ${res.status}).`;
151
- throw new CliApiError(message, res.status);
162
+ throw new CliApiError(message, res.status, payload);
152
163
  }
153
164
  return payload;
154
165
  }
@@ -272,10 +283,24 @@ var UsageError = class extends Error {
272
283
  this.hint = hint;
273
284
  }
274
285
  };
286
+ var HavenCliError = class extends Error {
287
+ exit;
288
+ hint;
289
+ constructor(message, exit, hint) {
290
+ super(message);
291
+ this.name = "HavenCliError";
292
+ this.exit = exit;
293
+ this.hint = hint;
294
+ }
295
+ };
275
296
  function toFailure(err) {
276
297
  if (err instanceof UsageError) {
277
298
  return { code: "usage", exit: EXIT.usage, message: err.message, hint: err.hint };
278
299
  }
300
+ if (err instanceof HavenCliError) {
301
+ const code = err.exit === EXIT.notAuthenticated ? "not_authenticated" : err.exit === EXIT.refused ? "refused" : err.exit === EXIT.network ? "network" : "failed";
302
+ return { code, exit: err.exit, message: err.message, hint: err.hint };
303
+ }
279
304
  if (err instanceof CliApiError) {
280
305
  if (err.status === 0) {
281
306
  return {
@@ -332,7 +357,7 @@ ${failure.hint}` : failure.message);
332
357
  }
333
358
 
334
359
  // src/agent-guidance-text.ts
335
- var HAVEN_AGENT_RUNBOOK_MD = "# Haven for agents \u2014 set your user up to let you pay\n\nYou are an AI agent whose user has no Haven account yet, or has one but has not connected you.\n\n> Links below are paths on this same host: resolve them against the URL you fetched this file from.\n\n## What Haven is, in three sentences\n\nYour user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.\n\n## The sequence\n\nFour of the six steps are your user's \u2014 each needs a human signature or a human decision. The rest is yours. If they already have a funded account, start at step 3.\n\n1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey (Face ID / Touch ID) or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device.\n2. **HUMAN \u2014 fund it.** USDC on Base, to the address the dashboard shows.\n3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back.\n4. **YOU \u2014 run the connector command** in that prompt (below). It makes your signing key locally, registering only the public address.\n5. **HUMAN \u2014 approve the budget** with their passkey, in the Haven tab they created the agent in: it advances to the approval step by itself once your run registers.\n6. **YOU \u2014 verify, then pay.**\n\n## Hand-off scripts\n\nSend these as your own message, `<host>` replaced by the host you fetched this file from. Say what you cannot do, not only what they must.\n\n**Before signup** (step 1):\n\n> I can do everything except the parts that need your signature. Please open `<host>/signup?next=/agents&via=agent` \u2014 name, email, password, then a passkey (Face ID / Touch ID) or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? `<host>/login?next=/agents`.\n\n**At the passkey step:**\n\n> The passkey must be made on your own device \u2014 it is what keeps the account yours, so nobody, Haven included, can move your funds without you. If this browser cannot, open `<host>/onboarding?next=/agents` on your phone and finish there.\n\n**At funding** (step 2):\n\n> Your Haven account needs USDC on Base before I can pay for anything \u2014 USDC only, no ETH: Haven sponsors the gas. The dashboard shows the address to send it to; a small amount first is fine.\n\n**At the budget** (step 3):\n\n> On `<host>/agents`, create an agent for me and set a budget \u2014 say 25 USDC per day. That is the limit I cannot exceed. It hands back a setup prompt: paste it to me and I run it here.\n\n**At budget approval** (step 5) \u2014 the moment your run reports that approval is required, before anything else. Send the first if your run carried `approval.url`, the second if it did not.\n\n> Setup is done on my side. Approve the budget here: <approval.url>. Approve it with your passkey; nothing can be spent until you do.\n\n> Setup is done on my side. Go back to the Haven tab where you created the agent \u2014 it should now be asking you to approve the budget. Approve it with your passkey; nothing can be spent until you do.\n\n## What you run\n\nThe setup prompt carries a **connector command** shaped like this:\n\n```\nnpx -y @haven_ai/connect@<channel> --setup EXAMPLE-SETUP-TOKEN-NOT-REAL --api <api-url> --ack-local-tools\n```\n\nRun the prompt's version, not this one: the token is one-time, the API URL is the backend's own, and `<channel>` is the npm tag your prompt names \u2014 never a tag you pick. The rules below come from that prompt, so \"me\" in them is your user, not Haven.\n\n- If you are an AI agent running this command yourself rather than a human pasting it, you should append --json: the connector then emits one machine-readable, secret-free result object on stdout with progress on stderr, and returns promptly instead of blocking while it waits for the budget approval.\n- When a --json outcome reports approval.required: true, your first action must be to relay the approval instruction to me in your own reply \u2014 if the outcome carries approval.url, give me that link; otherwise tell me to return to Haven and approve this agent's budget \u2014 before verifying the connection, restarting anything, or any other step. Never build that link yourself: relay the one the outcome gave you or none at all. Any restart the outcome asks for is a separate instruction to give me afterwards, once the approval is done.\n- If you ran the command without --json, the connector waits for the approval itself and prints its next steps when it finishes: relay the budget-approval instruction to me \u2014 the approval link if those steps printed one, otherwise that you need to return to Haven and approve this agent's budget \u2014 only if those printed next steps still ask for it. If they report the budget as already approved, there is nothing for me to approve.\n- Only two changes to the command above are permitted, and no others: appending --json, and \u2014 only if the connector refuses because it could not determine the agent runtime \u2014 re-running it once with --runtime <name> added, naming the harness you are running in, using one of the values that refusal lists. Never invent a runtime name and never change anything else.\n- Do not print private keys, API keys, credential file contents, or config secrets in chat or logs.\n\nIf the approval wait times out, nothing is lost \u2014 your agent is registered and the budget is still waiting to be approved. Send your user the `approval.url` your run reported, or, if it carried none, ask them to finish it in that same Haven tab. The outcome carries no setup id, so never assemble an approval link out of parts \u2014 relay the whole one it gave you or none at all.\n\n## How to verify\n\nCall `haven_get_agent`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus `spend_authority_readiness`:\n\n- `ready` \u2014 a budget is live; you can pay.\n- `needs_approval` \u2014 the connector finished, nobody approved yet. Ask your user again, in their Haven tab; there is no queue to wait in.\n- `revoked` \u2014 the credential is not active; ask your user to create a new agent.\n\n`ready` covers hosted identity and the budget only, not your local signer. Check that with `npx -y @haven_ai/connect@<channel> --doctor`, the same tag your prompt named \u2014 a separate command, so the two-changes rule does not bind it.\n\n## If you cannot open a browser\n\nNothing here needs you to. Steps 1-3 are links: hand your user the full `<host>/\u2026` URL and ask them to say when it is done. Step 5 is a link only when your run reported one in `approval.url` \u2014 otherwise it is the tab they already have open, as above. Then poll `haven_get_agent` until it reads `ready`. Do not route around the sign-in wall \u2014 it makes the account theirs, not yours.\n\n## Vocabulary\n\n| Term | What it is |\n|---|---|\n| **setup prompt** | The text the dashboard hands your user to paste to you. Carries the command and its rules. |\n| **connector command** | The `npx -y @haven_ai/connect@\u2026` line you run. One-time token, one use. |\n| **agent credential** | Your API key (`sk_agent_\u2026`), written to `~/.haven`. It identifies you; alone it cannot move money. |\n| **delegate key** | Your signing key, made on this machine and never sent anywhere. |\n| **budget** | The on-chain delegation your user signed. It authorises the payment; Haven constructs and relays it. |\n\nNext: [your agent hit a 402](/402.md) \xB7 [everything agent-readable](/llms.txt)\n";
360
+ var HAVEN_AGENT_RUNBOOK_MD = "# Haven for agents \u2014 set your user up to let you pay\n\nYou are an AI agent whose user has no Haven account yet, or has one but has not connected you.\n\n> Links below are paths on this same host: resolve them against the URL you fetched this file from.\n\n## What Haven is, in three sentences\n\nYour user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.\n\n## The sequence\n\nFour of the six steps are your user's \u2014 each needs a human signature or a human decision. The rest is yours. If they already have a funded account, start at step 3.\n\n1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey (Face ID / Touch ID) or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, `npx @haven_ai/cli login` afterwards gets you a scoped session for steps 3-4 \u2014 they approve a code in the browser, you never hold their password. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.\n2. **HUMAN \u2014 fund it.** USDC on Base, to the address the dashboard shows.\n3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back.\n4. **YOU \u2014 run the connector command** in that prompt (below). It makes your signing key locally, registering only the public address.\n5. **HUMAN \u2014 approve the budget** with their passkey, in the Haven tab they created the agent in: it advances to the approval step by itself once your run registers.\n6. **YOU \u2014 verify, then pay.**\n\n## Hand-off scripts\n\nSend these as your own message, `<host>` replaced by the host you fetched this file from. Say what you cannot do, not only what they must.\n\n**Before signup** (step 1):\n\n> I can do everything except the parts that need your signature. Please open `<host>/signup?next=/agents&via=agent` \u2014 name, email, password, then a passkey (Face ID / Touch ID) or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? `<host>/login?next=/agents`.\n\n**At the passkey step:**\n\n> The passkey must be made on your own device \u2014 it is what keeps the account yours, so nobody, Haven included, can move your funds without you. If this browser cannot, open `<host>/onboarding?next=/agents` on your phone and finish there.\n\n**At funding** (step 2):\n\n> Your Haven account needs USDC on Base before I can pay for anything \u2014 USDC only, no ETH: Haven sponsors the gas. The dashboard shows the address to send it to; a small amount first is fine.\n\n**At the budget** (step 3):\n\n> On `<host>/agents`, create an agent for me and set a budget \u2014 say 25 USDC per day. That is the limit I cannot exceed. It hands back a setup prompt: paste it to me and I run it here.\n\n**At budget approval** (step 5) \u2014 the moment your run reports that approval is required, before anything else. Send the first if your run carried `approval.url`, the second if it did not.\n\n> Setup is done on my side. Approve the budget here: <approval.url>. Approve it with your passkey; nothing can be spent until you do.\n\n> Setup is done on my side. Go back to the Haven tab where you created the agent \u2014 it should now be asking you to approve the budget. Approve it with your passkey; nothing can be spent until you do.\n\n## What you run\n\nThe setup prompt carries a **connector command** shaped like this:\n\n```\nnpx -y @haven_ai/connect@<channel> --setup EXAMPLE-SETUP-TOKEN-NOT-REAL --api <api-url> --ack-local-tools\n```\n\nRun the prompt's version, not this one: the token is one-time, the API URL is the backend's own, and `<channel>` is the npm tag your prompt names \u2014 never a tag you pick. The rules below come from that prompt, so \"me\" in them is your user, not Haven.\n\n- If you are an AI agent running this command yourself rather than a human pasting it, you should append --json: the connector then emits one machine-readable, secret-free result object on stdout with progress on stderr, and returns promptly instead of blocking while it waits for the budget approval.\n- When a --json outcome reports approval.required: true, your first action must be to relay the approval instruction to me in your own reply \u2014 if the outcome carries approval.url, give me that link; otherwise tell me to return to Haven and approve this agent's budget \u2014 before verifying the connection, restarting anything, or any other step. Never build that link yourself: relay the one the outcome gave you or none at all. Any restart the outcome asks for is a separate instruction to give me afterwards, once the approval is done.\n- If you ran the command without --json, the connector waits for the approval itself and prints its next steps when it finishes: relay the budget-approval instruction to me \u2014 the approval link if those steps printed one, otherwise that you need to return to Haven and approve this agent's budget \u2014 only if those printed next steps still ask for it. If they report the budget as already approved, there is nothing for me to approve.\n- Only two changes to the command above are permitted, and no others: appending --json, and \u2014 only if the connector refuses because it could not determine the agent runtime \u2014 re-running it once with --runtime <name> added, naming the harness you are running in, using one of the values that refusal lists. Never invent a runtime name and never change anything else.\n- Do not print private keys, API keys, credential file contents, or config secrets in chat or logs.\n\nIf the approval wait times out, nothing is lost \u2014 your agent is registered and the budget is still waiting to be approved. Send your user the `approval.url` your run reported, or, if it carried none, ask them to finish it in that same Haven tab. The outcome carries no setup id, so never assemble an approval link out of parts \u2014 relay the whole one it gave you or none at all.\n\n## How to verify\n\nCall `haven_get_agent`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus `spend_authority_readiness`:\n\n- `ready` \u2014 a budget is live; you can pay.\n- `needs_approval` \u2014 the connector finished, nobody approved yet. Ask your user again, in their Haven tab; there is no queue to wait in.\n- `revoked` \u2014 the credential is not active; ask your user to create a new agent.\n\n`ready` covers hosted identity and the budget only, not your local signer. Check that with `npx -y @haven_ai/connect@<channel> --doctor`, the same tag your prompt named \u2014 a separate command, so the two-changes rule does not bind it.\n\n## If you cannot open a browser\n\nNothing here needs you to. Steps 1-3 are links: hand your user the full `<host>/\u2026` URL and ask them to say when it is done. Step 5 is a link only when your run reported one in `approval.url` \u2014 otherwise it is the tab they already have open, as above. Then poll `haven_get_agent` until it reads `ready`. Do not route around the sign-in wall \u2014 it makes the account theirs, not yours.\n\n## Vocabulary\n\n| Term | What it is |\n|---|---|\n| **setup prompt** | The text the dashboard hands your user to paste to you. Carries the command and its rules. |\n| **connector command** | The `npx -y @haven_ai/connect@\u2026` line you run. One-time token, one use. |\n| **agent credential** | Your API key (`sk_agent_\u2026`), written to `~/.haven`. It identifies you; alone it cannot move money. |\n| **delegate key** | Your signing key, made on this machine and never sent anywhere. |\n| **budget** | The on-chain delegation your user signed. It authorises the payment; Haven constructs and relays it. |\n\nNext: [your agent hit a 402](/402.md) \xB7 [everything agent-readable](/llms.txt)\n";
336
361
 
337
362
  // src/token.ts
338
363
  function sessionExpiry(token) {
@@ -350,7 +375,7 @@ function sessionExpiry(token) {
350
375
 
351
376
  // src/commands.ts
352
377
  var DEFAULT_API = "https://havenbackend-production-8a00.up.railway.app";
353
- var CLI_VERSION = "0.0.0-dev.202609051106.201fc90";
378
+ var CLI_VERSION = "0.0.0-dev.202609051302.7cc48d5";
354
379
  async function run(argv, deps = {}) {
355
380
  const out = deps.out ?? ((l) => process.stdout.write(`${l}
356
381
  `));
@@ -362,6 +387,7 @@ async function run(argv, deps = {}) {
362
387
  sessionStore: deps.sessionStore ?? createSessionStore(),
363
388
  makeApi: deps.makeApi ?? ((baseUrl, token) => createCliApi({ baseUrl, token })),
364
389
  promptPassword: deps.promptPassword ?? (() => Promise.reject(new Error("No password input available"))),
390
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms))),
365
391
  out,
366
392
  err,
367
393
  env: deps.env ?? process.env,
@@ -456,10 +482,67 @@ async function cmdGuide(_args, d) {
456
482
  d.o.data({ ok: true, format: "markdown", content: HAVEN_AGENT_RUNBOOK_MD }, () => HAVEN_AGENT_RUNBOOK_MD);
457
483
  return EXIT.ok;
458
484
  }
485
+ async function deviceLogin(args, d, baseUrl) {
486
+ const api = d.makeApi(baseUrl);
487
+ const label = d.env.HAVEN_CLIENT_LABEL ?? `Haven CLI on ${d.env.HOSTNAME ?? "this machine"}`;
488
+ const start = await api.post("/auth/device/start", { client_label: label });
489
+ const deadline = Date.now() + start.expires_in * 1e3;
490
+ d.o.data(
491
+ {
492
+ ok: true,
493
+ verification_url: start.verification_url,
494
+ user_code: start.user_code,
495
+ expires_at: new Date(deadline).toISOString()
496
+ },
497
+ () => `Open ${start.verification_url}
498
+ and approve the code ${start.user_code}.
499
+ It expires in ${Math.round(start.expires_in / 60)} minutes.`
500
+ );
501
+ if (args.flags.noWait) return EXIT.ok;
502
+ let interval = start.interval * 1e3;
503
+ for (; ; ) {
504
+ if (Date.now() >= deadline) {
505
+ throw new HavenCliError("The code expired before it was approved.", EXIT.notAuthenticated);
506
+ }
507
+ await d.sleep(interval);
508
+ let res = null;
509
+ try {
510
+ res = await api.post("/auth/device/token", {
511
+ device_code: start.device_code
512
+ });
513
+ } catch (err) {
514
+ const code = deviceErrorCode(err);
515
+ if (code === "authorization_pending") continue;
516
+ if (code === "slow_down") {
517
+ interval += 5e3;
518
+ continue;
519
+ }
520
+ if (code === "access_denied") {
521
+ throw new HavenCliError("The request was denied.", EXIT.refused);
522
+ }
523
+ if (code === "expired_token") {
524
+ throw new HavenCliError("The code expired before it was approved.", EXIT.notAuthenticated);
525
+ }
526
+ throw err;
527
+ }
528
+ await d.sessionStore.save({ token: res.token, apiBaseUrl: baseUrl, user: res.user });
529
+ emit(
530
+ d,
531
+ args.flags.json,
532
+ { ok: true, email: res.user.email, expires_at: sessionExpiry(res.token), user: res.user, apiBaseUrl: baseUrl },
533
+ () => `Signed in as ${res.user.email}.`
534
+ );
535
+ return EXIT.ok;
536
+ }
537
+ }
538
+ function deviceErrorCode(err) {
539
+ const body = err?.body;
540
+ return typeof body?.error === "string" ? body.error : null;
541
+ }
459
542
  async function cmdLogin(args, d) {
460
543
  const email = args.flags.email ?? d.env.HAVEN_EMAIL;
461
544
  if (!email) {
462
- throw new UsageError("An email is required.", "Pass --email <address>, or set HAVEN_EMAIL.");
545
+ return deviceLogin(args, d, baseUrlFor(args, d, null));
463
546
  }
464
547
  const password = d.env.HAVEN_PASSWORD ?? await d.promptPassword();
465
548
  if (!password) {