diff --git a/cli/shared/deployment/deploy-project.test.ts b/cli/shared/deployment/deploy-project.test.ts index 77bf0013dd..4ef841f367 100644 --- a/cli/shared/deployment/deploy-project.test.ts +++ b/cli/shared/deployment/deploy-project.test.ts @@ -23,6 +23,7 @@ import { createHttpDeployControlPlane, type DeployControlPlane, type DeployReleaseAssetManifestBody, + type DeployReleaseFile, } from "./control-plane.ts"; import { assertProjectOwnership, @@ -2145,3 +2146,232 @@ describe("deployment routing convergence", () => { }); }); }); + +describe("unroutable hosted environment names", () => { + /** + * Live infrastructure only routes `{slug}.{preview|staging|production}.veryfront.com`. + * Any other label either has no wildcard certificate at all (TLS handshake failure) + * or resolves to a proxy that answers + * `404 {"error":"No project configured for domain: ..."}` — both of which the + * readiness poller treats as transient and retries until the timeout expires. + */ + function hostedNotFound() { + return new Response( + JSON.stringify({ error: "No project configured for domain", status: 404 }), + { status: 404, headers: { "content-type": "application/json" } }, + ); + } + + /** Matches on the parsed host, so a control-plane URL can never be mistaken for one. */ + function isHostedEnvironmentRequest(input: string | URL | Request): boolean { + const url = input instanceof Request ? input.url : String(input); + try { + return new URL(url).hostname.endsWith(".veryfront.com"); + } catch { + return false; + } + } + + it("rejects a user-created environment name before creating a release", async () => { + await withDeployEnv(async () => { + const { projectDir } = await createPushedProject(); + const controlPlane = new InMemoryDeployControlPlane(); + controlPlane.environmentDomains = []; + try { + const error = await expectDeployError(() => + withFetchStub( + (input) => isHostedEnvironmentRequest(input) ? hostedNotFound() : new Response("ready"), + () => + createDeployment(controlPlane).execute({ + projectDir, + environment: "development", + mode: "apply", + source: { kind: "already-pushed" }, + }), + ) + ); + + const message = (error as Error).message; + assertStringIncludes(message, "development"); + assertStringIncludes(message, "preview"); + assertStringIncludes(message, "staging"); + assertStringIncludes(message, "production"); + assertEquals( + controlPlane.createdReleases, + [], + "an unroutable environment must be rejected before any release is created", + ); + assertEquals( + controlPlane.createdDeployments, + [], + "an unroutable environment must be rejected before any deployment is created", + ); + } finally { + await Deno.remove(projectDir, { recursive: true }); + } + }); + }); + + it("does not spend the readiness timeout on a guaranteed failure", async () => { + await withDeployEnv(async () => { + const { projectDir } = await createPushedProject(); + const controlPlane = new InMemoryDeployControlPlane(); + controlPlane.environmentDomains = []; + let probes = 0; + try { + await expectDeployError(() => + withFetchStub( + (input) => { + if (isHostedEnvironmentRequest(input)) { + probes++; + return hostedNotFound(); + } + return new Response("ready"); + }, + () => + createDeployment(controlPlane).execute({ + projectDir, + environment: "qa", + mode: "apply", + source: { kind: "already-pushed" }, + }), + ) + ); + + assertEquals(probes, 0, "no readiness probe may be sent to an unroutable host"); + } finally { + await Deno.remove(projectDir, { recursive: true }); + } + }); + }); + + it("omits the canonical companion probe for a protected custom-domain environment", async () => { + const probed: string[] = []; + + await withMockFetch( + (input: string | URL | Request, init?: RequestInit) => { + const request = input instanceof Request ? input : new Request(input, init); + probed.push(request.url); + return Promise.resolve( + new Response(null, { + status: 302, + headers: { location: "https://veryfront.com/sign-in" }, + }), + ); + }, + () => + waitForEnvironmentReady({ + projectSlug: "my-project", + environmentName: "development", + url: "https://dev.example.com", + protected: true, + apiToken: "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJ1XzEifQ.test-signature", + }, { pollIntervalMs: 1, timeoutMs: 1_000 }), + ); + + assertEquals( + probed, + ["https://dev.example.com/"], + "the custom domain answered; no unroutable canonical host may be probed", + ); + }); + + /** + * An API-only, agent-only or otherwise page-less project. `readinessRoute` is + * null for it, so `buildEnvironmentReadinessProbes` yields nothing and the + * deploy never asks the platform for a hosted address — which is why the name + * check must not apply to it. + */ + const SERVER_ONLY_CONTENT = "export const handler = () => new Response('ok');\n"; + + async function createPushedPagelessProject(): Promise<{ + projectDir: string; + files: DeployReleaseFile[]; + }> { + const projectDir = await Deno.makeTempDir(); + await Deno.mkdir(`${projectDir}/server`, { recursive: true }); + await Deno.writeTextFile(`${projectDir}/veryfront.json`, projectConfigText()); + await Deno.writeTextFile(`${projectDir}/server/handler.ts`, SERVER_ONLY_CONTENT); + const commitSha = await commitProject(projectDir); + const files: DeployReleaseFile[] = [ + { path: "server/handler.ts", content: SERVER_ONLY_CONTENT }, + { path: "veryfront.json", content: projectConfigText() }, + ]; + await writePushReceipt(projectDir, { + controlPlane: CONTROL_PLANE, + projectId: PROJECT_ID, + projectSlug: PROJECT_SLUG, + branch: "main", + commitSha, + sourceDigest: await computeSourceDigest(files), + clean: true, + }); + return { projectDir, files }; + } + + it("still deploys a page-less project to an environment with no hosted address", async () => { + await withDeployEnv(async () => { + const { projectDir, files } = await createPushedPagelessProject(); + const controlPlane = new InMemoryDeployControlPlane(); + controlPlane.environmentDomains = []; + controlPlane.releaseFiles = files; + controlPlane.manifestResponses = [readyManifest({})]; + let probes = 0; + try { + const outcome = await withFetchStub( + (input) => { + if (isHostedEnvironmentRequest(input)) { + probes++; + return hostedNotFound(); + } + return new Response("ready"); + }, + () => + createDeployment(controlPlane).execute({ + projectDir, + environment: "development", + mode: "apply", + source: { kind: "already-pushed" }, + }), + ); + + assertEquals( + outcome.kind, + "deployed", + "a deploy that never probes a hosted address does not depend on the environment name", + ); + assertEquals(probes, 0, "a page-less deploy sends no readiness probe"); + } finally { + await Deno.remove(projectDir, { recursive: true }); + } + }); + }); + + it("still deploys an unroutable environment name that has a custom domain", async () => { + await withDeployEnv(async () => { + const { projectDir } = await createPushedProject(); + const controlPlane = new InMemoryDeployControlPlane(); + controlPlane.environmentDomains = ["https://dev.example.com"]; + try { + const outcome = await withFetchStub( + () => new Response("ready"), + () => + createDeployment(controlPlane).execute({ + projectDir, + environment: "development", + mode: "apply", + source: { kind: "already-pushed" }, + }), + ); + + assertEquals( + outcome.kind, + "deployed", + "a custom domain makes the environment name irrelevant to routing", + ); + } finally { + await Deno.remove(projectDir, { recursive: true }); + } + }); + }); +}); diff --git a/cli/shared/deployment/deploy-project.ts b/cli/shared/deployment/deploy-project.ts index 16f4690d85..cc6fdda9a3 100644 --- a/cli/shared/deployment/deploy-project.ts +++ b/cli/shared/deployment/deploy-project.ts @@ -7,7 +7,11 @@ import { import { createFileSystem, isNotFoundError, runtime } from "veryfront/platform"; import { join, relative, resolve } from "veryfront/platform/path"; import { isWithinDirectory, normalizePath } from "veryfront/utils"; -import { parseProjectDomain } from "veryfront/server"; +import { + HOSTED_ENVIRONMENT_NAMES, + isHostedEnvironmentName, + parseProjectDomain, +} from "veryfront/server"; import { describeReadyReleaseAssetManifestRejection, isSafeBoundedText, @@ -21,6 +25,7 @@ import { CONFIG_NOT_DEPLOYABLE, DEPLOYMENT_ERROR, ENVIRONMENT_NOT_FOUND, + ENVIRONMENT_NOT_ROUTABLE, RELEASE_MISSING_VERSION, SOURCE_DIGEST_MISMATCH, VeryfrontError, @@ -836,14 +841,51 @@ export async function waitForReleaseAssetManifest( } } -function buildEnvironmentUrl(projectSlug: string, environment: DeployEnvironment): string { +/** The environment's own domain, or null when it has none configured. */ +function configuredEnvironmentDomain(environment: DeployEnvironment): string | null { const domain = environment.domains?.[0]; - if (domain) { - return domain.startsWith("http://") || domain.startsWith("https://") - ? domain - : `https://${domain}`; - } - return `https://${projectSlug}.${environment.name}.veryfront.com`; + if (!domain) return null; + return domain.startsWith("http://") || domain.startsWith("https://") + ? domain + : `https://${domain}`; +} + +/** + * Rejects an environment whose name has no address the deploy could ever reach. + * + * The hosted URL is synthesised as `{slug}.{environment}.veryfront.com`, which + * only resolves for the labels in `HOSTED_ENVIRONMENT_NAMES`. Any other label — + * `development`, `qa`, anything an operator invented in Studio — has no wildcard + * certificate and no routing rule, so the readiness probe cannot succeed: it + * fails the TLS handshake, or the proxy answers `404 No project configured for + * domain`. Both are retried as transient, so without this check the deploy + * pushes source, builds assets, commits a deployment, and only then spends the + * full readiness window before failing with a message about the deployment. + * + * A configured custom domain removes the constraint entirely — routing then has + * nothing to do with the environment's name — so this only guards the fallback. + * + * Call this only for a deploy that will actually ask for that address. A project + * with no static page route probes nothing, so its name never has to resolve. + */ +function assertEnvironmentIsReachable(environment: DeployEnvironment): void { + if (configuredEnvironmentDomain(environment) !== null) return; + if (isHostedEnvironmentName(environment.name)) return; + + throw ENVIRONMENT_NOT_ROUTABLE.create({ + detail: `Environment "${environment.name}" has no Veryfront-hosted address. ` + + `Veryfront serves ${ + HOSTED_ENVIRONMENT_NAMES.join(", ") + } at https://..veryfront.com; ` + + `no other environment name resolves. Deploy to one of those, or attach a custom domain ` + + `to "${environment.name}" in Studio under Environments and deploy again.`, + context: { environmentName: environment.name, hostedEnvironments: HOSTED_ENVIRONMENT_NAMES }, + }); +} + +function buildEnvironmentUrl(projectSlug: string, environment: DeployEnvironment): string { + return configuredEnvironmentDomain(environment) ?? + buildCanonicalEnvironmentUrl(projectSlug, environment.name); } function buildCanonicalEnvironmentUrl(projectSlug: string, environmentName: string): string { @@ -965,12 +1007,18 @@ function buildEnvironmentReadinessProbes( const targetUrl = buildEnvironmentProbeUrl(target.url, route); if (target.protected && !isMatchingVeryfrontHostedUrl(new URL(targetUrl), target)) { + const challengeProbe = { + url: targetUrl, + authenticate: false, + acceptAuthenticationChallenge: true, + }; + // The canonical companion probe only exists where the platform routes it. + // An environment on a custom domain may be named anything, and synthesising + // an address for a name hosting cannot serve would poll an unreachable host + // to the deadline after the custom domain has already answered. + if (!isHostedEnvironmentName(target.environmentName)) return [challengeProbe]; return [ - { - url: targetUrl, - authenticate: false, - acceptAuthenticationChallenge: true, - }, + challengeProbe, { url: buildEnvironmentProbeUrl( buildCanonicalEnvironmentUrl(target.projectSlug, target.environmentName), @@ -1336,6 +1384,12 @@ export function createDeployProject(options: { controlPlane = createControlPlane(config); } + // Read from the project directory, before the environment is resolved, + // because whether this deploy needs a hosted address at all decides + // whether an unroutable environment name is fatal to it. + const expectedPageRoutes = await collectProjectPageRoutes(request.projectDir); + const readinessRoute = expectedPageRoutes.find((route) => !route.includes("[")) ?? null; + const environment = await step(observer, "resolve-target", async () => { if (!project) project = await controlPlane.getProject(projectApiReference(config)); const resolvedEnvironment = await controlPlane.getEnvironment( @@ -1348,6 +1402,14 @@ export function createDeployProject(options: { }); } assertProjectOwnership("Environment", resolvedEnvironment, project.id); + // Only a deploy that will ask the platform for a page address depends on + // the name resolving. A project with no static page route sends no + // readiness probe at all, so its deploy never touches the synthesised + // host and must not be refused for a name it never resolves. When the + // address is needed this still runs before any release or deployment + // exists, so an unreachable target costs one API call rather than a full + // deploy and a readiness window. + if (readinessRoute !== null) assertEnvironmentIsReachable(resolvedEnvironment); return resolvedEnvironment; }); @@ -1383,17 +1445,14 @@ export function createDeployProject(options: { }; } - const { source, expectedPageRoutes } = await step(observer, "verify-source", async () => { - const source = await resolvePushedSource({ + const source = await step(observer, "verify-source", async () => + resolvePushedSource({ projectDir: request.projectDir, controlPlane: config.apiUrl, projectId: project!.id, projectSlug: project!.slug, branch, - }); - const expectedPageRoutes = await collectProjectPageRoutes(request.projectDir); - return { source, expectedPageRoutes }; - }); + })); const release = await step(observer, "create-release", async () => { const created = await controlPlane.createRelease(project!.id, { @@ -1454,7 +1513,6 @@ export function createDeployProject(options: { }, { verifiedRelease }), ); - const readinessRoute = expectedPageRoutes.find((route) => !route.includes("[")) ?? null; const environmentUrl = buildReadyEnvironmentUrl( buildEnvironmentUrl(verification.projectSlug, environment), readinessRoute, diff --git a/cli/test-utils/deploy-test-support.ts b/cli/test-utils/deploy-test-support.ts index 5ea8ad7c8c..2e445850f5 100644 --- a/cli/test-utils/deploy-test-support.ts +++ b/cli/test-utils/deploy-test-support.ts @@ -201,6 +201,11 @@ export class InMemoryDeployControlPlane implements DeployControlPlane { readonly projectLookups: string[] = []; getProjectError: unknown; environment: DeployEnvironment | null | undefined; + /** + * Custom domains the environment reports. Set to `[]` to make the CLI fall + * back to synthesising the `{slug}.{environment}.veryfront.com` hosted URL. + */ + environmentDomains: string[] = ["https://my-project.production.veryfront.com"]; /** Whether the default environment sits behind the platform access gate. */ environmentProtected = false; releaseVersion: string | null = "2026.07.30-1"; @@ -237,7 +242,7 @@ export class InMemoryDeployControlPlane implements DeployControlPlane { release: { id: this.release.id, name: this.release.name }, } : null, - domains: ["https://my-project.production.veryfront.com"], + domains: this.environmentDomains, }; } diff --git a/docs/api-reference/veryfront/errors.md b/docs/api-reference/veryfront/errors.md index 1241f0f288..55da77eb72 100644 --- a/docs/api-reference/veryfront/errors.md +++ b/docs/api-reference/veryfront/errors.md @@ -48,7 +48,7 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `API_ROUTE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L43) | | `ASSET_OPTIMIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L35) | | `AUTHENTICATION_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L11) | -| `BRANCH_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L91) | +| `BRANCH_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L100) | | `BUILD_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/build-errors.ts#L4) | | `BUILD_FAILED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L3) | | `BUNDLE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L11) | @@ -63,7 +63,7 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `COMPONENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L19) | | `CONFIG_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/config-errors.ts#L4) | | `CONFIG_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L12) | -| `CONFIG_NOT_DEPLOYABLE` | The project's configuration file uses a construct Veryfront Cloud's configuration evaluator can never accept, so the release would answer 500 to every request. Raised before a release is created; the detail names the file, the change that makes the project deployable, and the line when the evaluator located the construct it refused. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L106) | +| `CONFIG_NOT_DEPLOYABLE` | The project's configuration file uses a construct Veryfront Cloud's configuration evaluator can never accept, so the release would answer 500 to every request. Raised before a release is created; the detail names the file, the change that makes the project deployable, and the line when the evaluator located the construct it refused. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L115) | | `CONFIG_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L3) | | `CONFIG_PARSE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L20) | | `CONFIG_TYPE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L37) | @@ -74,13 +74,14 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `DEPENDENCY_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L35) | | `DEPLOYMENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L3) | | `DEPLOYMENT_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/deployment-errors.ts#L4) | -| `DEPLOYMENT_VERIFICATION_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L59) | +| `DEPLOYMENT_VERIFICATION_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L68) | | `DEV_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/dev-errors.ts#L4) | | `DEV_SERVER_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L11) | | `DURABLE_RUN_EVENT_PERSISTENCE_FAILED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L59) | | `DYNAMIC_ROUTE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L27) | | `ENV_VAR_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L19) | | `ENVIRONMENT_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L35) | +| `ENVIRONMENT_NOT_ROUTABLE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L43) | | `ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/index.ts#L31) | | `ERROR_OVERLAY_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L27) | | `ERROR_REGISTRY` | Central registry mapping every error slug to its definition. Assembled from the per-category registry fragments. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry.ts#L39) | @@ -116,16 +117,16 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `PERMISSION_DENIED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L19) | | `PLATFORM_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L11) | | `PORT_IN_USE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L3) | -| `PREVIEW_HOSTNAME_TOO_LONG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L83) | +| `PREVIEW_HOSTNAME_TOO_LONG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L92) | | `PROBLEM_JSON_CONTENT_TYPE` | Content-Type header for RFC 9457 responses | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/http-error.ts#L32) | | `PRODUCTION_BUILD_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L27) | | `PROJECT_EXECUTION_UNAVAILABLE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L52) | | `PROJECT_SOURCE_EMPTY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L94) | -| `PUSH_RECEIPT_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L67) | +| `PUSH_RECEIPT_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L76) | | `RAG_STORE_CORRUPT` | Persisted RAG index is malformed or failed structural validation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L138) | | `RAG_STORE_UNAVAILABLE` | A persisted RAG index operation could not be completed safely. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L147) | -| `RELEASE_BUILD_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L51) | -| `RELEASE_MISSING_VERSION` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L43) | +| `RELEASE_BUILD_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L60) | +| `RELEASE_MISSING_VERSION` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L52) | | `RELEASE_NOT_FOUND` | Production domain resolved but no active release found | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L120) | | `RENDER_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L11) | | `REQUEST_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L36) | @@ -144,7 +145,7 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `SERVER_ONLY_IN_CLIENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L11) | | `SERVER_START_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L12) | | `SERVICE_OVERLOADED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L44) | -| `SOURCE_DIGEST_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L75) | +| `SOURCE_DIGEST_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L84) | | `SOURCE_MAP_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/dev.ts#L35) | | `SOURCEMAP_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L51) | | `SSG_GENERATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/build.ts#L43) | diff --git a/docs/api-reference/veryfront/index.client.md b/docs/api-reference/veryfront/index.client.md index fbe04c74af..ff40ef3136 100644 --- a/docs/api-reference/veryfront/index.client.md +++ b/docs/api-reference/veryfront/index.client.md @@ -73,10 +73,10 @@ export function GET() { | `MDXFrontmatter` | Parsed frontmatter values from an MDX page. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/types/index.ts#L90) | | `PageContext` | Runtime page context passed to page components. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/types/index.ts#L107) | | `PageWithData` | Page with data fetching capabilities | [source](https://github.com/veryfront/veryfront-code/blob/main/src/data/types.ts#L16) | -| `StartServerOptions` | Server options. Defaults to development mode with HMR. Set `mode: "production"` for a production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L138) | +| `StartServerOptions` | Server options. Defaults to development mode with HMR. Set `mode: "production"` for a production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L143) | | `StaticPathsResult` | Return type for `getStaticPaths()`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/data/schemas/data.schema.ts#L61) | | `ValidatedHandlerConfig` | Configuration for `createValidatedHandler()`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/handler.ts#L11) | | `ValidatedHandlerFunction` | Handler signature that receives validated request data. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/handler.ts#L18) | | `VeryfrontConfig` | Project configuration. The underlying runtime schema stores `extensions` as `unknown[]`; this tightened alias surfaces the expected `ExtensionConfigEntry[]` shape to TypeScript consumers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/config/schemas/index.ts#L24) | -| `VeryfrontHandler` | Web API request handler with WebSocket upgrade and HMR helpers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L153) | -| `VeryfrontServer` | Running server instance with lifecycle controls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L141) | +| `VeryfrontHandler` | Web API request handler with WebSocket upgrade and HMR helpers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L158) | +| `VeryfrontServer` | Running server instance with lifecycle controls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L146) | diff --git a/docs/api-reference/veryfront/index.md b/docs/api-reference/veryfront/index.md index 808968b1e5..785ba31ebc 100644 --- a/docs/api-reference/veryfront/index.md +++ b/docs/api-reference/veryfront/index.md @@ -61,7 +61,7 @@ export function getServerData(ctx: DataContext) { | `apiNotFound` | Create a 404 Not Found response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/http/responses.ts#L119) | | `apiRedirect` | Create an HTTP redirect response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/http/responses.ts#L97) | | `badRequest` | Create a 400 Bad Request response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/http/responses.ts#L124) | -| `createHandler` | Create a Veryfront request handler for development or production. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L233) | +| `createHandler` | Create a Veryfront request handler for development or production. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L238) | | `createValidatedHandler` | Create a validated API handler with bounded body/query validation. Bodies without a schema are preflighted through a clone, leaving the original request body available to the handler after its size is verified. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/handler.ts#L163) | | `createValidationError` | Create an input validation error. Convenience wrapper around INPUT_VALIDATION_FAILED.create(). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/errors.ts#L12) | | `defineConfig` | Define a Veryfront project configuration object. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/config/define-config-core.ts#L4) | @@ -77,7 +77,7 @@ export function getServerData(ctx: DataContext) { | `redirect` | Redirect the request from a data loader. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/data/helpers.ts#L34) | | `sanitizeData` | Sanitize JSON-like data by HTML-encoding string values and removing keys that can mutate an object's prototype chain. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/sanitizers.ts#L8) | | `serverError` | Create a 500 Internal Server Error response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/http/responses.ts#L139) | -| `startServer` | Start a Veryfront server in development or production mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L520) | +| `startServer` | Start a Veryfront server in development or production mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L525) | | `toNodeHandler` | Convert a Web API request handler into a Node.js HTTP listener. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/node-handler.ts#L4) | | `unauthorized` | Create a 401 Unauthorized response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/http/responses.ts#L129) | @@ -94,10 +94,10 @@ export function getServerData(ctx: DataContext) { | `MDXFrontmatter` | Parsed frontmatter values from an MDX page. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/types/index.ts#L90) | | `PageContext` | Runtime page context passed to page components. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/types/index.ts#L107) | | `PageWithData` | Page with data fetching capabilities | [source](https://github.com/veryfront/veryfront-code/blob/main/src/data/types.ts#L16) | -| `StartServerOptions` | Server options. Defaults to development mode with HMR. Set `mode: "production"` for a production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L138) | +| `StartServerOptions` | Server options. Defaults to development mode with HMR. Set `mode: "production"` for a production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L143) | | `StaticPathsResult` | Return type for `getStaticPaths()`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/data/schemas/data.schema.ts#L61) | | `ValidatedHandlerConfig` | Configuration for `createValidatedHandler()`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/handler.ts#L11) | | `ValidatedHandlerFunction` | Handler signature that receives validated request data. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/handler.ts#L18) | | `VeryfrontConfig` | Project configuration. The underlying runtime schema stores `extensions` as `unknown[]`; this tightened alias surfaces the expected `ExtensionConfigEntry[]` shape to TypeScript consumers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/config/schemas/index.ts#L24) | -| `VeryfrontHandler` | Web API request handler with WebSocket upgrade and HMR helpers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L153) | -| `VeryfrontServer` | Running server instance with lifecycle controls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L141) | +| `VeryfrontHandler` | Web API request handler with WebSocket upgrade and HMR helpers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L158) | +| `VeryfrontServer` | Running server instance with lifecycle controls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L146) | diff --git a/docs/api-reference/veryfront/server.md b/docs/api-reference/veryfront/server.md index a9348c5b80..c3f6102c5a 100644 --- a/docs/api-reference/veryfront/server.md +++ b/docs/api-reference/veryfront/server.md @@ -11,9 +11,9 @@ import { createHandler, createVeryfrontServer, gracefullyShutdownProductionServer, + isHostedEnvironmentName, parseProjectDomain, startDevServer, - startNodeVeryfrontServer, } from "veryfront/server"; ``` @@ -38,24 +38,26 @@ await server.fetch(new Request("https://example.com/health")); ### Components -| Name | Description | Source | -| ---------------- | ----------------------- | -------------------------------------------------------------------------------------------------- | -| `ReloadNotifier` | Render reload notifier. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/reload-notifier.ts#L146) | +| Name | Description | Source | +| -------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `HOSTED_ENVIRONMENT_NAMES` | Environment labels that `{slug}.{environment}.veryfront.com` actually routes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L44) | +| `ReloadNotifier` | Render reload notifier. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/reload-notifier.ts#L146) | ### Functions -| Name | Description | Source | -| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| `createHandler` | Create a Veryfront request handler for development or production. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L233) | -| `createVeryfrontServer` | Create veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L157) | -| `gracefullyShutdownProductionServer` | Enter lame-duck mode, mark readiness false, drain tracked requests and SSE response bodies, and stop a production server process. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/graceful-shutdown.ts#L218) | -| `parseProjectDomain` | Extract project slug and branch from domain/host header | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L73) | -| `startDevServer` | Starts dev server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/index.ts#L15) | -| `startNodeVeryfrontServer` | Starts node veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L575) | -| `startProductionServer` | Starts production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L181) | -| `startServer` | Start a Veryfront server in development or production mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L520) | -| `startVeryfrontServer` | Starts veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L555) | -| `toNodeHandler` | Convert a Web API request handler into a Node.js HTTP listener. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/node-handler.ts#L4) | +| Name | Description | Source | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `createHandler` | Create a Veryfront request handler for development or production. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L238) | +| `createVeryfrontServer` | Create veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L157) | +| `gracefullyShutdownProductionServer` | Enter lame-duck mode, mark readiness false, drain tracked requests and SSE response bodies, and stop a production server process. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/graceful-shutdown.ts#L218) | +| `isHostedEnvironmentName` | Whether `{slug}.{name}.veryfront.com` is a host the platform can route. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L58) | +| `parseProjectDomain` | Extract project slug and branch from domain/host header | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L122) | +| `startDevServer` | Starts dev server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/index.ts#L15) | +| `startNodeVeryfrontServer` | Starts node veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L575) | +| `startProductionServer` | Starts production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L181) | +| `startServer` | Start a Veryfront server in development or production mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L525) | +| `startVeryfrontServer` | Starts veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L555) | +| `toNodeHandler` | Convert a Web API request handler into a Node.js HTTP listener. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/node-handler.ts#L4) | ### Classes @@ -66,33 +68,34 @@ await server.fetch(new Request("https://example.com/health")); ### Types -| Name | Description | Source | -| -------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `BuildOptions` | Build System Type Definitions Consolidated from cli/commands/build/types.ts and server/build-types.ts | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/build-types.ts#L6) | -| `BuildStats` | Public API contract for build stats. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/build-types.ts#L28) | -| `CreateVeryfrontServerOptions` | Options accepted by create veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L32) | -| `DevServerOptions` | Options accepted by dev server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/types.ts#L2) | -| `DiscoveryOptions` | Configuration for AI primitives discovery during server startup | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L119) | -| `FileWatcherMetrics` | Public API contract for file watcher metrics. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/types.ts#L33) | -| `GracefulProductionShutdownOptions` | Inputs required to drain and stop a production server process. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/graceful-shutdown.ts#L25) | -| `NodeVeryfrontServiceServer` | Public API contract for node veryfront service server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L83) | -| `RouteDirectory` | Public API contract for route directory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/types.ts#L27) | -| `ServerHandle` | Public API contract for server handle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L156) | -| `StartDevModeOptions` | Options accepted by start dev mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L113) | -| `StartNodeVeryfrontServerOptions` | Options accepted by start node veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L47) | -| `StartProductionModeOptions` | Options accepted by start production mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L122) | -| `StartProductionServerOptions` | Options accepted by start production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L162) | -| `StartServerOptions` | Server options. Defaults to development mode with HMR. Set `mode: "production"` for a production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L138) | -| `StartVeryfrontServerOptions` | Options accepted by start veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L59) | -| `VeryfrontHandler` | Web API request handler with WebSocket upgrade and HMR helpers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L153) | -| `VeryfrontServer` | Running server instance with lifecycle controls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L141) | -| `VeryfrontServiceServer` | Public API contract for veryfront service server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L74) | -| `VeryfrontServiceServerFetch` | Public API contract for veryfront service server fetch. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L9) | -| `VeryfrontServiceServerLogger` | Public API contract for veryfront service server logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L24) | -| `VeryfrontServiceServerModule` | Public API contract for veryfront service server module. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L14) | -| `VeryfrontServiceServerModuleResponse` | Response payload for veryfront service server module. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L11) | -| `VeryfrontServiceServerRuntime` | Public API contract for veryfront service server runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L40) | -| `VeryfrontServiceServerRuntimeKind` | Public API contract for veryfront service server runtime kind. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L71) | +| Name | Description | Source | +| -------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `BuildOptions` | Build System Type Definitions Consolidated from cli/commands/build/types.ts and server/build-types.ts | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/build-types.ts#L6) | +| `BuildStats` | Public API contract for build stats. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/build-types.ts#L28) | +| `CreateVeryfrontServerOptions` | Options accepted by create veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L32) | +| `DevServerOptions` | Options accepted by dev server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/types.ts#L2) | +| `DiscoveryOptions` | Configuration for AI primitives discovery during server startup | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L119) | +| `FileWatcherMetrics` | Public API contract for file watcher metrics. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/types.ts#L33) | +| `GracefulProductionShutdownOptions` | Inputs required to drain and stop a production server process. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/graceful-shutdown.ts#L25) | +| `HostedEnvironmentName` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L46) | +| `NodeVeryfrontServiceServer` | Public API contract for node veryfront service server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L83) | +| `RouteDirectory` | Public API contract for route directory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/types.ts#L27) | +| `ServerHandle` | Public API contract for server handle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L156) | +| `StartDevModeOptions` | Options accepted by start dev mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L118) | +| `StartNodeVeryfrontServerOptions` | Options accepted by start node veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L47) | +| `StartProductionModeOptions` | Options accepted by start production mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L127) | +| `StartProductionServerOptions` | Options accepted by start production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L162) | +| `StartServerOptions` | Server options. Defaults to development mode with HMR. Set `mode: "production"` for a production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L143) | +| `StartVeryfrontServerOptions` | Options accepted by start veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L59) | +| `VeryfrontHandler` | Web API request handler with WebSocket upgrade and HMR helpers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L158) | +| `VeryfrontServer` | Running server instance with lifecycle controls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L146) | +| `VeryfrontServiceServer` | Public API contract for veryfront service server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L74) | +| `VeryfrontServiceServerFetch` | Public API contract for veryfront service server fetch. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L9) | +| `VeryfrontServiceServerLogger` | Public API contract for veryfront service server logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L24) | +| `VeryfrontServiceServerModule` | Public API contract for veryfront service server module. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L14) | +| `VeryfrontServiceServerModuleResponse` | Response payload for veryfront service server module. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L11) | +| `VeryfrontServiceServerRuntime` | Public API contract for veryfront service server runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L40) | +| `VeryfrontServiceServerRuntimeKind` | Public API contract for veryfront service server runtime kind. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L71) | ### Constants diff --git a/docs/guides/errors.md b/docs/guides/errors.md index e28dbb305f..d39c544209 100644 --- a/docs/guides/errors.md +++ b/docs/guides/errors.md @@ -612,6 +612,13 @@ Deployment environment not found. - **HTTP status:** 404 - **What to do:** Check environment names with: veryfront config +### environment-not-routable + +Environment name has no Veryfront-hosted address. + +- **HTTP status:** 400 +- **What to do:** Deploy to preview, staging, or production, or attach a custom domain to this environment in Studio + ### release-missing-version Release has no version. diff --git a/src/errors/error-registry.test.ts b/src/errors/error-registry.test.ts index eb701dccbf..8f2a2f63fc 100644 --- a/src/errors/error-registry.test.ts +++ b/src/errors/error-registry.test.ts @@ -29,9 +29,9 @@ describe("error-registry", () => { assertEquals(slugs.length, uniqueSlugs.size, "Duplicate slugs detected"); }); - it("should have 108 registered errors", () => { + it("should have 109 registered errors", () => { const slugs = getAllSlugs(); - assertEquals(slugs.length, 108); + assertEquals(slugs.length, 109); }); }); @@ -325,7 +325,7 @@ describe("error-registry", () => { SERVER: 18, BOUNDARY: 7, DEV: 5, - DEPLOY: 13, + DEPLOY: 14, AGENT: 8, GENERAL: 13, }; diff --git a/src/errors/error-registry/deploy.ts b/src/errors/error-registry/deploy.ts index c2dd9a852e..fc5cb61ca8 100644 --- a/src/errors/error-registry/deploy.ts +++ b/src/errors/error-registry/deploy.ts @@ -40,6 +40,15 @@ export const ENVIRONMENT_NOT_FOUND = defineError({ suggestion: "Check environment names with: veryfront config", }); +export const ENVIRONMENT_NOT_ROUTABLE = defineError({ + slug: "environment-not-routable", + category: "DEPLOY", + status: 400, + title: "Environment name has no Veryfront-hosted address", + suggestion: + "Deploy to preview, staging, or production, or attach a custom domain to this environment in Studio", +}); + export const RELEASE_MISSING_VERSION = defineError({ slug: "release-missing-version", category: "DEPLOY", @@ -121,6 +130,7 @@ export const DEPLOY_REGISTRY = { "env-var-missing": ENV_VAR_MISSING, "production-build-required": PRODUCTION_BUILD_REQUIRED, "environment-not-found": ENVIRONMENT_NOT_FOUND, + "environment-not-routable": ENVIRONMENT_NOT_ROUTABLE, "release-missing-version": RELEASE_MISSING_VERSION, "release-build-timeout": RELEASE_BUILD_TIMEOUT, "deployment-verification-timeout": DEPLOYMENT_VERIFICATION_TIMEOUT, diff --git a/src/errors/index.ts b/src/errors/index.ts index 9a6b7944dc..7a59c87305 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -72,6 +72,7 @@ export { DYNAMIC_ROUTE_ERROR, ENV_VAR_MISSING, ENVIRONMENT_NOT_FOUND, + ENVIRONMENT_NOT_ROUTABLE, ERROR_OVERLAY_ERROR, // Registry ERROR_REGISTRY, diff --git a/src/server/index.ts b/src/server/index.ts index dde7c65f94..8e507efcd0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -88,7 +88,12 @@ export { ReloadNotifier }; export { RouteDiscovery } from "./dev-server/route-discovery.ts"; export type { BuildOptions, BuildStats } from "./build-types.ts"; export { defaultDistributedCacheInitializers } from "./distributed-cache-initializers.ts"; -export { parseProjectDomain } from "./utils/domain-parser.ts"; +export { + HOSTED_ENVIRONMENT_NAMES, + type HostedEnvironmentName, + isHostedEnvironmentName, + parseProjectDomain, +} from "./utils/domain-parser.ts"; /** Shared options for both development and production server modes. */ interface BaseServerOptions { diff --git a/src/server/utils/domain-parser.test.ts b/src/server/utils/domain-parser.test.ts index 4e13c9c790..136923d59b 100644 --- a/src/server/utils/domain-parser.test.ts +++ b/src/server/utils/domain-parser.test.ts @@ -3,6 +3,8 @@ import { assertEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { getEffectiveProjectSlug, + HOSTED_ENVIRONMENT_NAMES, + isHostedEnvironmentName, isLocalDevHost, isVeryfrontDomain, parseProjectDomain, @@ -419,4 +421,49 @@ describe("domain-parser", () => { assertEquals(parseProjectDomain("example.com.prod.lvh.me").allowIframeEmbed, false); }); }); + + describe("HOSTED_ENVIRONMENT_NAMES", () => { + it("names exactly the labels parseProjectDomain routes to a hosted project", () => { + for (const name of HOSTED_ENVIRONMENT_NAMES) { + const parsed = parseProjectDomain(`myproject.${name}.veryfront.com`); + assertEquals(parsed.slug, "myproject", `${name} must resolve a project slug`); + assertEquals(parsed.environment, name); + assertEquals(parsed.isVeryfrontDomain, true); + } + }); + + it("excludes labels the hosted platform cannot route", () => { + // `development` is the one that matters: it is a valid local environment + // and reads like a natural deploy target, but no hosted rule produces it. + for (const name of ["development", "dev", "qa", "test", "sandbox"]) { + assertEquals(isHostedEnvironmentName(name), false, `${name} must not be hosted-routable`); + const parsed = parseProjectDomain(`myproject.${name}.veryfront.com`); + assertEquals(parsed.slug, null, `${name} must not resolve a project slug`); + assertEquals(parsed.environment, null); + assertEquals(parsed.isVeryfrontDomain, false); + } + }); + + it("matches environment names case-insensitively", () => { + assertEquals(isHostedEnvironmentName("Production"), true); + assertEquals(isHostedEnvironmentName("STAGING"), true); + assertEquals(isHostedEnvironmentName("Development"), false); + }); + + it("answers about the label, not about the caller's string", () => { + // The check folds case, so a true answer says nothing about the spelling + // the caller holds. It must therefore stay a plain boolean: as a + // `name is HostedEnvironmentName` predicate it typed `"Production"` as a + // lowercase-only literal, and an exhaustive switch or keyed lookup built + // on that narrowing misses at runtime — exactly as this assertion shows. + const name = "Production"; + const routable: boolean = isHostedEnvironmentName(name); + assertEquals(routable, true); + assertEquals( + (HOSTED_ENVIRONMENT_NAMES as readonly string[]).includes(name), + false, + "the caller's spelling is not one of the hosted labels", + ); + }); + }); }); diff --git a/src/server/utils/domain-parser.ts b/src/server/utils/domain-parser.ts index 03a2714049..d544b7b62e 100644 --- a/src/server/utils/domain-parser.ts +++ b/src/server/utils/domain-parser.ts @@ -23,6 +23,55 @@ const PROD_DOMAINS = "veryfront\\.com|veryfront\\.org"; // Domains that allow iframe embedding but aren't veryfront domains const IFRAME_EMBED_DOMAINS = /^(localhost|.*\.xip\.io|.*\.zip\.io)$/i; +/** + * Environment labels that `{slug}.{environment}.veryfront.com` actually routes. + * + * This is not a naming preference — it is what the hosted platform can serve. + * Each label needs a wildcard TLS certificate (`*.{label}.veryfront.com`) and a + * rule below that resolves the host to a project. A label with neither is not a + * slow environment, it is an unreachable one: TLS fails outright, or the proxy + * falls through to the custom-domain lookup and answers + * `404 {"error":"No project configured for domain: ..."}`. + * + * `development` is deliberately absent. It is a valid `ParsedDomain.environment` + * for *local* roots (`lvh.me`, `localhost`, `veryfront.dev`), where it means + * "running on this machine". No hosted rule produces it, so a hosted + * `{slug}.development.veryfront.com` resolves to no project. + * + * Keep in sync with the hosted rules in `parseProjectDomain`; the lock test in + * `domain-parser.test.ts` fails if they drift apart. + */ +export const HOSTED_ENVIRONMENT_NAMES = ["preview", "staging", "production"] as const; + +export type HostedEnvironmentName = typeof HOSTED_ENVIRONMENT_NAMES[number]; + +/** + * Whether `{slug}.{name}.veryfront.com` is a host the platform can route. + * + * Deliberately not a `name is HostedEnvironmentName` predicate. Host labels are + * case-insensitive, so the comparison folds case — and a predicate would then + * narrow the *unfolded* `"Production"` to a type whose members are all + * lowercase, letting a caller feed it to an exhaustive `switch` or a keyed + * lookup that misses at runtime. A caller that needs a value of that type must + * take the folded label this is built on rather than its own string. + */ +export function isHostedEnvironmentName(name: string): boolean { + return toHostedEnvironmentName(name) !== null; +} + +/** + * The routable label `name` denotes, case-folded, or null when the platform + * cannot route it. Returns the constant rather than the caller's string, so the + * value always matches its `HostedEnvironmentName` type. + */ +function toHostedEnvironmentName(name: string): HostedEnvironmentName | null { + const folded = name.toLowerCase(); + return HOSTED_ENVIRONMENT_NAMES.find((hosted) => hosted === folded) ?? null; +} + +/** Alternation source for the hosted environment labels, e.g. `preview|staging|production`. */ +const HOSTED_ENVIRONMENTS = HOSTED_ENVIRONMENT_NAMES.join("|"); + /** All recognized veryfront domains */ const ALL_DOMAINS = `${LOCAL_DEV_DOMAINS}|${PROD_DOMAINS}`; @@ -126,7 +175,7 @@ export function parseProjectDomain(host: string): ParsedDomain { // Local environment root domains (no slug): preview|staging|production.{lvh.me|veryfront.dev} const localEnvRootMatch = matchDomain( domain, - `^(preview|staging|production)\\.(${LOCAL_DEV_DOMAINS})$`, + `^(${HOSTED_ENVIRONMENTS})\\.(${LOCAL_DEV_DOMAINS})$`, ); if (localEnvRootMatch?.[1]) { const env = localEnvRootMatch[1] as Environment; @@ -171,7 +220,7 @@ export function parseProjectDomain(host: string): ParsedDomain { } // Environment root domains (no slug): preview|staging|production.veryfront.{com|org} - const envRootMatch = matchDomain(domain, `^(preview|staging|production)\\.(${PROD_DOMAINS})$`); + const envRootMatch = matchDomain(domain, `^(${HOSTED_ENVIRONMENTS})\\.(${PROD_DOMAINS})$`); if (envRootMatch?.[1]) { const env = envRootMatch[1] as Environment; return createParsedDomain(null, null, env, true, env === "preview");