/** * webui-serving.ts * * Two opt-in capabilities that let a browser-hosted web UI reach the daemon: * * 1. Same-origin bundle serving (PRIMARY). When `controlPlane.webui.serve` is * on, the daemon serves a configured build directory at `/` so the bundle * and the API share an origin, the browser's same-origin policy is then a * non-issue. This is wire-compatible with `tailscale serve`, which fronts * the single daemon origin over HTTPS: bundle + API arrive same-origin. * The bundle is public (served without a token); the app itself * token-authenticates its API calls, so no wire data leaks from serving * static files. * * 2. Cross-origin request support (SECONDARY). When `controlPlane.cors.enabled` * is on, OPTIONS preflight is answered and Access-Control-Allow-* headers * are emitted ONLY for origins on the explicit `controlPlane.cors.allowedOrigins` * allowlist (the Vite dev server, a deliberately separate origin, etc.). * There is no wildcard: the matched origin is echoed back, credentials are * allowlist-gated, and `Vary: Origin` is always set so caches stay correct. * * BOTH default OFF. With both off the daemon behaves exactly as before: no bundle * is served and no Access-Control-Allow-Origin is ever emitted. The loopback * default posture is unchanged; these capabilities are explicit opt-ins, never * auto-enabled by network host mode. */ import type { ConfigManager } from '../../config/manager.js'; /** Resolved, per-request view of the two opt-in serving capabilities. */ export interface WebuiServingPosture { /** True when the daemon should serve the configured bundle directory at `/`. */ readonly serveBundle: boolean; /** * Absolute or working-dir-relative directory whose contents are served. * * `controlPlane.webui.bundleDir` when that key names one, and * `web.staticAssetsDir` otherwise, see {@link resolveWebuiServingPosture} for * why the precedence runs that way. Empty string only when both are empty. */ readonly bundleDir: string; /** Which config key supplied {@link bundleDir}. Empty when neither did. */ readonly bundleDirSource: 'controlPlane.webui.bundleDir' | 'web.staticAssetsDir' | ''; /** Cross-origin request support (OPTIONS preflight + Access-Control-Allow-*). */ readonly cors: { readonly enabled: boolean; /** Explicit origin allowlist. Never wildcarded; empty means "refuse every origin". */ readonly allowedOrigins: readonly string[]; }; /** OpenAI-compatible path prefix (reserved so bundle serving never shadows it). */ readonly openaiPathPrefix: string; /** * Top-level path roots the daemon's own routes own. A GET at one of these is * never answered from the bundle directory, see {@link reservedApiPathRoots}. */ readonly reservedApiPaths: readonly string[]; } /** * The path roots the daemon serves regardless of what any method descriptor * says: the API tree, the login post, the inbound webhook tree and the task * route. Everything else is derived from the operator contract's own http * bindings, so a route added there is reserved without a second list to keep * in step. */ export declare const BASE_RESERVED_API_PATHS: readonly string[]; /** * The reserved set, from the operator contract's advertised paths. * * Every method descriptor with an http binding is a promise that this daemon * answers that path. `/status`, `/config` and `/config/credentials` are the * top-level ones, and a bundle served at `/` was answering all three with the * app shell: an extension-less GET looks exactly like an SPA navigation, so the * shell came back with 200 text/html and every client's liveness probe, the web * surface's own admin reads and the version gate read HTML instead of JSON. * * Only the FIRST segment is taken. `/config/credentials` reserves `/config`, * which is the boundary a client's path actually falls inside; reserving the * full template instead would leave `/config` itself shadowed. */ export declare function reservedApiPathRoots(descriptorPaths: Iterable): readonly string[]; /** What the posture read needs from the method catalog: the advertised paths. */ export interface WebuiServingRouteAuthority { list(): readonly { readonly http?: { readonly path: string; } | undefined; }[]; } /** * Read the current serving posture from config. Cheap; called per request. * * DIRECTORY PRECEDENCE. Two keys name a directory of static files for the * embedded web surface, and until this read existed only one of them did * anything: * * - `controlPlane.webui.bundleDir` wins whenever it names a directory. It is * the specific answer to "which built bundle does THIS daemon serve", it * ships empty, and an operator who filled it in has answered that question * for this machine. * - `web.staticAssetsDir` supplies the directory when bundleDir is empty. It * is the web surface's own assets directory, it ships with the build's * conventional output path (`dist/web`), and a host that lays its bundle * down where the build puts it now needs only `controlPlane.webui.serve`. * * The order is deliberately NOT the other way round: `web.staticAssetsDir` has a * non-empty shipped default, so letting it win would make bundleDir unreachable * for anyone who never touched either key, trading one dead key for another. * `controlPlane.webui.serve` remains the only switch; neither directory key * turns serving on by itself. */ export declare function resolveWebuiServingPosture(configManager: ConfigManager, routeAuthority?: WebuiServingRouteAuthority): WebuiServingPosture; /** * Answer a CORS preflight (OPTIONS). Emits Access-Control-Allow-* only for an * allowlisted origin; a non-allowlisted origin is refused honestly (403, no * Access-Control-Allow-Origin) so the browser preflight fails cleanly. */ export declare function handleCorsPreflight(req: Request, posture: WebuiServingPosture): Response; /** * Decorate an actual (non-preflight) response with CORS headers when the request * carries an allowlisted Origin. `Vary: Origin` is always added while CORS is * enabled so caches never serve an allow-origin to the wrong origin. A * non-allowlisted origin gets Vary but NO Access-Control-Allow-Origin, the * browser then blocks the read honestly. */ export declare function applyCorsHeaders(req: Request, response: Response, posture: WebuiServingPosture): Response; /** * Serve a file from the configured bundle directory, with SPA fallback to * index.html for navigation routes. Returns null when serving is disabled, the * request is not a GET/HEAD, or the path is a reserved API route, in every * "null" case the caller continues with normal daemon routing, so behavior is * unchanged when the capability is off. Path traversal outside the bundle * directory is refused with 403. */ export declare function serveWebuiBundle(req: Request, posture: WebuiServingPosture): Promise; //# sourceMappingURL=webui-serving.d.ts.map