Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,17 @@ bun run changeset

**Before committing:**
```bash
bun run test # Run all tests
bun run test # Run all tests (root script — NOT `bun test`, which uses Bun's native runner)
bun typecheck # Type check all packages
bun lint # Run linting
```

Host tests specifically use vitest via the workspace script:
```bash
bun run --cwd host test # NODE_ENV=production BOS_CONFIG_PATH=../bos.config.json vitest run
```
Always use `bun run test` / `bun run --cwd host test` — never `bun test`, which invokes Bun's built-in runner and produces different (and misleading) results.

## Common Patterns

### Authentication Check
Expand Down
39 changes: 39 additions & 0 deletions host/src/middleware/static-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { Context } from "hono";
import { proxy } from "hono/proxy";
import type { AuthVariables } from "../lib/auth";
import { STATIC_ASSET_PATTERN } from "../middleware/security";
import type { RuntimeConfig } from "../services/config";
import { getTenantRuntimeErrorResponse, resolveRequestRuntime } from "../services/tenant-runtime";
import { logger } from "../utils/logger";

type HonoEnv = { Variables: AuthVariables };

export async function proxyRequest(
req: Request,
Expand Down Expand Up @@ -83,3 +91,34 @@ export async function proxyStaticAssetRequest(req: Request, targetBase: string):

return response;
}

export function createStaticAssetProxyHandler(config: RuntimeConfig) {
return async (c: Context<HonoEnv>, next: () => Promise<void>) => {
const { pathname } = new URL(c.req.url);

if (
pathname === "/" ||
pathname === "/api" ||
pathname.startsWith("/api/") ||
pathname === "/health"
) {
return next();
}

const lastSegment = pathname.split("/").pop() ?? "";
if (!STATIC_ASSET_PATTERN.test(lastSegment)) {
return next();
}

try {
const runtime = await resolveRequestRuntime(config, c.req.raw, {
verification: "stale-while-revalidate",
});
return await proxyStaticAssetRequest(c.req.raw, runtime.config.ui.url);
} catch (error) {
const { message, status } = getTenantRuntimeErrorResponse(error);
logger.error(`[Proxy Asset] ${c.req.method} ${c.req.path} — ${message}`);
return c.text(message, { status: status as 404 | 500 | 502 });
}
};
}
286 changes: 9 additions & 277 deletions host/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,128 +8,24 @@ import {
Layer,
ManagedRuntime,
} from "every-plugin/effect";
import { getBaseStyles, getHydrateScript, getThemeInitScript } from "everything-dev/ui/head";
import { type Context, Hono } from "hono";
import type { AuthVariables } from "./lib/auth";
import { getCspStrict, SecurityMiddleware, STATIC_ASSET_PATTERN } from "./middleware/security";
import { proxyStaticAssetRequest } from "./middleware/static-proxy";
import { getCspStrict, SecurityMiddleware } from "./middleware/security";
import { createStaticAssetProxyHandler } from "./middleware/static-proxy";
import { setupApiRoutes } from "./routes/api";
import type { HealthLoadingState } from "./routes/health";
import { buildPluginContext, createSessionMiddleware, registerAuthHandler } from "./services/auth";
import { type ClientRuntimeConfig, ConfigService, type RuntimeConfig } from "./services/config";
import { loadRouterModule, resetFederationInstance } from "./services/federation.server";
import { createSsrFallbackHandler } from "./routes/ssr";
import { createSessionMiddleware, registerAuthHandler } from "./services/auth";
import { ConfigService, type RuntimeConfig } from "./services/config";
import { resetFederationInstance } from "./services/federation.server";
import { startIntegrityMonitor } from "./services/integrity-monitor";
import { closeMcpServer } from "./services/mcp";
import { createPluginsClient, type PluginResult, PluginsService } from "./services/plugins";
import { getTenantRuntimeErrorResponse, resolveRequestRuntime } from "./services/tenant-runtime";
import type { RouterModule, RuntimePlugin } from "./types";
import { PluginsService } from "./services/plugins";
import { extractErrorDetails } from "./utils/errors";
import { logger } from "./utils/logger";
import { normalizeUrl } from "./utils/normalize";

type HonoEnv = { Variables: AuthVariables };

type ActiveRuntimeState = NonNullable<ClientRuntimeConfig["runtime"]>;

type RuntimeClientConfig = ClientRuntimeConfig & { runtime?: ActiveRuntimeState };

