From b3d4220cea7e97761ccd5dfdaee1b86c13ffdeea Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 11 Aug 2026 21:56:39 +0200 Subject: [PATCH 1/4] fix(cli): give veryfront open a way to reach the deployed site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `open` only ever built Cloud dashboard addresses, so nothing in the CLI could produce the environment URL Deploy prints. A reader who did not record that URL had no command to get it back, and `open --json` handed automation a dashboard link instead. `veryfront open --site` now opens the deployed environment — the canonical `https://..veryfront.com` address `deploy` falls back to — defaulting to `production` and honoring `--env`. Without `--site`, `open` keeps opening dashboard pages exactly as before, and `--help` now says which is which. The deploy docs point readers at `--site` when they need the environment URL again, next to the existing note that plain `open` is the dashboard shortcut. --- cli/commands/open/command-help.ts | 11 +++++++++-- cli/commands/open/command.ts | 19 +++++++++++++++++++ cli/commands/open/handler.test.ts | 21 +++++++++++++++++++++ docs/api-reference/veryfront/cli.md | 2 +- docs/getting-started/deploy-project.md | 18 ++++++++++++++---- docs/guides/deploying.md | 14 ++++++++++---- tests/docs/guide-content.test.ts | 15 +++++++++++++++ 7 files changed, 89 insertions(+), 11 deletions(-) diff --git a/cli/commands/open/command-help.ts b/cli/commands/open/command-help.ts index 7f4316c328..94e5cc65fd 100644 --- a/cli/commands/open/command-help.ts +++ b/cli/commands/open/command-help.ts @@ -3,7 +3,7 @@ import type { CommandHelp } from "../../help/types.ts"; export const openHelp: CommandHelp = { name: "open", category: "project", - description: "Open project URLs in the browser", + description: "Open the Cloud dashboard, or the deployed site with --site", usage: "veryfront open [options]", options: [ { @@ -12,7 +12,11 @@ export const openHelp: CommandHelp = { }, { flag: "--env ", - description: "Open the project's Environments panel", + description: "Environment to open with --site; otherwise the Environments panel", + }, + { + flag: "--site", + description: "Open the deployed site instead of a dashboard page (default env: production)", }, { flag: "--studio", description: "Open Veryfront Studio" }, { flag: "--json", description: "Output URL as JSON instead of opening" }, @@ -21,6 +25,9 @@ export const openHelp: CommandHelp = { "veryfront open", "veryfront open --project my-project", "veryfront open --env staging", + "veryfront open --site", + "veryfront open --site --env staging", + "veryfront open --site --json", "veryfront open --studio", "veryfront open --json", ], diff --git a/cli/commands/open/command.ts b/cli/commands/open/command.ts index f25af43863..6c92b902d6 100644 --- a/cli/commands/open/command.ts +++ b/cli/commands/open/command.ts @@ -6,6 +6,7 @@ export const getOpenArgsSchema = defineSchema((v) => v.object({ env: v.string().optional(), studio: v.boolean().default(false), + site: v.boolean().optional(), projectSlug: v.string().optional(), }) ); @@ -17,12 +18,30 @@ export type OpenOptions = InferSchema>; export const parseOpenArgs = createArgParser(OpenArgsSchema, { env: { keys: ["env"], type: "string" }, studio: { keys: ["studio"], type: "boolean" }, + site: { keys: ["site"], type: "boolean" }, projectSlug: { keys: ["project", "project-slug", "p"], type: "string" }, }); const DASHBOARD_BASE = "https://veryfront.com"; +/** The environment `--site` targets when `--env` is absent. */ +const DEFAULT_SITE_ENVIRONMENT = "production"; + +/** + * The canonical Veryfront Cloud address of a deployed environment, the same + * `https://..veryfront.com` form `deploy` falls back to when + * an environment carries no custom domain. `open` has no API token, so it + * cannot look a custom domain up; a project that has one reaches the same + * deployment through both names. + */ +function buildSiteUrl(projectSlug: string, environment: string): string { + return `https://${projectSlug}.${environment}.veryfront.com`; +} + export function buildUrl(projectSlug: string, options: OpenOptions): string { + if (options.site) { + return buildSiteUrl(projectSlug, options.env ?? DEFAULT_SITE_ENVIRONMENT); + } if (options.studio) { return `${DASHBOARD_BASE}/studio/${projectSlug}`; } diff --git a/cli/commands/open/handler.test.ts b/cli/commands/open/handler.test.ts index 73ad02a1f1..9888e07ac8 100644 --- a/cli/commands/open/handler.test.ts +++ b/cli/commands/open/handler.test.ts @@ -122,6 +122,21 @@ describe("Open Command", () => { const url = buildUrl("custom-slug", { studio: false }); assertEquals(url, "https://veryfront.com/projects/custom-slug"); }); + + it("builds the deployed site URL with --site", () => { + const url = buildUrl("my-app", { studio: false, site: true }); + assertEquals(url, "https://my-app.production.veryfront.com"); + }); + + it("builds the deployed site URL for a named environment", () => { + const url = buildUrl("my-app", { env: "staging", studio: false, site: true }); + assertEquals(url, "https://my-app.staging.veryfront.com"); + }); + + it("keeps --site on the deployed site rather than a dashboard page", () => { + const url = buildUrl("my-app", { studio: true, site: true }); + assertEquals(url, "https://my-app.production.veryfront.com"); + }); }); describe("JSON output", () => { @@ -146,6 +161,12 @@ describe("Open Command", () => { assertSuccess(result); assertEquals(result.data.projectSlug, "my-project"); }); + + it("parses --site from raw open argv", () => { + const result = parseOpenArgs(parseCliArgs(["open", "--site"])); + assertSuccess(result); + assertEquals(result.data.site, true); + }); }); describe("resolveOpenProjectSlug", () => { diff --git a/docs/api-reference/veryfront/cli.md b/docs/api-reference/veryfront/cli.md index 847402f68c..1b908ff0ea 100644 --- a/docs/api-reference/veryfront/cli.md +++ b/docs/api-reference/veryfront/cli.md @@ -52,7 +52,7 @@ The CLI groups commands by category. Each command supports `--help` for its full | `veryfront demo` | Interactive guided tour of Veryfront CLI | | `veryfront init` | Initialize a new Veryfront project | | `veryfront install` | Install AI assistant integrations (Cursor, Claude Code, etc.) | -| `veryfront open` | Open project URLs in the browser | +| `veryfront open` | Open the Cloud dashboard, or the deployed site with --site | | `veryfront project` | Delete a cloud project and everything it owns | | `veryfront start` | Run the production dashboard with proxy and TUI | | `veryfront studio` | Open Veryfront Studio in browser | diff --git a/docs/getting-started/deploy-project.md b/docs/getting-started/deploy-project.md index 5245a60061..6e9fbd0acb 100644 --- a/docs/getting-started/deploy-project.md +++ b/docs/getting-started/deploy-project.md @@ -110,10 +110,20 @@ curl -sSf -N -X POST /api/ag-ui \ -d '{"messages":[{"id":"1","role":"user","parts":[{"type":"text","text":"What is Veryfront in one sentence?"}]}]}' ``` -`veryfront open` opens the project in the Cloud dashboard, where the deployment -is listed; `veryfront open --env production` opens the project's Environments -panel. Neither opens the deployed site, so use the environment URL Deploy printed -to check the running deployment. +If you did not record the URL Deploy printed, `veryfront open --site` opens the +deployed site, and `veryfront open --site --json` prints that URL for scripts: + +```bash +veryfront open --site --json +``` + +```json +{ "success": true, "command": "open", "data": { "url": "https://.production.veryfront.com" } } +``` + +Without `--site`, `veryfront open` opens the project in the Cloud dashboard, +where the deployment is listed, and `veryfront open --env production` opens the +project's Environments panel. Neither opens the deployed site. For an automated production workflow, see [Deploy from CI](../guides/deploy-from-ci.md). diff --git a/docs/guides/deploying.md b/docs/guides/deploying.md index 541290f51e..406be16ffe 100644 --- a/docs/guides/deploying.md +++ b/docs/guides/deploying.md @@ -103,10 +103,15 @@ npx veryfront@latest deploy --branch feature-x --env staging Deploy uses the last verified Push receipt and verifies the release was built from that exact source digest before assigning it to the environment. If no Push receipt exists, Deploy first runs a quiet Push so the first deployment still -works as one command. Deploy prints the environment URL; record it when -automation needs the deployed URL later. Use `npx veryfront@latest open` after -deployment to open the project in the Cloud dashboard, and -`npx veryfront@latest open --json` to print that dashboard URL. `open` resolves +works as one command. Deploy prints the environment URL, and +`npx veryfront@latest open --site --json` prints it again when automation needs +it later; `npx veryfront@latest open --site` opens it in a browser, defaulting to +`production` unless `--env` names another environment. Without `--site`, use +`npx veryfront@latest open` after deployment to open the project in the Cloud +dashboard, and `npx veryfront@latest open --json` to print that dashboard URL. +`--site` builds the canonical `https://..veryfront.com` +address; an environment served under a custom domain answers on both names. +`open` resolves the same project reference Push and Deploy use, including the local `.veryfront/project.json` link. Dashboard URLs are built from the project slug, so `open` skips an ID-only `VERYFRONT_PROJECT_ID` or `TENANT_PROJECT_ID` @@ -165,6 +170,7 @@ After `veryfront deploy`: unconfirmed data-plane update is a warning after commit, not a failed deploy; do not retry solely because of that warning. - The environment URL Deploy printed serves the deployed page and API routes. +- `veryfront open --site` opens that same environment URL. - `veryfront open` opens the project in the Cloud dashboard, not the deployed site. - The same page, API route, agent, workflow, task, or run path works in diff --git a/tests/docs/guide-content.test.ts b/tests/docs/guide-content.test.ts index 63fc860aff..4896f86c79 100644 --- a/tests/docs/guide-content.test.ts +++ b/tests/docs/guide-content.test.ts @@ -200,6 +200,21 @@ describe("guide content contracts", () => { ); }); + it("offers open --site as the way back to the deployed environment URL", async () => { + // The deploy docs tell readers to use the URL Deploy printed. A reader who + // did not record it needs a command that reproduces it, so both deploy + // pages must name `open --site` alongside the dashboard-only `open`. + for ( + const path of [ + "docs/getting-started/deploy-project.md", + "docs/guides/deploying.md", + ] + ) { + const text = await Deno.readTextFile(path); + assertStringIncludes(text, "open --site"); + } + }); + it("uses serve for local production builds", async () => { const docs = [ "docs/getting-started/deploy-project.md", From 57940ee891cb036871e2e50332742078c1632ee6 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 11 Aug 2026 22:20:27 +0200 Subject: [PATCH 2/4] docs: stop claiming open --site reprints the URL Deploy showed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up. When an environment carries a custom domain, Deploy prints `environment.domains[0]`, while `--site` always synthesizes the canonical `veryfront.com` hostname — `open` has no API token with which to read the configured domains. Both reach the same deployment, but the origins differ, so the guide no longer says `--site` prints the same URL and now tells automation that needs the custom domain to record what Deploy printed. --- docs/guides/deploying.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/guides/deploying.md b/docs/guides/deploying.md index 406be16ffe..b0e995c9c7 100644 --- a/docs/guides/deploying.md +++ b/docs/guides/deploying.md @@ -103,17 +103,23 @@ npx veryfront@latest deploy --branch feature-x --env staging Deploy uses the last verified Push receipt and verifies the release was built from that exact source digest before assigning it to the environment. If no Push receipt exists, Deploy first runs a quiet Push so the first deployment still -works as one command. Deploy prints the environment URL, and -`npx veryfront@latest open --site --json` prints it again when automation needs -it later; `npx veryfront@latest open --site` opens it in a browser, defaulting to -`production` unless `--env` names another environment. Without `--site`, use -`npx veryfront@latest open` after deployment to open the project in the Cloud -dashboard, and `npx veryfront@latest open --json` to print that dashboard URL. -`--site` builds the canonical `https://..veryfront.com` -address; an environment served under a custom domain answers on both names. -`open` resolves -the same project reference Push and Deploy use, including the local -`.veryfront/project.json` link. Dashboard URLs are built from the project slug, +works as one command. Deploy prints the environment URL. +`npx veryfront@latest open --site` opens the deployed environment in a browser +and `npx veryfront@latest open --site --json` prints its URL for automation, +defaulting to `production` unless `--env` names another environment. Without +`--site`, use `npx veryfront@latest open` after deployment to open the project in +the Cloud dashboard, and `npx veryfront@latest open --json` to print that +dashboard URL. + +`--site` always builds the canonical +`https://..veryfront.com` address, because `open` has no API +token with which to read the environment's configured domains. Deploy prints the +custom domain when the environment has one, so the two can differ in origin even +though both reach the same deployment. Automation that must use the custom domain +should record the URL Deploy printed rather than rebuild it from `open --site`. + +`open` resolves the same project reference Push and Deploy use, including the +local `.veryfront/project.json` link. Dashboard URLs are built from the project slug, so `open` skips an ID-only `VERYFRONT_PROJECT_ID` or `TENANT_PROJECT_ID` reference and uses the link instead. @@ -170,7 +176,8 @@ After `veryfront deploy`: unconfirmed data-plane update is a warning after commit, not a failed deploy; do not retry solely because of that warning. - The environment URL Deploy printed serves the deployed page and API routes. -- `veryfront open --site` opens that same environment URL. +- `veryfront open --site` reaches that deployment at its canonical + `https://..veryfront.com` address. - `veryfront open` opens the project in the Cloud dashboard, not the deployed site. - The same page, API route, agent, workflow, task, or run path works in From 97ef2f67eaf6d69f84aa5ed80d7e012319c592c8 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Wed, 12 Aug 2026 06:11:58 +0200 Subject: [PATCH 3/4] fix(cli): keep open --site on a Veryfront origin `--site` is the only `open` path that puts a resolved value in the URL authority rather than its path. A slug or environment carrying `/`, `?`, or `#` pushed the hard-coded suffix into the path and produced a link Veryfront does not own: `evil.example/x` built `https://evil.example/x.production.veryfront.com`, which `open` then handed to the browser or printed as the `--json` result. The slug is not always typed at the prompt. It also comes from `veryfront.json` and the local project link, which arrive with a cloned repository, so it is validated rather than trusted. Both labels now have to be DNS labels, the same guard `push` applies before building a preview hostname, and fail with the existing INVALID_ARGUMENT usage error. Dashboard URLs are unchanged: the slug sits in the path there, where it cannot move the origin. --- cli/commands/open/command.ts | 32 +++++++++++++++++++++++++++++ cli/commands/open/handler.test.ts | 34 ++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/cli/commands/open/command.ts b/cli/commands/open/command.ts index 6c92b902d6..644e389d67 100644 --- a/cli/commands/open/command.ts +++ b/cli/commands/open/command.ts @@ -1,6 +1,7 @@ import { defineSchema, lazySchema } from "veryfront/schemas"; import type { InferSchema } from "veryfront/extensions/schema"; import { createArgParser } from "#cli/shared/args"; +import { INVALID_ARGUMENT } from "veryfront/errors"; export const getOpenArgsSchema = defineSchema((v) => v.object({ @@ -27,6 +28,35 @@ const DASHBOARD_BASE = "https://veryfront.com"; /** The environment `--site` targets when `--env` is absent. */ const DEFAULT_SITE_ENVIRONMENT = "production"; +/** + * A single DNS label. `--site` is the only `open` path that puts a resolved + * value in the URL *authority* rather than its path, so it is the only one + * where a stray `/`, `?`, or `#` changes the origin: `evil.example/x` would + * build `https://evil.example/x.production.veryfront.com`, pushing the + * hard-coded suffix into the path and leaving a link Veryfront does not own. + * The slug is not always typed by the person running the command — it also + * comes from `veryfront.json` and the local project link, which arrive with a + * cloned repository — so it is validated rather than trusted. + */ +const SITE_HOSTNAME_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i; + +/** The longest a single DNS label may be, as `push` also enforces. */ +const MAX_HOSTNAME_LABEL_LENGTH = 63; + +function assertSiteHostnameLabel(value: string, label: string): void { + if ( + SITE_HOSTNAME_LABEL_PATTERN.test(value) && + value.length <= MAX_HOSTNAME_LABEL_LENGTH + ) { + return; + } + + throw INVALID_ARGUMENT.create({ + detail: `The ${label} "${value}" is not a DNS label, so it cannot name a deployed site. ` + + `Use letters, digits, and hyphens only.`, + }); +} + /** * The canonical Veryfront Cloud address of a deployed environment, the same * `https://..veryfront.com` form `deploy` falls back to when @@ -35,6 +65,8 @@ const DEFAULT_SITE_ENVIRONMENT = "production"; * deployment through both names. */ function buildSiteUrl(projectSlug: string, environment: string): string { + assertSiteHostnameLabel(projectSlug, "project slug"); + assertSiteHostnameLabel(environment, "environment"); return `https://${projectSlug}.${environment}.veryfront.com`; } diff --git a/cli/commands/open/handler.test.ts b/cli/commands/open/handler.test.ts index 9888e07ac8..a390384364 100644 --- a/cli/commands/open/handler.test.ts +++ b/cli/commands/open/handler.test.ts @@ -1,5 +1,5 @@ import "#veryfront/schemas/_test-setup.ts"; -import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts"; +import { assertEquals, assertRejects, assertThrows } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { join } from "veryfront/platform/path"; import { parseCliArgs } from "#cli/shared/args"; @@ -137,6 +137,38 @@ describe("Open Command", () => { const url = buildUrl("my-app", { studio: true, site: true }); assertEquals(url, "https://my-app.production.veryfront.com"); }); + + it("refuses a project slug that would change the site origin", () => { + // `--site` is the only `open` path that interpolates into the URL + // authority, so a slug carrying `/`, `?`, or `#` pushes the hard-coded + // `.veryfront.com` suffix into the path and leaves an origin Veryfront + // does not own. The slug can come from a cloned repo's `veryfront.json`, + // so it is never trusted. + for (const slug of ["evil.example/x", "evil.example?x", "evil.example#x"]) { + assertThrows( + () => buildUrl(slug, { studio: false, site: true }), + Error, + "DNS label", + ); + } + }); + + it("refuses an environment that would change the site origin", () => { + for (const env of ["attacker.example/", "a?b", "a#b"]) { + assertThrows( + () => buildUrl("my-app", { env, studio: false, site: true }), + Error, + "DNS label", + ); + } + }); + + it("still builds dashboard URLs for a slug --site would reject", () => { + // Dashboard URLs put the slug in the path, where it cannot move the + // origin, so validation is scoped to `--site` and does not change them. + const url = buildUrl("evil.example/x", { studio: false }); + assertEquals(url, "https://veryfront.com/projects/evil.example/x"); + }); }); describe("JSON output", () => { From 7f02e3357b8fdb32e7c2810f299ce943e68ca83c Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Wed, 12 Aug 2026 06:12:03 +0200 Subject: [PATCH 4/4] docs: name the environment in the staging open --site examples The examples sit under "For an existing nonproduction environment named `staging`", but a bare `open --site` targets `production`, so a reader copying the sequence would land on the wrong environment. Both examples now pass `--env staging`, and the sentence still states the default. The "After `veryfront deploy`" checklist keeps the generic `https://..veryfront.com` form; it covers any environment, not the staging walkthrough. --- docs/guides/deploying.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/guides/deploying.md b/docs/guides/deploying.md index b0e995c9c7..49bd3cac3f 100644 --- a/docs/guides/deploying.md +++ b/docs/guides/deploying.md @@ -104,12 +104,12 @@ Deploy uses the last verified Push receipt and verifies the release was built from that exact source digest before assigning it to the environment. If no Push receipt exists, Deploy first runs a quiet Push so the first deployment still works as one command. Deploy prints the environment URL. -`npx veryfront@latest open --site` opens the deployed environment in a browser -and `npx veryfront@latest open --site --json` prints its URL for automation, -defaulting to `production` unless `--env` names another environment. Without -`--site`, use `npx veryfront@latest open` after deployment to open the project in -the Cloud dashboard, and `npx veryfront@latest open --json` to print that -dashboard URL. +`npx veryfront@latest open --site --env staging` opens that deployed environment +in a browser, and `npx veryfront@latest open --site --env staging --json` prints +its URL for automation. `--site` targets `production` unless `--env` names +another environment, so name the environment you deployed. Without `--site`, use +`npx veryfront@latest open` after deployment to open the project in the Cloud +dashboard, and `npx veryfront@latest open --json` to print that dashboard URL. `--site` always builds the canonical `https://..veryfront.com` address, because `open` has no API