// june.config.ts support — the resource manifest / feature config.
//
// The agent surface (discovery + MCP) is ON by default: it exposes the same
// actions and the same authorization the UI already exposes, so it does not
// widen the attack surface. Gate sensitive actions with permissions, not by
// hiding the endpoint. Turn any of it off here when you must.
//
// PURITY: this module is the config SCHEMA and its pure resolvers only. The
// `loadJuneConfig(appDir)` reader (node:fs / node:path / dynamic import of the
// user's june.config.ts) is a HOST concern and lives in the Phase-2 host layer
// — keeping the contract layer free of `node:*` (zero node:*/Bun.* in this layer).
import type { CacheStoreFactory } from "./cache";
import type { I18nConfig } from "./i18n";
import type { JuneDb, ResourceConfig } from "./resources";
import { validateOriginPolicy } from "./origin-policy";
// The DURABLE conversational agent (the agent/ directory). Distinct from the
// discoverability flags above, which make a web app agent-readable; this mounts a
// running agent: its chat endpoint + inbound channels. Its tools are the SAME
// defineActions the discovery/mcp surface already exposes. Off unless an agent/
// directory + `runtime.enabled` are present.
// Site-wide identity the document's
is built from (see JuneConfig.site).
// url the public origin ("https://acme.com"), used to make og:url, canonical,
// and og:image absolute. Unset → https://, else the
// request's own origin — never the build's prerender host. An i18n
// locale's own domain always wins for its pages.
// twitter the site's X/Twitter handle ("@acme") → twitter:site.
// themeColor the mobile browser-toolbar colour ():
// one colour, or { light, dark } for a page that follows the system
// colour scheme. Unset → June's starter background when June owns the
// page's look (see Document), else no tag.
// shortName the home-screen label in the generated web manifest. Unset → the
// part of `name` before a " — ", " - ", " | ", or ": " separator.
// organization who runs the site → an Organization node in the homepage JSON-LD
// (the WebSite's publisher): the machine-readable identity agents and
// search engines verify a site against. `email` becomes a ContactPoint;
// `sameAs` lists official profiles (GitHub, LinkedIn, …) and is merged
// with the `twitter` handle's profile URL. Unset → no Organization node:
// June never invents who runs a site.
// jsonLd extra schema.org nodes (SoftwareApplication, FAQPage, Product, …)
// appended to the homepage's JSON-LD @graph (a node's own top-level
// @context is dropped; the graph carries one). They can reference the
// built-in nodes by @id: "#website", "#organization",
// where is the public origin + basePath + "/"
// (e.g. "https://acme.github.io/docs/#website").
export type SiteConfig = {
name?: string;
shortName?: string;
titleTemplate?: string;
description?: string;
icon?: string;
lang?: string;
url?: string;
twitter?: string;
themeColor?: string | { light: string; dark: string };
organization?: SiteOrganization;
jsonLd?: JsonLdNode | JsonLdNode[];
};
// A schema.org node, as plain JSON (it freezes into the worker manifest).
export type JsonLdNode = { "@type": string | string[] } & Record;
// The Organization behind a site. Every field is optional: `name` defaults to
// site.name, `url` to the site's public origin, `logo` to site.icon. Relative
// URLs resolve against the public origin.
export type SiteOrganization = {
name?: string;
url?: string;
logo?: string;
email?: string; // → contactPoint { contactType: "customer support", email }
telephone?: string; // → the same ContactPoint
sameAs?: string[];
address?: {
streetAddress?: string;
addressLocality?: string;
addressRegion?: string;
postalCode?: string;
addressCountry?: string;
};
};
// The site's short label: site.shortName, else the site name's leading segment
// ("June — build agents into real apps" → "June"). Used wherever a tagline would
// be wrong — the home-screen label, the MCP server's title.
export function siteShortName(site: Pick): string | undefined {
if (site.shortName) return site.shortName;
const name = site.name?.trim();
if (!name) return undefined;
return name.split(/\s+[—–|-]\s+|:\s+/)[0]!.trim() || name;
}
// DocumentConfig.deployOrigin from the config's deploy domain — the public-origin
// fallback for prerendered pages, which render against a placeholder host.
export function deployOrigin(cfg: Pick): string | undefined {
return cfg.deploy?.domain ? `https://${cfg.deploy.domain}` : undefined;
}
export type AgentRuntimeConfig = {
enabled: boolean; // mount the durable agent surface
dir: string; // the agent/ directory, relative to the app (default "agent")
backend: "native" | "memory" | "durable"; // native = SQLite; memory = ephemeral; durable = DO (edge)
chat: { path: string }; // where a turn is POSTed (default "/message")
channels: boolean; // mount discovered channels/* at their paths (slack/crisp/…)
};
export type AgentConfig = {
enabled: boolean; // master switch
discovery: boolean; // Link header, llms.txt, sitemap, api-catalog, mcp server-card, ai-catalog (ARD), agent skills
mcp: boolean; // the /mcp execution endpoint
api: boolean; // the REST projection: POST /api/ + /openapi.json
webmcp: boolean; // inject WebMCP tool registrations into the view
// Which browsers may call /mcp and /api/ (#308; see OriginPolicy in ./origin-policy).
// allowedOrigins — origins allowed besides the site's own, e.g. a separate web app
// that calls the API from the browser. A request without an
// Origin (CLI, SDK, server-side connector) always passes.
// allowedHosts — when set, the Host must be an IP literal or match an entry
// ("example.com", or ".example.com" with its subdomains): the
// DNS-rebinding check. `june dev` adds "localhost" and ".localhost";
// add a tunnel's name (".trycloudflare.com") to reach dev through it.
allowedOrigins?: string[];
allowedHosts?: string[];
// Optional llms.txt customization for apps built ON June (e.g. the Kura docs framework):
// framework — replaces the built-in "canonical names" block so a meta-framework can point
// agents at ITS scaffold/scope instead of June's.
// sections — extra Markdown lines appended (e.g. a list of every doc page + its `.md`).
// whenToUse — the jobs this site/product is the right tool for, one bullet each, rendered
// as "## When to use" right under the summary: an agent choosing between
// tools reads it first. Specific use cases, not marketing copy.
// All plain string arrays so they freeze into the worker manifest as-is.
llms?: { framework?: string[]; sections?: string[]; whenToUse?: string[] };
// The /mcp server's identity — the initialize handshake (serverInfo + instructions)
// and the server card. Every field is optional; unset ones are derived from `site`
// and the app's tools (see mcpServerIdentity in ./mcp):
// name serverInfo.name and the card's reverse-DNS name. Default
// "/" (june.build → "build.june/june").
// title the display name. Default: the site's short name.
// description the server card's one-liner (≤ 100 chars, the v1 schema's limit).
// Default: site.description, trimmed at a sentence or word boundary.
// version Default "0.0.0" — set it to your app's release version.
// instructions the usage guidance clients hand the model. Default: generated from
// the FULL site.description, the tool list, and the llms.txt / .md pointers.
// Overrides are validated against the v1 Server Card schema when the config resolves
// (see validateMcpServer), so a bad value fails the build, not a client.
mcpServer?: { name?: string; title?: string; description?: string; version?: string; instructions?: string };
// The durable agent runtime (opt-in). Resolved to full shape below.
runtime: AgentRuntimeConfig;
};
export type SpeculationConfig = {
// hover-intent prerender: "moderate" (hover) | "conservative" (mousedown,
// for heavy pages) | false. Default "moderate" — light MPA pages get 0ms
// navigations for free.
prerender?: "moderate" | "conservative" | false;
prefetch?: "moderate" | "conservative" | false;
// App-specific exclusions, ADDED to the built-in ones (agent surfaces:
// *.md *.json *.txt *.xml /mcp — those are always excluded).
exclude?: string[];
// "inline" (default): rules in a