function getFallbackGatewayId(config: RuntimeConfig) {
if (config.domain) {
return config.domain;
}

return normalizeUrl(config.host?.url)?.replace(/^https?:\/\//, "") ?? "runtime";
}

function resolveActiveRuntime(config: RuntimeConfig, request: Request) {
const url = new URL(request.url);
const fallbackGatewayId = getFallbackGatewayId(config);
return {
accountId: config.account,
gatewayId: fallbackGatewayId,
runtimeBasePath: "/",
title: config.title ?? config.account,
description: config.description ?? null,
hostUrl: url.origin,
} satisfies ActiveRuntimeState;
}

function buildRuntimeClientConfig(
config: RuntimeConfig,
request: Request,
activeRuntime: ActiveRuntimeState,
plugins: PluginResult,
): RuntimeClientConfig {
const requestUrl = new URL(request.url);
const uiConfig = config.ui;

if (!uiConfig) {
throw new Error("UI config is required to build the runtime client config");
}

return {
env: config.env,
account: activeRuntime.accountId,
networkId: config.account.endsWith(".testnet") ? "testnet" : "mainnet",
hostUrl: requestUrl.origin,
assetsUrl: uiConfig.url,
apiBase: "/api",
rpcBase: "/api/rpc",
authAvailable: plugins.auth !== null,
repository: config.repository,
ui: {
name: uiConfig.name,
url: uiConfig.url,
entry: uiConfig.entry,
integrity: uiConfig.integrity,
},
api: config.api
? {
name: config.api.name,
url: config.api.url,
entry: config.api.entry,
integrity: config.api.integrity,
...(config.api.variables ? { variables: config.api.variables } : {}),
}
: undefined,
auth: config.auth
? {
name: config.auth.name,
url: config.auth.url,
entry: config.auth.entry,
integrity: config.auth.integrity,
...(config.auth.variables ? { variables: config.auth.variables } : {}),
}
: undefined,
plugins: Object.fromEntries(
(Object.entries(config.plugins ?? {}) as Array<[string, RuntimePlugin]>).map(
([key, plugin]) => [
key,
{
name: plugin.name,
url: plugin.url,
entry: plugin.entry,
integrity: plugin.integrity,
...(plugin.variables ? { variables: plugin.variables } : {}),
...(plugin.ui
? {
ui: {
name: plugin.ui.name,
url: plugin.ui.url,
entry: plugin.ui.entry,
source: plugin.ui.source,
integrity: plugin.ui.integrity,
},
}
: {}),
},
],
),
),
runtime: activeRuntime,
} as RuntimeClientConfig;
}

export const createStartServer = (onReady?: () => void) =>
Effect.gen(function* () {
const port = Number(process.env.PORT) || 3000;
Expand Down Expand Up @@ -171,73 +67,7 @@ export const createStartServer = (onReady?: () => void) =>
ssrEnabled: Boolean(uiConfig.ssrUrl),
};

const renderClientShell = (
ctx: Context<HonoEnv>,
runtimeSourceConfig: RuntimeConfig,
runtimeConfig: ClientRuntimeConfig,
error?: Error | null,
) => {
const nonce = CSP_STRICT ? ctx.get("secureHeadersNonce") : undefined;
const uiIntegrity = runtimeSourceConfig.ui.integrity;
const assetsUrl = runtimeConfig.assetsUrl.replace(/\/$/, "");
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
const sriAttr = uiIntegrity ? ` integrity="${uiIntegrity}" crossorigin="anonymous"` : "";
const uiVersion = uiIntegrity ? `?v=${encodeURIComponent(uiIntegrity)}` : "";

const baseStyles = `
${getBaseStyles()}
.shell { min-height: 100vh; min-height: 100dvh; display: flex; align-items: center; justify-content: center; }
.fade { animation: fadeIn 0.3s ease-in; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.error { color: #fca5a5; }
`.trim();

const themeScript = `<script${nonceAttr}>${(getThemeInitScript() as { children?: string }).children ?? ""}</script>`;

const shellBody = `<div id="root"><div class="shell"><div class="fade">${
error
? `<p class="error">SSR unavailable, showing client app.</p><p>${error.message}</p>`
: `<p>Loading...</p>`
}</div></div></div>`;

const title =
runtimeConfig.runtime?.title ?? runtimeSourceConfig.title ?? runtimeSourceConfig.account;
const hydrateScript =
(
getHydrateScript(
runtimeConfig as Partial<ClientRuntimeConfig>,
undefined,
undefined,
nonce,
) as { children?: string }
).children ?? "";

return ctx.html(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>${title}</title>
<link rel="manifest" href="${assetsUrl}/site.webmanifest" />
<link rel="stylesheet" href="${assetsUrl}/static/css/style.css${uiVersion}" />
<style>${baseStyles}</style>
${themeScript}
<script${nonceAttr} src="${assetsUrl}/remoteEntry.js${uiVersion}"${sriAttr}></script>
<script${nonceAttr}>${hydrateScript}</script>
</head>
<body>${shellBody}</body>
</html>`,
200,
);
};

const proxyUiAssetRequest = async (c: Context<HonoEnv>) => {
const runtime = await resolveRequestRuntime(config, c.req.raw, {
verification: "stale-while-revalidate",
});
return await proxyStaticAssetRequest(c.req.raw, runtime.config.ui.url);
};
app.on(["GET", "HEAD"], "*", createStaticAssetProxyHandler(config));

const sessionMiddleware = createSessionMiddleware(plugins);

Expand All @@ -256,107 +86,9 @@ export const createStartServer = (onReady?: () => void) =>
setupApiRoutes(app, config, plugins, sessionMiddleware, loadingState),
);

app.on(["GET", "HEAD"], "*", async (c: Context<HonoEnv>, next) => {
const { pathname } = new URL(c.req.url);

if (
pathname === "/" ||
pathname === "/api" ||
pathname.startsWith("/api/") ||
pathname === "/health"
) {
return next();
}

const lastSegment = pathname.split("/").pop() ?? "";
if (!STATIC_ASSET_PATTERN.test(lastSegment)) {
return next();
}

try {
return await proxyUiAssetRequest(c);
} catch (error) {
const { message, status } = getTenantRuntimeErrorResponse(error);
logger.error(`[Proxy Asset] ${c.req.method} ${c.req.path} — ${message}`);
return c.text(message, { status: status as 404 | 500 | 502 });
}
});

app.use("/*", sessionMiddleware);

app.get("*", async (c: Context<HonoEnv>) => {
if (c.req.path === "/api" || c.req.path.startsWith("/api/")) {
return c.notFound();
}

let resolvedRuntime: Awaited<ReturnType<typeof resolveRequestRuntime>>;
try {
resolvedRuntime = await resolveRequestRuntime(config, c.req.raw, {
verification: "blocking",
});
} catch (error) {
const { message, status } = getTenantRuntimeErrorResponse(error);
logger.error(`[SSR] ${c.req.method} ${c.req.path} — ${message}`);
return c.text(message, { status: status as 404 | 500 | 502 });
}

const effectiveConfig = resolvedRuntime.config;
const activeRuntime = await resolveActiveRuntime(effectiveConfig, c.req.raw);
const nonce = CSP_STRICT ? c.get("secureHeadersNonce") : undefined;
const runtimeConfig = buildRuntimeClientConfig(
effectiveConfig,
c.req.raw,
activeRuntime,
plugins,
);

let ssrRouterModule: RouterModule | null = null;
let moduleLoadError: Error | null = null;

if (effectiveConfig.ui.ssrUrl) {
const result = await Effect.runPromise(
loadRouterModule(effectiveConfig).pipe(Effect.either),
);
if (result._tag === "Right") {
ssrRouterModule = result.right;
} else {
moduleLoadError = result.left;
logger.error("[SSR] Failed to load Router module:", moduleLoadError);
}
}

if (ssrRouterModule && effectiveConfig.ui.ssrUrl) {
try {
const pluginContext = buildPluginContext(c);
const ssrApiClient = createPluginsClient(plugins, pluginContext);

const render = () =>
ssrRouterModule.renderToStream(c.req.raw, {
session: c.get("session") ? { session: c.get("session"), user: c.get("user") } : null,
basepath: runtimeConfig.runtime?.runtimeBasePath,
runtimeConfig,
apiClient: ssrApiClient,
cspNonce: nonce,
});

const result = await render();
const responseHeaders = new Headers(result?.headers);
const cspHeader = c.res.headers.get("Content-Security-Policy");
if (cspHeader) {
responseHeaders.set("Content-Security-Policy", cspHeader);
}
return new Response(result?.stream, {
status: result?.statusCode,
headers: responseHeaders,
});
} catch (error) {
logger.error("[SSR] Streaming error:", error);
moduleLoadError = error as Error;
}
}

return renderClientShell(c, effectiveConfig, runtimeConfig, moduleLoadError);
});
app.get("*", createSsrFallbackHandler(config, plugins, CSP_STRICT));

const startHttpServer = () => {
const hostname = process.env.HOST || "0.0.0.0";
Expand Down
Loading
Loading