Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/api-reference/veryfront/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ await server.fetch(new Request("https://example.com/health"));
| `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#L125) |
| `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) |
| `startProductionServer` | Starts production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L182) |
| `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) |
Expand All @@ -63,7 +63,7 @@ await server.fetch(new Request("https://example.com/health"));

| Name | Description | Source |
| ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------ |
| `DevServer` | Implement dev server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/server.ts#L70) |
| `DevServer` | Implement dev server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/server.ts#L71) |
| `RouteDiscovery` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/route-discovery.ts#L33) |

### Types
Expand All @@ -74,17 +74,17 @@ await server.fetch(new Request("https://example.com/health"));
| `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) |
| `DiscoveryOptions` | Configuration for AI primitives discovery during server startup | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L120) |
| `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#L49) |
| `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) |
| `ServerHandle` | Public API contract for server handle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L157) |
| `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) |
| `StartProductionServerOptions` | Options accepted by start production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L163) |
| `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) |
Expand Down
8 changes: 8 additions & 0 deletions src/platform/adapters/runtime/shared/request-peer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ export function inheritRequestPeerProvenance<T extends Request>(
return target;
}

/** @internal Run an interceptor without discarding transport peer authority. */
export async function runRequestInterceptor(
request: Request,
interceptor: (request: Request) => Request | Promise<Request>,
): Promise<Request> {
return inheritRequestPeerProvenance(request, await interceptor(request));
Comment thread
kojiwakayama marked this conversation as resolved.
Outdated
}

/** @internal True for IPv4 127/8, IPv6 ::1, or mapped IPv4 127/8. */
export function isLoopbackAddress(hostname: string): boolean {
const ipv4 = parseCanonicalIpv4(hostname);
Expand Down
3 changes: 2 additions & 1 deletion src/server/dev-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getHostEnv, setEnv } from "#veryfront/platform/compat/process.ts";
import { isTruthyEnvValue } from "#veryfront/utils/constants/env.ts";
import { initializeDistributedCaches } from "#veryfront/cache/distributed-cache-init.ts";
import { defaultDistributedCacheInitializers } from "#veryfront/server/distributed-cache-initializers.ts";
import { runRequestInterceptor } from "#veryfront/platform/adapters/runtime/shared/request-peer.ts";
import { isDiskCacheConfigured } from "#veryfront/cache/backend.ts";
import { clearTranspileCache, discoverAll } from "#veryfront/discovery";
import type { DiscoveryConfig } from "#veryfront/discovery";
Expand Down Expand Up @@ -276,7 +277,7 @@ export class DevServer {
const isWebSocketUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
if (isWebSocketUpgrade) return baseHandler(req);

const interceptedReq = await interceptor(req);
const interceptedReq = await runRequestInterceptor(req, interceptor);
return baseHandler(interceptedReq);
}
: baseHandler;
Expand Down
43 changes: 36 additions & 7 deletions src/server/handlers/dev/projects/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { createDevUiAssetProvider } from "#veryfront/extensions/dev-ui";
import type { HandlerContext } from "../../types.ts";
import { recordRequestPeerFromTransport } from "#veryfront/platform/adapters/runtime/shared/request-peer.ts";
import type { HandlerContext } from "#veryfront/types";
import { ProjectsHandler } from "./index.ts";

const BUNDLE = "globalThis.__veryfrontProjectsTest = true;";
const PROVIDER = createDevUiAssetProvider(BUNDLE);

function projectsRequest(pathname: string, init: RequestInit = {}): Request {
const headers = new Headers(init.headers);
if (!headers.has("host")) headers.set("host", "localhost");
const request = new Request(`http://localhost${pathname}`, { ...init, headers });
recordRequestPeerFromTransport(request, {
runtime: "deno",
transport: "tcp",
hostname: "127.0.0.1",
});
return request;
}

function projectsContext(): HandlerContext {
return {
projectDir: "/project",
Expand All @@ -17,24 +30,40 @@ function projectsContext(): HandlerContext {
}

describe("ProjectsHandler", () => {
it("does not expose the project chooser to a non-loopback peer", async () => {
const request = new Request("http://localhost/_projects", {
headers: { host: "localhost" },
});
recordRequestPeerFromTransport(request, {
runtime: "deno",
transport: "tcp",
hostname: "192.168.1.25",
});

const result = await new ProjectsHandler(PROVIDER).handle(request, projectsContext());

assertEquals(result.response, undefined);
assertEquals(result.continue, true);
});

it("serves its shell and exact captured bundle", async () => {
const handler = new ProjectsHandler(PROVIDER);
const shell = (await handler.handle(
new Request("https://veryfront.test/_projects"),
projectsRequest("/_projects"),
projectsContext(),
)).response!;
assertEquals(shell.status, 200);
assertStringIncludes(await shell.text(), 'data-veryfront-dev-ui="projects"');

const asset = (await handler.handle(
new Request("https://veryfront.test/_projects/ui/index.js"),
projectsRequest("/_projects/ui/index.js"),
projectsContext(),
)).response!;
assertEquals(asset.status, 200);
assertEquals(await asset.text(), BUNDLE);

const nested = (await handler.handle(
new Request("https://veryfront.test/_projects/ui/components/App.js"),
projectsRequest("/_projects/ui/components/App.js"),
projectsContext(),
)).response!;
assertEquals(nested.status, 404);
Expand All @@ -43,15 +72,15 @@ describe("ProjectsHandler", () => {
it("rejects asset mutations and fails closed without assets", async () => {
const handler = new ProjectsHandler(PROVIDER);
const mutation = (await handler.handle(
new Request("https://veryfront.test/_projects/ui/index.js", { method: "POST" }),
projectsRequest("/_projects/ui/index.js", { method: "POST" }),
projectsContext(),
)).response!;
assertEquals(mutation.status, 405);
assertEquals(mutation.headers.get("allow"), "GET, HEAD");

const unavailable = new ProjectsHandler();
const unavailableShell = (await unavailable.handle(
new Request("https://veryfront.test/_projects"),
projectsRequest("/_projects"),
projectsContext(),
)).response!;
assertEquals(unavailableShell.status, 503);
Expand All @@ -60,7 +89,7 @@ describe("ProjectsHandler", () => {
assertStringIncludes(await unavailableShell.text(), "@veryfront/ext-dev-ui-react");

const unavailableBundle = (await unavailable.handle(
new Request("https://veryfront.test/_projects/ui/index.js"),
projectsRequest("/_projects/ui/index.js"),
projectsContext(),
)).response!;
assertEquals(unavailableBundle.status, 503);
Expand Down
2 changes: 2 additions & 0 deletions src/server/handlers/dev/projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
omitHeadResponseBody,
} from "../shared/dev-ui-bundle-response.ts";
import type { DevUiAssetProvider } from "#veryfront/extensions/dev-ui";
import { isTrustedLocalControlRequest } from "#veryfront/security/http/local-control-request.ts";

const PROJECTS_ALLOWED_METHODS = "GET, HEAD";

Expand Down Expand Up @@ -60,6 +61,7 @@ export class ProjectsHandler extends BaseHandler {

protected override shouldHandle(req: Request, ctx: HandlerContext): boolean {
if (!this.metadata.enabled?.(ctx)) return false;
if (!isTrustedLocalControlRequest(req)) return false;
Comment thread
kojiwakayama marked this conversation as resolved.

const { pathname } = new URL(req.url);
return pathname === "/" || pathname.startsWith("/_projects");
Expand Down
23 changes: 18 additions & 5 deletions src/server/handlers/dev/projects/method-policy.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { createDevUiAssetProvider } from "#veryfront/extensions/dev-ui";
import type { HandlerContext } from "../../types.ts";
import { recordRequestPeerFromTransport } from "#veryfront/platform/adapters/runtime/shared/request-peer.ts";
import type { HandlerContext } from "#veryfront/types";
import { ProjectsHandler } from "./index.ts";

const PROVIDER = createDevUiAssetProvider("globalThis.__projectsMethodPolicy = true;");

function projectsRequest(pathname: string, init: RequestInit = {}): Request {
const headers = new Headers(init.headers);
if (!headers.has("host")) headers.set("host", "localhost");
const request = new Request(`http://localhost${pathname}`, { ...init, headers });
recordRequestPeerFromTransport(request, {
runtime: "deno",
transport: "tcp",
hostname: "127.0.0.1",
});
return request;
}

function projectsContext(): HandlerContext {
return {
projectDir: "/project",
Expand All @@ -32,7 +45,7 @@ describe("ProjectsHandler method policy", () => {
}),
duplex: "half",
};
const request = new Request("http://localhost/_projects", init);
const request = projectsRequest("/_projects", init);
let responseSettled = false;
const responsePromise = new ProjectsHandler(PROVIDER).handle(request, projectsContext());
void responsePromise.then(() => {
Expand All @@ -53,11 +66,11 @@ describe("ProjectsHandler method policy", () => {
const handler = new ProjectsHandler(PROVIDER);
for (const path of ["/_projects", "/_projects/ui/index.js", "/_projects/api/config"]) {
const getResponse = (await handler.handle(
new Request(`http://localhost${path}`),
projectsRequest(path),
projectsContext(),
)).response!;
const headResponse = (await handler.handle(
new Request(`http://localhost${path}`, { method: "HEAD" }),
projectsRequest(path, { method: "HEAD" }),
projectsContext(),
)).response!;

Expand All @@ -71,7 +84,7 @@ describe("ProjectsHandler method policy", () => {
}

const unavailable = (await new ProjectsHandler().handle(
new Request("http://localhost/_projects", { method: "HEAD" }),
projectsRequest("/_projects", { method: "HEAD" }),
projectsContext(),
)).response!;
assertEquals(unavailable.status, 503);
Expand Down
3 changes: 2 additions & 1 deletion src/server/production-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import { isSharedProjectRuntime } from "#veryfront/security/project-locality.ts";
import { getIsolationPosture } from "#veryfront/security/sandbox/worker-pool.ts";
import { runStartupDiscovery } from "./startup-discovery.ts";
import { runRequestInterceptor } from "#veryfront/platform/adapters/runtime/shared/request-peer.ts";

const serverLog = logger.component("server");
const globalLog = logger.component("global");
Expand Down Expand Up @@ -320,7 +321,7 @@ export function startProductionServer(
async (req: Request) => {
const isWebSocketUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
if (isWebSocketUpgrade) return coreHandler(req);
return coreHandler(await requestInterceptor(req));
return coreHandler(await runRequestInterceptor(req, requestInterceptor));
},
{ ready: coreHandler.ready },
)
Expand Down
26 changes: 26 additions & 0 deletions src/server/request-interceptor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { assert, assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import {
isRequestFromLoopbackPeer,
recordRequestPeerFromTransport,
runRequestInterceptor,
} from "#veryfront/platform/adapters/runtime/shared/request-peer.ts";

describe("runRequestInterceptor", () => {
it("preserves transport peer provenance when an interceptor replaces the request", async () => {
const request = new Request("http://localhost/");
recordRequestPeerFromTransport(request, {
runtime: "deno",
transport: "tcp",
hostname: "127.0.0.1",
});

const intercepted = await runRequestInterceptor(
request,
(incoming) => new Request(incoming, { headers: { host: "localhost" } }),
);

assert(intercepted !== request);
assertEquals(isRequestFromLoopbackPeer(intercepted), true);
});
});
7 changes: 6 additions & 1 deletion src/server/runtime-handler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,12 @@ export function createVeryfrontHandler(

// Handle projects discovery UI
if (
shouldHandleProjectsUI(url.pathname, projectRes.projectSlug, projectRes.parsedDomain)
shouldHandleProjectsUI(
request,
url.pathname,
projectRes.projectSlug,
projectRes.parsedDomain,
)
) {
const response = await handleProjectsRequest(
request,
Expand Down
Loading