New:Socket for Asana Is Now Available.Learn more
Get Started

devframe

Package Overview
Dependencies
Maintainers
1
Versions
63
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

devframe - npm Package Compare versions

Comparing version
0.9.0-beta.9
to
0.9.0-beta.10
+68
dist/_shared-CbZ_yhbz.mjs
import "./constants.mjs";
import { n as getPort } from "./dist-eNI9qM_I.mjs";
import { cleanDoubleSlashes, withLeadingSlash, withTrailingSlash, withoutLeadingSlash } from "ufo";
//#region src/adapters/_shared.ts
const DEFAULT_PORT = 9999;
/**
* Resolve the mount base path for a devframe's SPA. Hosted adapters
* (`vite`, `embedded`) default to `/__<id>/` so they don't collide
* with the host app; standalone adapters (`cli`, `build`)
* default to `/` because they own the origin.
*
* The devframe author can override with `basePath` on the definition.
*/
function resolveBasePath(def, kind) {
if (def.basePath) return normalizeBasePath(def.basePath);
return kind === "standalone" ? "/" : `/__${def.id}/`;
}
function normalizeBasePath(base) {
return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base)));
}
/**
* Resolve the listening port for `createDevServer` (and `createHandler`'s
* side-car tiers), honoring the definition's `cli.port` / `cli.portRange` /
* `cli.random` settings. Exposed separately so authors who run their own
* argv parsing can resolve a port up-front (to print it, log it, etc.)
* before starting the server.
*/
async function resolveDevServerPort(def, options = {}) {
const host = options.host ?? def.cli?.host ?? "localhost";
const portOptions = {
port: options.defaultPort ?? def.cli?.port ?? DEFAULT_PORT,
host
};
if (def.cli?.portRange) portOptions.portRange = def.cli.portRange;
if (def.cli?.random) portOptions.random = def.cli.random;
return getPort(portOptions);
}
/**
* Normalize the `cli.mcp` / `mcp` option (`boolean | McpRouteOptions`) into
* concrete options, or `undefined` when the MCP route is disabled.
*/
function resolveMcpConfig(mcp) {
if (!mcp) return void 0;
return mcp === true ? {} : mcp;
}
/**
* Resolve the `mcp` entry a `__connection.json` should advertise for a dev
* server started with the given `mcp` option (falling back to `def.cli?.mcp`,
* exactly like `createDevServer`), or `undefined` when the route is
* disabled.
*
* Hosted bridges that hand-roll their connection meta pass the side-car
* `port`: the advertised path becomes absolute (the side-car mounts at `/`)
* and the client dials `<page-host>:<port><path>`. Without `port` the path
* stays relative, resolved against `__connection.json`'s own location (the
* same-server default).
*/
function resolveMcpConnectionMeta(def, mcp, port) {
const config = resolveMcpConfig(mcp ?? def.cli?.mcp);
if (!config) return void 0;
const route = withoutLeadingSlash(config.path ?? "__mcp");
return port != null ? {
path: withLeadingSlash(route),
port
} : { path: route };
}
//#endregion
export { resolveMcpConnectionMeta as i, resolveBasePath as n, resolveDevServerPort as r, normalizeBasePath as t };
import { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs";
import { t as diagnostics } from "./diagnostics-DOQPnQmX.mjs";
import { t as createHostContext } from "./context-CEe4PBz1.mjs";
import { t as resolveStaticAssetsSource } from "./remote-assets-nTTxY2MU.mjs";
import { i as resolveMcpConnectionMeta, n as resolveBasePath, r as resolveDevServerPort, t as normalizeBasePath } from "./_shared-CbZ_yhbz.mjs";
import { t as createH3DevframeHost } from "./host-h3-Crkzm42q.mjs";
import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, t as createInstanceShell } from "./instance-shell-CMFyszWJ.mjs";
import { open } from "./utils/open.mjs";
import { mountStaticHandler } from "./utils/serve-static.mjs";
import { createServer } from "node:http";
import { resolve } from "pathe";
import process from "node:process";
import { joinURL, withBase } from "ufo";
import { H3, toNodeHandler } from "h3";
//#region src/adapters/initiate.ts
const INSTANCE_INTERNALS = /* @__PURE__ */ new WeakMap();
/** @internal */
function getInstanceInternals(handler) {
return INSTANCE_INTERNALS.get(handler) ?? {};
}
/**
* Serve a devframe through one framework-agnostic, web-standard handler —
* the SPA, `__connection.json` discovery, the WebSocket RPC endpoint, the
* auth gate, and the optional MCP route, all under a single mount base.
* Mount `handler` on any framework's catch-all route (or `nodeMiddleware` on
* a connect stack) and the devframe is live inside that app.
*
* The factory is synchronous and kicks off initialization eagerly;
* `handler`/`nodeMiddleware` await readiness internally. Nothing binds a port
* on its own: the WebSocket resolves in precedence order — `ws.port` (pinned
* side-car) > `server` (shared upgrade at `<base>__ws`) > `ws.sidecar`
* (auto-port side-car) > the host driving upgrades itself through
* {@link DevframeInstance.attach} — while `ws.url`, when set, overrides the
* advertised* endpoint (the tunnel pattern) and on its own hands the whole
* transport to an external server. `__connection.json` reflects whichever
* combination is active.
*/
function initDevframe(def, options) {
const base = normalizeBasePath(options.base);
const distDir = options.distDir === false ? void 0 : options.distDir ?? def.cli?.distDir;
const app = options.app ?? new H3();
const shell = createInstanceShell({
base,
app,
host: options.host ?? def.cli?.host ?? "localhost",
origin: options.origin,
auth: options.auth !== void 0 ? options.auth : def.cli?.auth,
server: options.server,
ws: options.ws ?? def.cli?.ws,
sse: options.sse ?? def.cli?.sse,
allowedOrigins: options.allowedOrigins,
destroyUnmatchedUpgrades: options.destroyUnmatchedUpgrades,
onPeerConnect: options.onPeerConnect,
onPeerDisconnect: options.onPeerDisconnect,
register: resolveInstanceRegister(options.register, {
id: def.id,
name: def.name
}),
resolveSidecarPort: (sidecarHost) => resolveDevServerPort(def, { host: sidecarHost }),
onMetaUnavailable: () => {
throw diagnostics.DF0054({ id: def.id });
},
async init(api) {
const h3Host = createH3DevframeHost({
origin: () => api.origin() ?? "http://localhost",
appName: def.id,
mount: (mountBase, dir) => {
mountStaticHandler(app, mountBase, dir);
}
});
const hostImpl = options.getStorageDir ? {
...h3Host,
getStorageDir: options.getStorageDir
} : h3Host;
const context = await createHostContext({
cwd: process.cwd(),
mode: "dev",
host: hostImpl
});
const setupInfo = { flags: options.flags ?? {} };
await def.setup(context, setupInfo);
const mcpOption = options.mcp ?? def.cli?.mcp;
const mcpMeta = resolveMcpConnectionMeta(def, mcpOption);
let mcpDispose;
if (mcpMeta) {
const mcpConfig = mcpOption === true || mcpOption === void 0 ? {} : mcpOption;
const mcpPath = joinURL(base, mcpMeta.path);
let mountMcpHttp;
try {
({mountMcpHttp} = await import("./http-DYpK0cUi.mjs").then((n) => n.t));
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw diagnostics.DF0017({
transport: "http",
reason,
cause: error
});
}
mcpDispose = mountMcpHttp(app, context, mcpPath, {
serverName: `${def.id} (devframe)`,
serverVersion: def.version ?? "0.0.0",
exposeSharedState: true,
allowedOrigins: mcpConfig.allowedOrigins
}).dispose;
}
return {
context,
...mcpMeta ? { mcp: mcpMeta } : {},
...mcpDispose ? { dispose: mcpDispose } : {}
};
},
mount(context, meta) {
app.use(joinURL(base, DEVFRAME_CONNECTION_META_FILENAME), () => meta);
if (distDir) {
const source = resolveStaticAssetsSource(distDir, context.host.getStorageDir("project"));
mountStaticHandler(app, base, typeof source === "string" ? resolve(source) : source);
}
}
});
const instance = {
base: shell.base,
handler: shell.handler,
nodeMiddleware: shell.nodeMiddleware,
attach: shell.attach,
handleUpgrade: shell.handleUpgrade,
ready: shell.ready,
context: shell.context,
connectionMeta: shell.connectionMeta,
close: shell.close
};
INSTANCE_INTERNALS.set(instance, shell.internals);
return instance;
}
//#endregion
//#region src/adapters/dev.ts
/**
* Start a devframe dev server for a {@link DevframeDefinition} —
* h3 + WebSocket RPC + (optionally) the author's SPA mounted at the
* resolved base path.
*
* When `distDir` is omitted (and `def.cli?.distDir` is unset) the
* server runs in **bridge mode**: only `__connection.json` and the WS
* endpoint are mounted, with no SPA mount. The SPA is expected to be
* hosted elsewhere (e.g. by a parent Vite/Nuxt dev server) — see
* `devframeViteBridge` from `@devframes/vite`.
*
* Returns the underlying {@link StartedServer} handle so callers can
* close it gracefully (SIGINT, hot-reload, test teardown).
*
* Use this directly when integrating devframe into an existing CLI
* framework (commander, yargs, hand-rolled CAC). For the all-in-one
* `dev` / `build` / `mcp` shell, reach for {@link createCac} instead.
*/
async function createDevServer(def, options = {}) {
if (def.capabilities?.dev === false && !options.force) throw diagnostics.DF0058({ id: def.id });
const host = options.host ?? def.cli?.host ?? "localhost";
const requestedPort = options.port ?? await resolveDevServerPort(def, { host });
const flags = options.flags ?? {};
const basePath = options.basePath ? normalizeBasePath(options.basePath) : resolveBasePath(def, "standalone");
const app = options.app ?? new H3();
const server = createServer(toNodeHandler(app));
try {
await new Promise((resolveListen, rejectListen) => {
const onError = (error) => rejectListen(error);
server.once("error", onError);
server.listen(requestedPort, host, () => {
server.removeListener("error", onError);
resolveListen();
});
});
} catch (error) {
throw diagnostics.DF0052({
host,
port: requestedPort,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
const address = server.address();
const port = typeof address === "object" && address ? address.port : requestedPort;
const origin = normalizeHttpServerUrl(host, port);
const devframe = initDevframe(def, {
base: basePath,
distDir: options.distDir,
app,
server,
host,
origin,
ws: options.ws,
allowedOrigins: options.allowedOrigins,
sse: options.sse,
auth: flags.auth === false ? false : options.auth,
mcp: options.mcp,
flags,
onPeerConnect: options.onPeerConnect,
onPeerDisconnect: options.onPeerDisconnect,
register: true,
destroyUnmatchedUpgrades: true
});
try {
await devframe.ready;
} catch (error) {
await new Promise((resolveClose) => server.close(() => resolveClose()));
throw error;
}
const internals = getInstanceInternals(devframe);
const transport = internals.started;
await options.onReady?.({
origin,
port,
app
});
await maybeOpenBrowser(def, flags, `${origin}${basePath}`, options.openBrowser, internals.authHandler);
return {
origin,
port,
app,
ws: transport.ws,
rpcGroup: transport.rpcGroup,
connectionMeta: transport.connectionMeta,
async close() {
await devframe.close();
await new Promise((resolveClose) => server.close(() => resolveClose()));
}
};
}
async function maybeOpenBrowser(def, flags, origin, override, authHandler) {
const flagsOpen = flags.open;
const cliOpen = def.cli?.open;
const resolved = override ?? flagsOpen ?? cliOpen;
if (resolved === void 0 || resolved === false) return;
const target = typeof resolved === "string" ? withBase(resolved, origin) : origin;
const authorizedTarget = authHandler?.buildOpenUrl?.(target) ?? target;
try {
await open(authorizedTarget);
} catch {}
}
//#endregion
export { getInstanceInternals as n, initDevframe as r, createDevServer as t };
import { n as __exportAll } from "./rolldown-runtime-JspESFgx.mjs";
import "node:fs/promises";
import { createServer } from "node:net";
import { networkInterfaces } from "node:os";
//#region ../../node_modules/.pnpm/get-port-please@3.2.0/node_modules/get-port-please/dist/index.mjs
var dist_exports = /* @__PURE__ */ __exportAll({
checkPort: () => checkPort,
getPort: () => getPort,
getRandomPort: () => getRandomPort,
isSafePort: () => isSafePort,
isUnsafePort: () => isUnsafePort
});
const unsafePorts = /* @__PURE__ */ new Set([
1,
7,
9,
11,
13,
15,
17,
19,
20,
21,
22,
23,
25,
37,
42,
43,
53,
69,
77,
79,
87,
95,
101,
102,
103,
104,
109,
110,
111,
113,
115,
117,
119,
123,
135,
137,
139,
143,
161,
179,
389,
427,
465,
512,
513,
514,
515,
526,
530,
531,
532,
540,
548,
554,
556,
563,
587,
601,
636,
989,
990,
993,
995,
1719,
1720,
1723,
2049,
3659,
4045,
5060,
5061,
6e3,
6566,
6665,
6666,
6667,
6668,
6669,
6697,
10080
]);
function isUnsafePort(port) {
return unsafePorts.has(port);
}
function isSafePort(port) {
return !isUnsafePort(port);
}
var GetPortError = class extends Error {
constructor(message, opts) {
super(message, opts);
this.message = message;
}
name = "GetPortError";
};
function _log(verbose, message) {
if (verbose) console.log(`[get-port] ${message}`);
}
function _generateRange(from, to) {
if (to < from) return [];
const r = [];
for (let index = from; index <= to; index++) r.push(index);
return r;
}
function _tryPort(port, host) {
return new Promise((resolve) => {
const server = createServer();
server.unref();
server.on("error", () => {
resolve(false);
});
server.listen({
port,
host
}, () => {
const { port: port2 } = server.address();
server.close(() => {
resolve(isSafePort(port2) && port2);
});
});
});
}
function _getLocalHosts(additional) {
const hosts = new Set(additional);
for (const _interface of Object.values(networkInterfaces())) for (const config of _interface || []) if (config.address && !config.internal && !config.address.startsWith("fe80::") && !config.address.startsWith("169.254")) hosts.add(config.address);
return [...hosts];
}
async function _findPort(ports, host) {
for (const port of ports) {
const r = await _tryPort(port, host);
if (r) return r;
}
}
function _fmtOnHost(hostname) {
return hostname ? `on host ${JSON.stringify(hostname)}` : "on any host";
}
const HOSTNAME_RE = /^(?!-)[\d.:A-Za-z-]{1,63}(?<!-)$/;
function _validateHostname(hostname, _public, verbose) {
if (hostname && !HOSTNAME_RE.test(hostname)) {
const fallbackHost = _public ? "0.0.0.0" : "127.0.0.1";
_log(verbose, `Invalid hostname: ${JSON.stringify(hostname)}. Using ${JSON.stringify(fallbackHost)} as fallback.`);
return fallbackHost;
}
return hostname;
}
async function getPort(_userOptions = {}) {
if (typeof _userOptions === "number" || typeof _userOptions === "string") _userOptions = { port: Number.parseInt(_userOptions + "") || 0 };
const _port = Number(_userOptions.port ?? process.env.PORT);
const _userSpecifiedAnyPort = Boolean(_userOptions.port || _userOptions.ports?.length || _userOptions.portRange?.length);
const options = {
random: _port === 0,
ports: [],
portRange: [],
alternativePortRange: _userSpecifiedAnyPort ? [] : [3e3, 3100],
verbose: false,
..._userOptions,
port: _port,
host: _validateHostname(_userOptions.host ?? process.env.HOST, _userOptions.public, _userOptions.verbose)
};
if (options.random && !_userSpecifiedAnyPort) return getRandomPort(options.host);
const portsToCheck = [
options.port,
...options.ports,
..._generateRange(...options.portRange)
].filter((port) => {
if (!port) return false;
if (!isSafePort(port)) {
_log(options.verbose, `Ignoring unsafe port: ${port}`);
return false;
}
return true;
});
if (portsToCheck.length === 0) portsToCheck.push(3e3);
let availablePort = await _findPort(portsToCheck, options.host);
if (!availablePort && options.alternativePortRange.length > 0) {
availablePort = await _findPort(_generateRange(...options.alternativePortRange), options.host);
if (portsToCheck.length > 0) {
let message = `Unable to find an available port (tried ${portsToCheck.join("-")} ${_fmtOnHost(options.host)}).`;
if (availablePort) message += ` Using alternative port ${availablePort}.`;
_log(options.verbose, message);
}
}
if (!availablePort && _userOptions.random !== false) {
availablePort = await getRandomPort(options.host);
if (availablePort) _log(options.verbose, `Using random port ${availablePort}`);
}
if (!availablePort) {
const triedRanges = [
options.port,
options.portRange.join("-"),
options.alternativePortRange.join("-")
].filter(Boolean).join(", ");
throw new GetPortError(`Unable to find an available port ${_fmtOnHost(options.host)} (tried ${triedRanges})`);
}
return availablePort;
}
async function getRandomPort(host) {
const port = await checkPort(0, host);
if (port === false) throw new GetPortError(`Unable to find a random port ${_fmtOnHost(host)}`);
return port;
}
async function checkPort(port, host = process.env.HOST, verbose) {
if (!host) host = _getLocalHosts([void 0, "0.0.0.0"]);
if (!Array.isArray(host)) return _tryPort(port, host);
for (const _host of host) {
const _port = await _tryPort(port, _host);
if (_port === false) {
if (port < 1024 && verbose) _log(verbose, `Unable to listen to the privileged port ${port} ${_fmtOnHost(_host)}`);
return false;
}
if (port === 0 && _port !== 0) port = _port;
}
return port;
}
//#endregion
export { getPort as n, dist_exports as t };
import { n as __exportAll } from "./rolldown-runtime-JspESFgx.mjs";
import { t as Diagnostic } from "./nostics-CzECRXpE.mjs";
import { i as isAllowedOrigin } from "./ws-server-BdSLrhxE.mjs";
import { t as diagnostics } from "./diagnostics-DOQPnQmX.mjs";
import { t as createHostContext } from "./context-CEe4PBz1.mjs";
import { t as toAgentToolName } from "./agent-tool-name-EgfoFO8C.mjs";
import { randomUUID } from "node:crypto";
import { join } from "pathe";
import process from "node:process";
import { homedir } from "node:os";
import { defineHandler } from "h3";
import { Server, WebStandardStreamableHTTPServerTransport, isInitializeRequest } from "@modelcontextprotocol/server";
//#region src/adapters/mcp/stringify.ts
/**
* JSON-coercing serializer for MCP text payloads.
*
* MCP carries tool results and resource reads as plain text over a
* JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
* format the WS RPC transport falls back to for non-JSON values. Instead,
* we coerce common non-JSON types into JSON-friendly forms so the LLM
* client sees something useful instead of `[object Object]`.
*
* Coercions:
* - `BigInt` → `"123n"`
* - `Date` → ISO string (via the native `toJSON`)
* - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
* - `Set` → `{ __type: 'Set', entries: [v, …] }`
* - `Error` → `{ name, message, stack, cause? }` (cause recurses)
* - `Function` → `"[Function: name]"`
* - `Symbol` → `value.toString()`
* - cycles → `"[Circular]"`
*/
function stringifyForMcp(value) {
if (value === void 0) return "undefined";
if (typeof value === "string") return value;
const seen = /* @__PURE__ */ new WeakSet();
return JSON.stringify(value, (_key, val) => {
if (typeof val === "bigint") return `${val}n`;
if (val instanceof Error) {
const out = {
name: val.name,
message: val.message,
stack: val.stack
};
if (val.cause !== void 0) out.cause = val.cause;
return out;
}
if (val instanceof Map) return {
__type: "Map",
entries: [...val.entries()]
};
if (val instanceof Set) return {
__type: "Set",
entries: [...val]
};
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
if (typeof val === "symbol") return val.toString();
if (val !== null && typeof val === "object") {
if (seen.has(val)) return "[Circular]";
seen.add(val);
}
return val;
}, 2);
}
/**
* Format a thrown value for an MCP `isError` text payload.
*
* A nostics `Diagnostic` (every coded devframe error) becomes structured
* JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
* the actionable next step (`fix`) and the docs URL instead of a bare
* message string. Other errors surface `Error.name`/`message`, plus one
* level of `cause.message` so context isn't dropped silently.
*/
function formatMcpError(error) {
if (error instanceof Diagnostic) return JSON.stringify({ error: {
code: error.code,
message: error.message,
...error.fix ? { fix: error.fix } : {},
...error.docs ? { docs: error.docs } : {}
} }, null, 2);
if (!(error instanceof Error)) return String(error);
const cause = error.cause;
const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
return `${error.name}: ${error.message}${causeText}`;
}
//#endregion
//#region src/adapters/mcp/to-json-schema.ts
const FALLBACK_OBJECT_SCHEMA = Object.freeze({
type: "object",
additionalProperties: true
});
/**
* Convert a Standard Schema to JSON Schema for the agent/MCP surface.
*
* Devframe stays validator-neutral, so conversion uses the schema's own
* [Standard JSON Schema](https://standardschema.dev/) converter
* (`~standard.jsonSchema`) when the validator provides one — zod 4 does,
* for example. Validators without a native converter (e.g. valibot) degrade
* to a permissive object schema rather than pulling in a converter library.
*/
function safeToJsonSchema(schema) {
const standard = schema["~standard"];
if (standard.jsonSchema) try {
return standard.jsonSchema.input({ target: "draft-2020-12" });
} catch {
return FALLBACK_OBJECT_SCHEMA;
}
return FALLBACK_OBJECT_SCHEMA;
}
/**
* JSON Schema for an RPC return value on the agent/MCP surface.
* @internal
*/
function returnToJsonSchema(schema) {
if (!schema) return void 0;
return safeToJsonSchema(schema);
}
/**
* JSON Schema for an RPC function's positional args on the agent/MCP
* surface. Each positional arg is advertised under `arg0` / `arg1` / … —
* matching how the agent bridge coerces the incoming object payload back
* into positional arguments.
*
* Returns `{ type: 'object', properties: {} }` when there are no args.
* @internal
*/
function argsToJsonSchema(args) {
if (!args || args.length === 0) return {
schema: {
type: "object",
properties: {}
},
unwrapped: false
};
const properties = {};
const required = [];
for (let i = 0; i < args.length; i++) {
const key = `arg${i}`;
properties[key] = safeToJsonSchema(args[i]);
required.push(key);
}
return {
schema: {
type: "object",
properties,
required,
additionalProperties: false
},
unwrapped: false
};
}
//#endregion
//#region src/adapters/mcp/build-server.ts
/**
* Wire an MCP {@link Server} to a devframe context. Returns the server
* plus a disposal function for the subscriptions it sets up. The
* transport is the caller's responsibility — `createMcpServer` connects
* stdio; tests can connect an {@link InMemoryTransport} instead.
*
* @internal
*/
function buildMcpServerFromContext(ctx, options) {
const server = new Server({
name: options.serverName,
version: options.serverVersion
}, { capabilities: {
tools: { listChanged: true },
resources: { listChanged: true }
} });
registerToolHandlers(server, ctx, options.exposeSharedState);
registerResourceHandlers(server, ctx, options.exposeSharedState);
const notify = (method) => {
server.notification({ method }).catch(() => {});
};
const offManifest = ctx.agent.events.on("agent:manifest:changed", () => {
notify("notifications/tools/list_changed");
notify("notifications/resources/list_changed");
});
const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
notify("notifications/resources/list_changed");
});
return {
server,
dispose: () => {
offManifest();
offKeyAdded();
}
};
}
/**
* Build an MCP server over the agent surface of a devframe definition.
* Currently supports `stdio` transport only.
*/
async function createMcpServer(definition, options = {}) {
const transport = options.transport ?? "stdio";
if (transport !== "stdio") throw diagnostics.DF0017({
transport,
reason: "Only stdio transport is supported in this release."
});
const ctx = await createHostContext({
cwd: process.cwd(),
mode: "dev",
host: {
mountStatic: () => {},
resolveOrigin: () => "mcp://devframe",
getStorageDir: (scope) => {
if (scope === "workspace") return join(process.cwd(), ".devframe");
if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
return join(homedir(), `.${definition.id}/devframe`);
}
}
});
await definition.setup(ctx);
const { server, dispose } = buildMcpServerFromContext(ctx, {
serverName: options.serverName ?? `${definition.id} (devframe)`,
serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
exposeSharedState: options.exposeSharedState ?? true
});
const { startStdioTransport } = await import("./transports-vhizgqXM.mjs");
let stop;
try {
stop = await startStdioTransport(server);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw diagnostics.DF0017({
transport,
reason,
cause: error
});
}
options.onReady?.({ transport: "stdio" });
return { async stop() {
dispose();
await stop();
} };
}
/**
* Id of the built-in shared-state read tool — namespaced like every other
* built-in (`devframe:<area>:<fn>`). Tool-shaped access matters because many
* MCP clients only consume tools — the parallel `devframe://state/<key>`
* resource projection stays for the clients that do read resources.
*/
const READ_STATE_TOOL = "devframe:state:read";
/** Wire name of the built-in shared-state read tool: `devframe_state_read`. */
const READ_STATE_NAME = toAgentToolName(READ_STATE_TOOL);
function sharedStateFilter(exposeSharedState) {
if (exposeSharedState === false) return void 0;
return typeof exposeSharedState === "function" ? exposeSharedState : () => true;
}
function readStateToolProjection() {
return {
name: READ_STATE_NAME,
title: "Read shared state",
description: "Read this devtool's live shared state. Call without arguments to list the available keys, then with a key to get that value as JSON. Safe to call freely.",
inputSchema: {
type: "object",
properties: { key: {
type: "string",
description: "A shared-state key from the key list. Omit to list all keys."
} }
},
annotations: {
title: "Read shared state",
readOnlyHint: true,
destructiveHint: false
}
};
}
async function readStateResult(ctx, filter, key) {
const keys = ctx.rpc.sharedState.keys().filter(filter);
if (key === void 0) return { keys };
if (!keys.includes(key)) throw diagnostics.DF0048({ key });
return {
key,
value: (await ctx.rpc.sharedState.get(key)).value()
};
}
function registerToolHandlers(server, ctx, exposeSharedState) {
const stateFilter = sharedStateFilter(exposeSharedState);
const warnedCollisions = /* @__PURE__ */ new Set();
/**
* Resolve a wire tool name back to the registered {@link AgentTool}.
* Wire-name matching runs first, in manifest order — the same tool the
* list projection advertises under that name — with a raw-id fallback so
* a colon-namespaced id keeps working as a call name.
*/
const resolveTool = (name) => {
return ctx.agent.list().tools.find((tool) => toAgentToolName(tool.id) === name) ?? ctx.agent.getTool(name);
};
server.setRequestHandler("tools/list", async () => {
const byName = /* @__PURE__ */ new Map();
for (const tool of ctx.agent.list().tools) {
const name = toAgentToolName(tool.id);
const existing = byName.get(name);
if (existing) {
if (!warnedCollisions.has(`${name}|${tool.id}`)) {
warnedCollisions.add(`${name}|${tool.id}`);
diagnostics.DF0047({
name,
id: tool.id,
existing: existing.id
});
}
continue;
}
byName.set(name, tool);
}
const tools = [...byName.entries()].map(([name, tool]) => projectTool(name, tool, ctx));
if (stateFilter && !byName.has(READ_STATE_NAME)) tools.push(readStateToolProjection());
return { tools };
});
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
try {
const tool = resolveTool(name);
if (stateFilter && !tool && (name === READ_STATE_NAME || name === READ_STATE_TOOL)) {
const key = args?.key;
const result = await readStateResult(ctx, stateFilter, key);
return {
content: [{
type: "text",
text: stringifyForMcp(result)
}],
structuredContent: result
};
}
const outputSchema = tool ? usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx)) : void 0;
const result = await ctx.agent.invoke(tool?.id ?? name, args ?? {});
return {
content: [{
type: "text",
text: stringifyForMcp(result)
}],
...outputSchema ? { structuredContent: result } : {}
};
} catch (error) {
return {
isError: true,
content: [{
type: "text",
text: `Error invoking "${name}": ${formatMcpError(error)}`
}]
};
}
});
}
function registerResourceHandlers(server, ctx, exposeSharedState) {
server.setRequestHandler("resources/list", async () => {
const resources = ctx.agent.list().resources.map((resource) => ({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType
}));
if (exposeSharedState !== false) {
const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
for (const key of ctx.rpc.sharedState.keys()) {
if (!filter(key)) continue;
resources.push({
uri: `devframe://state/${encodeURIComponent(key)}`,
name: key,
description: `Shared state: ${key}`,
mimeType: "application/json"
});
}
}
return { resources };
});
server.setRequestHandler("resources/read", async (request) => {
const { uri } = request.params;
const parsed = parseResourceUri(uri);
if (parsed.kind === "resource") {
const content = await ctx.agent.read(parsed.id);
return { contents: [{
uri,
mimeType: content.mimeType ?? "application/json",
text: content.text ?? stringifyForMcp(content.json)
}] };
}
if (parsed.kind === "state") return { contents: [{
uri,
mimeType: "application/json",
text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
}] };
throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
});
}
/**
* MCP constrains a tool's `outputSchema` to a JSON Schema of `type:
* "object"` — clients (the SDK included) reject anything else. Non-object
* return schemas (e.g. a schema for `void` / a bare string) simply project
* no output schema; the text content still carries the result.
*/
function usableOutputSchema(schema) {
return schema && typeof schema === "object" && schema.type === "object" ? schema : void 0;
}
function projectTool(name, tool, ctx) {
const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
const outputSchema = usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx));
return {
name,
title: tool.title,
description: tool.description,
inputSchema,
...outputSchema ? { outputSchema } : {},
annotations: {
title: tool.title,
readOnlyHint: tool.safety === "read",
destructiveHint: tool.safety === "destructive"
}
};
}
function computeInputSchema(tool, ctx) {
if (tool.kind === "tool") return argsToJsonSchema(tool.args).schema;
if (tool.kind !== "rpc" || !tool.rpcName) return {
type: "object",
properties: {}
};
const def = ctx.rpc.definitions.get(tool.rpcName);
if (!def) return {
type: "object",
properties: {}
};
const args = def.args;
return argsToJsonSchema(args).schema;
}
function computeOutputSchema(tool, ctx) {
if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
const def = ctx.rpc.definitions.get(tool.rpcName);
if (!def) return void 0;
return returnToJsonSchema(def.returns);
}
function parseResourceUri(uri) {
const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
if (!match) return { kind: "unknown" };
const [, kind, rest] = match;
const decoded = decodeURIComponent(rest);
if (kind === "resource") return {
kind: "resource",
id: decoded
};
return {
kind: "state",
key: decoded
};
}
//#endregion
//#region src/adapters/mcp/fetch.ts
/**
* Build a framework-agnostic MCP Streamable-HTTP endpoint over a devframe
* context: a web-standard `Request → Response` handler any host can mount —
* h3 (see `mountMcpHttp`), a Next.js App Router route, or any other
* fetch-shaped server.
*
* Each MCP session gets its own {@link WebStandardStreamableHTTPServerTransport}
* and MCP server (built from the shared, live `ctx` via
* `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
* `initialize` POST spins up a session; later requests route to it; a `DELETE`
* (or client disconnect) tears it down. The origin gate guards every request:
* loopback-default DNS-rebinding protection that — unlike the WS upgrade's
* `isAllowedOrigin` — also rejects `Origin`-less requests, so a route-based
* endpoint isn't reachable by an arbitrary local process.
*/
function createMcpFetchHandler(ctx, options) {
const sessions = /* @__PURE__ */ new Map();
const allowedOrigins = options.allowedOrigins;
function drop(sessionId) {
const session = sessions.get(sessionId);
if (!session) return;
sessions.delete(sessionId);
session.dispose();
}
async function createSession() {
let session;
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
sessions.set(id, session);
},
onsessionclosed: (id) => {
drop(id);
}
});
const { server, dispose } = buildMcpServerFromContext(ctx, {
serverName: options.serverName,
serverVersion: options.serverVersion,
exposeSharedState: options.exposeSharedState
});
session = {
transport,
dispose: async () => {
dispose();
await server.close();
}
};
transport.onclose = () => {
if (transport.sessionId) drop(transport.sessionId);
};
await server.connect(transport);
return session;
}
async function handle(req) {
const origin = req.headers.get("origin") ?? void 0;
if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
const sessionId = req.headers.get("mcp-session-id") ?? void 0;
let session = sessionId ? sessions.get(sessionId) : void 0;
if (!session && req.method === "POST") {
let body;
try {
body = await req.json();
} catch {
body = void 0;
}
if (!sessionId && isInitializeRequest(body)) session = await createSession();
else return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: no valid session ID and not an initialize request", { status: sessionId ? 404 : 400 });
return session.transport.handleRequest(req, { parsedBody: body });
}
if (!session) return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: missing MCP session ID", { status: sessionId ? 404 : 400 });
return session.transport.handleRequest(req);
}
return {
fetch: handle,
dispose: async () => {
const live = [...sessions.values()];
sessions.clear();
await Promise.all(live.map((session) => session.dispose()));
}
};
}
//#endregion
//#region src/adapters/mcp/http.ts
var http_exports = /* @__PURE__ */ __exportAll({ mountMcpHttp: () => mountMcpHttp });
/**
* Mount an MCP Streamable-HTTP endpoint on an h3 app at `path` — the h3
* binding over {@link createMcpFetchHandler}, which owns the sessions, the
* origin gate, and the transport plumbing.
*
* The handler is web-standard — it takes the h3 event's web `Request` and
* returns a web `Response` (an SSE `ReadableStream` body for the
* server→client stream). We copy that response onto `event.res` and return
* its body rather than returning the `Response` object directly, so a
* legitimate MCP 404 (unknown session) isn't swallowed by h3's
* "Response-with-404 falls through to the next handler" rule (which would
* otherwise hand the request to the SPA static catch-all).
*/
function mountMcpHttp(app, ctx, path, options) {
const handler = createMcpFetchHandler(ctx, options);
app.use(path, defineHandler(async (event) => respond(event, await handler.fetch(event.req))));
return { dispose: handler.dispose };
}
/**
* Copy a web `Response` from the MCP transport onto the h3 event's response
* and return its body. Returning the body (a `ReadableStream` or `null`)
* rather than the `Response` object avoids h3's 404-fall-through behavior.
*/
function respond(event, response) {
event.res.status = response.status;
event.res.statusText = response.statusText;
response.headers.forEach((value, key) => {
event.res.headers.set(key, value);
});
return response.body ?? "";
}
//#endregion
export { createMcpServer as i, mountMcpHttp as n, createMcpFetchHandler as r, http_exports as t };
import "./constants.mjs";
import { t as diagnostics } from "./diagnostics-DOQPnQmX.mjs";
import { t as getInternalContext } from "./context-xdynGSLP.mjs";
import { createInteractiveAuth } from "./recipes/interactive-auth.mjs";
import { createServer } from "node:http";
import process from "node:process";
import { isIP } from "node:net";
import { joinURL, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo";
import { H3, defineHandler, toNodeHandler } from "h3";
//#region src/node/utils.ts
const NON_DIALABLE_HOSTS = /* @__PURE__ */ new Set([
"0.0.0.0",
"127.0.0.1",
"::",
"0000:0000:0000:0000:0000:0000:0000:0000",
""
]);
/** Map a bind host to a host a client can actually connect to. */
function toDialableHost(host) {
return NON_DIALABLE_HOSTS.has(host) ? "localhost" : host;
}
/** Format a bind host for use in a URL authority (dialable, IPv6-bracketed). */
function formatHostForUrl(host) {
const dialable = toDialableHost(host);
return isIP(dialable) === 6 ? `[${dialable}]` : dialable;
}
function normalizeHttpServerUrl(host, port) {
return `http://${formatHostForUrl(host)}:${port}`;
}
//#endregion
//#region src/node/instance-shell.ts
/**
* Compose an h3 + WebSocket RPC server for a devframe context — the low-level
* "listen on a port (or share one) + attach the WS transport" binding the
* side-car and shared-server tiers below are built on. Owns and listens on a
* fresh `node:http` server unless `server` is supplied, in which case it only
* attaches the upgrade listener and leaves that server's lifecycle to its
* owner.
*/
async function bindHttpAndWs(options) {
const { context, port, core } = options;
const bindHost = options.host;
const app = new H3();
const ownsHttpServer = !options.server;
const httpServer = options.server ?? createServer(toNodeHandler(app));
const rpcHost = context.rpc;
const websocket = options.websocket !== false;
let ws;
let closeWs = async () => {};
if (websocket) {
const { attachWsRpcTransport } = await import("./rpc/transports/ws-server.mjs");
const transport = attachWsRpcTransport(core.rpcGroup, {
server: httpServer,
path: options.path,
destroyUnmatched: options.destroyUnmatched ?? ownsHttpServer,
allowedOrigins: options.allowedOrigins,
onConnected: core.onConnected,
onDisconnected: core.onDisconnected
});
ws = transport.ws;
closeWs = transport.close;
}
if (ownsHttpServer) try {
await new Promise((resolve, reject) => {
const onError = (error) => reject(error);
httpServer.once("error", onError);
httpServer.listen(port, bindHost, () => {
httpServer.removeListener("error", onError);
resolve();
});
});
} catch (error) {
await closeWs().catch(() => {});
throw diagnostics.DF0052({
host: bindHost,
port,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
const address = httpServer.address();
const resolvedPort = typeof address === "object" && address ? address.port : port;
const origin = normalizeHttpServerUrl(bindHost, resolvedPort);
const internal = getInternalContext(context);
const wsUrl = `ws://${formatHostForUrl(bindHost)}:${resolvedPort}${options.path ?? ""}`;
if (websocket) internal.setWsEndpoint({ url: wsUrl });
function connectionMeta() {
const jsonSerializableMethods = [];
for (const def of rpcHost.definitions.values()) if (def.jsonSerializable === true) jsonSerializableMethods.push(def.name);
return {
backend: "websocket",
websocket: { path: options.path },
jsonSerializableMethods
};
}
return {
origin,
port: resolvedPort,
app,
ws,
rpcGroup: core.rpcGroup,
connectionMeta,
async close() {
await closeWs();
if (ownsHttpServer) await new Promise((r) => httpServer.close(() => r()));
if (websocket && getInternalContext(context).wsEndpoint?.url === wsUrl) getInternalContext(context).setWsEndpoint(void 0);
}
};
}
/**
* Translate the public `register?: boolean | Partial<DevframeInstanceRecord>`
* option into a shell {@link InstanceRegisterConfig}, or `undefined` when
* registration is opted out. The object form supplies record overrides on top
* of the caller-provided identity defaults.
*/
function resolveInstanceRegister(option, defaults) {
if (!option) return void 0;
return {
id: defaults.id,
...defaults.name !== void 0 ? { name: defaults.name } : {},
...defaults.rootDir !== void 0 ? { rootDir: defaults.rootDir } : {},
...typeof option === "object" ? { overrides: option } : {}
};
}
/** Compare two URL paths ignoring a trailing slash. */
function samePath(a, b) {
return withoutTrailingSlash(a) === withoutTrailingSlash(b);
}
/**
* Copy a web `Response` from a fetch-style transport handler onto the h3
* event's response and return its body — mirroring the MCP route's bridge.
* Returning the body (a `ReadableStream`, or `''` for an empty one — h3
* middleware only falls through on `undefined`) terminates the chain with
* the status/headers set here instead of continuing to the SPA catch-all.
*/
function respondWith(event, response) {
event.res.status = response.status;
event.res.statusText = response.statusText;
response.headers.forEach((value, key) => {
event.res.headers.set(key, value);
});
return response.body ?? "";
}
/**
* The shared machinery behind `initDevframe` and `initHub`: one mount base,
* one h3 app, one lazily-derived public origin (and the auth banner that waits
* for it), one WebSocket binding, and the fetch / connect-middleware pair that
* serves them. Each factory supplies only what makes it itself — its context,
* its routes, its diagnostics — through `init` / `mount`.
*
* Nothing here listens on a port unless a side-car was explicitly requested:
* the default tier leaves the socket `unbound`, so a host chains it onto its
* own server through {@link InstanceShell.attach} /
* {@link InstanceShell.handleUpgrade}.
*
* @internal
*/
function createInstanceShell(options) {
const base = options.base;
const baseNoSlash = withoutTrailingSlash(base);
const app = options.app ?? new H3();
const wsDisabled = options.ws === false;
const ws = options.ws === false ? {} : options.ws ?? {};
const route = withoutLeadingSlash(ws.route ?? "__ws");
/** Where an upgrade lands on the host's own origin. */
const routePath = joinURL(base, route);
/** What `__connection.json` advertises for a same-origin socket. */
const advertisedPath = options.absoluteWsPath ? routePath : route;
const sidecarRequested = ws.port != null || ws.sidecar === true;
const tier = wsDisabled ? "disabled" : sidecarRequested ? "sidecar" : options.server ? "server" : ws.url ? "external" : "unbound";
const sseEnabled = options.sse !== false && tier !== "external";
const sseRoute = withoutLeadingSlash((typeof options.sse === "object" ? options.sse.route : void 0) ?? "__sse");
const sseRoutePath = joinURL(base, sseRoute);
const advertisedSsePath = options.absoluteWsPath ? sseRoutePath : sseRoute;
let derivedOrigin;
function currentOrigin() {
return (typeof options.origin === "function" ? options.origin() : options.origin) || derivedOrigin;
}
let authHandler;
let bannerPrinted = false;
function maybePrintBanner() {
if (bannerPrinted || !authHandler || !currentOrigin()) return;
bannerPrinted = true;
authHandler.printBanner();
}
let meta;
let registration;
let registerPromise;
/**
* Publish the instance in the global registry the moment both its origin
* and connection meta are known — at init end for a pinned origin, or on
* the first request for a derived one. Registration never throws (the
* registry writer degrades to a coded warning), so failures never surface.
*/
function maybeRegister() {
const cfg = options.register;
const origin = currentOrigin();
if (!cfg || registerPromise || !origin || !meta) return;
const resolvedMeta = meta;
registerPromise = import("./instance-registry-WvQkt42E.mjs").then((n) => n.t).then(({ registerDevframeInstance }) => {
let port = 0;
try {
const url = new URL(origin);
port = Number(url.port) || (url.protocol === "https:" ? 443 : 80);
} catch {}
registration = registerDevframeInstance({
pid: process.pid,
port,
origin,
basePath: base,
id: cfg.id,
...cfg.name !== void 0 ? { name: cfg.name } : {},
rootDir: cfg.rootDir ?? process.cwd(),
mcp: resolvedMeta.mcp ? { path: joinURL(base, resolvedMeta.mcp.path) } : null,
startedAt: Date.now(),
...cfg.overrides
});
}).catch(() => {});
}
function noteOrigin(origin) {
derivedOrigin ??= origin;
maybePrintBanner();
maybeRegister();
}
let started;
let transport;
let dispose;
let ctx;
const api = {
base,
app,
origin: currentOrigin,
connectionMeta: () => meta
};
/**
* Auth resolution: gate by default, `false` opts out, a handler object
* installs a custom scheme. The `external` tier has no local transport to
* gate — the server behind `ws.url` owns auth — so it resolves to nothing.
*/
function resolveAuth() {
if (options.auth === false) return false;
if (typeof options.auth === "object") {
authHandler = options.auth;
return options.auth;
}
authHandler = createInteractiveAuth(ctx);
return authHandler;
}
/**
* The context's RPC core (birpc group, session lifecycle, auth gate) —
* one per instance, shared by every transport binding (WS and SSE), so a
* WS peer and an SSE session live in the same session/broadcast space.
* Built lazily: an `unbound` host that never wires a transport pays
* nothing for it, not even the imports. `resolvedAuth` and `ctx` are
* assigned during `init()` before any caller can reach this.
*/
let resolvedAuth = false;
let corePromise;
function ensureCore() {
corePromise ??= import("./rpc-core-DMTe-hLo.mjs").then((n) => n.n).then(({ createContextRpcServer }) => createContextRpcServer({
context: ctx,
auth: resolvedAuth,
onPeerConnect: options.onPeerConnect,
onPeerDisconnect: options.onPeerDisconnect
}));
return corePromise;
}
/**
* The SSE transport, built on the first request to its route so an
* instance nobody dials over SSE never loads it.
*/
let ssePromise;
function ensureSse() {
ssePromise ??= (async () => {
const [core, { attachSseRpcTransport }] = await Promise.all([ensureCore(), import("./rpc/transports/sse-server.mjs")]);
return attachSseRpcTransport(core.rpcGroup, {
allowedOrigins: options.allowedOrigins,
onConnected: core.onConnected,
onDisconnected: core.onDisconnected
});
})();
return ssePromise;
}
/**
* A side-car server on its own port. `getPort` probes and the bind can
* still race (or disagree across the v4/v6 duals of `localhost`), so an
* auto-port side-car retries on a fresh random port instead of failing
* init; a pinned `ws.port` is honored as given and fails loudly.
*/
async function startSidecar(core) {
const sidecarHost = options.host ?? "localhost";
const start = (port) => bindHttpAndWs({
context: ctx,
core,
host: sidecarHost,
port,
path: withLeadingSlash(route),
allowedOrigins: options.allowedOrigins
});
if (ws.port != null) return await start(ws.port);
const { getPort } = await import("./dist-eNI9qM_I.mjs").then((n) => n.t);
let lastError;
for (let attempt = 0; attempt < 3; attempt++) {
const port = attempt === 0 && options.resolveSidecarPort ? await options.resolveSidecarPort(sidecarHost) : await getPort({
random: true,
host: sidecarHost
});
try {
return await start(port);
} catch (error) {
lastError = error;
}
}
throw lastError;
}
async function init() {
const result = await options.init(api);
ctx = result.context;
dispose = result.dispose;
resolvedAuth = tier === "external" ? false : resolveAuth();
let websocketMeta;
if (tier === "sidecar") {
started = await startSidecar(await ensureCore());
websocketMeta = {
port: started.port,
path: route
};
} else if (tier === "server") {
started = await bindHttpAndWs({
context: ctx,
core: await ensureCore(),
host: options.host ?? "localhost",
port: 0,
server: options.server,
path: routePath,
allowedOrigins: options.allowedOrigins,
destroyUnmatched: options.destroyUnmatchedUpgrades
});
websocketMeta = { path: advertisedPath };
} else if (tier === "external") websocketMeta = ws.url;
else if (tier === "unbound") websocketMeta = { path: advertisedPath };
if (!wsDisabled && ws.url) websocketMeta = ws.url;
if (sseEnabled) app.use(sseRoutePath, defineHandler(async (event) => respondWith(event, await (await ensureSse()).handler(event.req))));
meta = {
backend: wsDisabled ? sseEnabled ? "sse" : "none" : "websocket",
...websocketMeta !== void 0 ? { websocket: websocketMeta } : {},
...sseEnabled ? { sse: { path: advertisedSsePath } } : {},
...result.mcp ? { mcp: result.mcp } : {}
};
if (Object.keys(ctx.staticConfig).length > 0) meta.configs = ctx.staticConfig;
await options.mount?.(ctx, meta, api);
maybePrintBanner();
maybeRegister();
}
const initPromise = init();
initPromise.catch(() => {});
const contextPromise = initPromise.then(() => ctx);
contextPromise.catch(() => {});
/**
* The `unbound` tier: the RPC core and its crossws adapter, bound to
* nothing. Built on the first `attach` / `handleUpgrade` — a host that
* never wires the socket (or whose runtime brings its own WS transport)
* pays nothing for it, not even the adapter's imports.
*/
let transportPromise;
function ensureTransport() {
transportPromise ??= initPromise.then(async () => {
const [core, { attachWsRpcTransport }] = await Promise.all([ensureCore(), import("./rpc/transports/ws-server.mjs")]);
transport = attachWsRpcTransport(core.rpcGroup, {
unbound: true,
path: routePath,
allowedOrigins: options.allowedOrigins,
onConnected: core.onConnected,
onDisconnected: core.onDisconnected
});
return transport;
});
return transportPromise;
}
async function handleRequest(request) {
await initPromise;
noteOrigin(new URL(request.url).origin);
const response = await app.fetch(request);
if (response.status === 404) return new Response(null, { status: 404 });
return response;
}
let nodeHandler;
function nodeMiddleware(req, res, next) {
let pathname = req.url ?? "/";
try {
pathname = new URL(pathname, "http://localhost").pathname;
} catch {}
if (!(samePath(pathname, baseNoSlash) || pathname.startsWith(base))) {
if (next) {
next();
return;
}
res.statusCode = 404;
res.end();
return;
}
initPromise.then(async () => {
const host = req.headers.host;
if (host) {
const encrypted = req.socket.encrypted;
noteOrigin(`${encrypted ? "https" : "http"}://${host}`);
}
if (!nodeHandler) {
const { toNodeHandler } = await import("h3/node");
nodeHandler = toNodeHandler(app);
}
return nodeHandler(req, res);
}).catch((err) => {
if (next) {
next(err);
return;
}
res.statusCode = 500;
res.end();
});
}
/** The `unbound` tier is the only one whose socket the host may drive. */
function assertUnbound() {
if (tier === "disabled") throw diagnostics.DF0057();
if (tier === "external") throw diagnostics.DF0056({ url: ws.url });
if (tier !== "unbound") throw diagnostics.DF0055({ tier });
}
/**
* Publish the socket's absolute URL on the context, so surfaces that hand
* out a complete endpoint (the hub's remote docks) work on this tier too.
* {@link bindHttpAndWs} does the same for the tiers it owns.
*/
function publishWsEndpoint(server) {
const record = () => {
const address = server.address();
if (typeof address !== "object" || !address) return;
const host = options.host ?? (address.address === "::" || address.address === "0.0.0.0" ? "localhost" : address.address);
getInternalContext(ctx).setWsEndpoint({ url: `ws://${formatHostForUrl(host)}:${address.port}${routePath}` });
};
if (server.listening) record();
else server.once("listening", record);
}
function handleUpgrade(req, socket, head) {
assertUnbound();
if (transport) {
transport.handleUpgrade(req, socket, head);
return;
}
ensureTransport().then((live) => live.handleUpgrade(req, socket, head)).catch(() => socket.destroy());
}
function attach(server) {
assertUnbound();
server.on("upgrade", handleUpgrade);
ensureTransport().then(() => publishWsEndpoint(server)).catch(() => {});
return () => server.off("upgrade", handleUpgrade);
}
return {
base,
handler: handleRequest,
nodeMiddleware,
ready: initPromise,
context: contextPromise,
connectionMeta: () => meta ?? options.onMetaUnavailable(),
handleUpgrade,
attach,
async close() {
await initPromise.catch(() => {});
await registerPromise?.catch(() => {});
registration?.unregister();
await dispose?.();
await ssePromise?.then((live) => live.close()).catch(() => {});
await started?.close();
await transportPromise?.then((live) => live.close()).catch(() => {});
},
internals: {
get started() {
return started;
},
get authHandler() {
return authHandler;
}
}
};
}
//#endregion
export { normalizeHttpServerUrl as i, resolveInstanceRegister as n, samePath as r, createInstanceShell as t };
//#region ../../node_modules/.pnpm/get-port-please@3.2.0/node_modules/get-port-please/dist/index.d.mts
interface GetPortOptions {
name: string;
random: boolean;
port: number;
ports: number[];
portRange: [fromInclusive: number, toInclusive: number];
alternativePortRange: [fromInclusive: number, toInclusive: number];
host: string;
verbose?: boolean;
public?: boolean;
}
type GetPortInput = Partial<GetPortOptions> | number | string;
type PortNumber = number;
declare function getPort(_userOptions?: GetPortInput): Promise<PortNumber>;
//#endregion
export { type GetPortInput, type GetPortOptions, getPort };
import { n as getPort } from "../dist-eNI9qM_I.mjs";
export { getPort };
import { n as strictJsonStringify } from "./serialization-BGzEwAdr.mjs";
import { n as structuredCloneStringify, t as structuredCloneParse } from "./structured-clone-CbAV5rFI.mjs";
//#region src/rpc/wire-codec.ts
const EMPTY_WIRE_DEFS = /* @__PURE__ */ new Map();
/**
* Build the per-connection wire codec every live transport (WS server, WS
* client, SSE server, SSE client) shares: per-method dispatch between strict
* JSON (methods declared `jsonSerializable: true`) and `s:`-prefixed
* structured-clone (everything else, including all error envelopes), with a
* request-id → method map so a response independently picks the same
* encoder as its request. One codec per connection — request-id spaces
* don't collide across connections.
*
* @internal
*/
function createRpcWireCodec(definitions = EMPTY_WIRE_DEFS) {
const pendingRequestMethods = /* @__PURE__ */ new Map();
return {
serialize: (msg) => {
let method;
if (msg.t === "q") method = msg.m;
else {
method = pendingRequestMethods.get(msg.i);
pendingRequestMethods.delete(msg.i);
}
if (!(msg.t === "s" && "e" in msg) && !!method && definitions.get(method)?.jsonSerializable === true) return strictJsonStringify(msg, method ?? "");
return `s:${structuredCloneStringify(msg)}`;
},
deserialize: (raw) => {
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
if (msg.t === "q" && msg.i && msg.m) pendingRequestMethods.set(msg.i, msg.m);
return msg;
}
};
}
/**
* Peek at a wire frame's birpc envelope without engaging a codec's
* request-id bookkeeping — used by the SSE transport to route a frame
* (park a POST for its response / answer with a bare 202) before it is
* handed to birpc proper.
*
* @internal
*/
function peekRpcWireFrame(raw) {
try {
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
return {
t: msg?.t,
i: msg?.i
};
} catch {
return {};
}
}
//#endregion
export { peekRpcWireFrame as n, createRpcWireCodec as t };
import "./constants.mjs";
import { t as createRpcWireCodec } from "./wire-codec-0K-o5MYW.mjs";
import { n as randomToken, r as timingSafeEqual } from "./crypto-token-XCqTSMg9.mjs";
import { createServer } from "node:http";
import { createServer as createServer$1 } from "node:https";
import crossws from "crossws/adapters/node";
//#region src/rpc/transports/session.ts
let sessionId = 0;
/**
* Mint the per-connection session meta every transport binding shares —
* one id space across transports, so session bookkeeping (streaming
* subscriptions, shared-state sync, auth trust) never collides between a
* WS peer and an SSE session on the same server.
*/
function createRpcSessionMeta() {
return {
id: sessionId++,
subscribedStates: /* @__PURE__ */ new Set()
};
}
//#endregion
//#region src/rpc/transports/ws-server.ts
/**
* Create a live, token-protected origin allowlist for external browser
* viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
* `registerFromUrl()` in the connection metadata handler to authorize a
* viewer without sharing a mutable array or disabling DNS-rebinding protection.
*/
function createWsOriginRegistry(options = {}) {
const token = randomToken();
const origins = new Set(options.allowedOrigins ?? []);
function normalizeOrigin(origin) {
if (!origin) return;
try {
const url = new URL(origin);
const normalized = url.origin === "null" ? `${url.protocol}//${url.host}` : url.origin;
return origin === normalized ? normalized : void 0;
} catch {}
}
function registerOrigin(origin, candidateToken) {
const normalized = normalizeOrigin(origin);
if (!normalized || !candidateToken || !timingSafeEqual(token, candidateToken)) return false;
if (options.validateOrigin && !options.validateOrigin(normalized)) return false;
origins.add(normalized);
return true;
}
return {
token,
registerFromUrl(url) {
let parsed;
try {
parsed = new URL(url, "http://localhost");
} catch {
return;
}
const origin = parsed.searchParams.get("devframe_viewer_origin") ?? void 0;
return registerOrigin(origin, parsed.searchParams.get("devframe_viewer_origin_token") ?? void 0) ? origin : void 0;
},
isAllowed(origin) {
return isAllowedOrigin(origin, [...origins]);
}
};
}
const EMPTY_DEFS = /* @__PURE__ */ new Map();
function NOOP() {}
function listen(server, port, host) {
return new Promise((resolve, reject) => {
const onError = (error) => reject(error);
server.once("error", onError);
try {
server.listen(port, host, () => {
server.off("error", onError);
resolve();
});
} catch (error) {
server.off("error", onError);
reject(error);
}
});
}
/** Compare two URL paths ignoring a trailing slash. */
function pathMatches(a, b) {
const strip = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
return strip(a) === strip(b);
}
function isLoopbackHostname(hostname) {
const h = hostname.replace(/^\[|\]$/g, "");
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost") || h.startsWith("127.");
}
/**
* Default origin policy for a localhost dev tool: allow requests with no
* `Origin` header (native, non-browser clients), allow any loopback origin
* (so cross-port localhost dev setups keep working), and allow explicitly
* configured origins. Everything else — a real remote page in the dev's
* browser — is rejected.
*/
function isAllowedOrigin(origin, allowedOrigins) {
if (!origin) return true;
if (allowedOrigins.includes(origin)) return true;
try {
return isLoopbackHostname(new URL(origin).hostname);
} catch {
return false;
}
}
function isWsOriginRegistry(value) {
return !!value && !Array.isArray(value);
}
/**
* Build the `upgrade` listener that hands a request to the crossws adapter,
* optionally filtered to a single `path`. Non-matching requests are left
* untouched so other upgrade listeners (e.g. a Vite dev server's HMR socket)
* can claim them, unless `destroyUnmatched` is set.
*/
function createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins) {
return (req, socket, head) => {
socket.on("error", () => {});
if (path) {
let pathname = req.url ?? "/";
try {
pathname = new URL(req.url ?? "/", "http://localhost").pathname;
} catch {}
if (!pathMatches(pathname, path)) {
if (destroyUnmatched) {
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
socket.destroy();
}
return;
}
}
const originAllowed = isWsOriginRegistry(allowedOrigins) ? allowedOrigins.isAllowed(req.headers.origin) : isAllowedOrigin(req.headers.origin, allowedOrigins || []);
if (allowedOrigins !== false && !originAllowed) {
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
socket.destroy();
return;
}
ws.handleUpgrade(req, socket, head);
};
}
/**
* The per-peer lifecycle hooks driving a devframe RPC WebSocket, shaped for
* any [crossws](https://crossws.h3.dev) adapter. {@link attachWsRpcTransport}
* feeds them to the Node adapter; runtime-specific attachments (e.g. Bun's
* fetch-upgrade adapter) reuse the same hooks so every transport speaks the
* identical wire protocol — one birpc channel per peer, per-method
* `jsonSerializable` dispatch between strict JSON and structured-clone.
*/
function createWsRpcPeerHooks(rpcGroup, options = {}) {
const { onConnected = NOOP, onDisconnected = NOOP, definitions = EMPTY_DEFS, serialize: serializeOverride, deserialize: deserializeOverride } = options;
const states = /* @__PURE__ */ new WeakMap();
return {
open: (peer) => {
const meta = createRpcSessionMeta();
meta.peer = peer;
const connection = {
id: meta.id,
transport: "websocket",
request: peer.request,
send: (data) => peer.send(data),
close: (code, reason) => peer.close(code, reason),
peer
};
const codec = createRpcWireCodec(definitions);
const state = {
meta,
connection,
channel: void 0
};
const channel = {
post: (data) => {
peer.send(data);
},
on: (fn) => {
state.onMessage = fn;
},
serialize: serializeOverride ?? codec.serialize,
deserialize: deserializeOverride ?? codec.deserialize,
meta
};
state.channel = channel;
states.set(peer, state);
rpcGroup.updateChannels((channels) => {
channels.push(channel);
});
onConnected(connection, meta);
},
message: (peer, message) => {
states.get(peer)?.onMessage?.(message.text());
},
close: (peer) => {
const state = states.get(peer);
if (!state) return;
states.delete(peer);
rpcGroup.updateChannels((channels) => {
const index = channels.indexOf(state.channel);
if (index >= 0) channels.splice(index, 1);
});
onDisconnected(state.connection, state.meta);
}
};
}
/**
* Attach a WebSocket transport to an existing RPC group, powered by
* [crossws](https://crossws.h3.dev). Either attach to an existing HTTP(S)
* `server` (sharing its port, optionally scoped to a `path`), or let this
* helper create a standalone server from `port` / `host` / `https`.
*
* Returns the crossws node adapter, standalone-server readiness/address
* accessors, `detach` (remove the upgrade listener from a shared `server`),
* and `close` (full deterministic teardown).
*/
function attachWsRpcTransport(rpcGroup, options = {}) {
const { server, port, host = "localhost", path, destroyUnmatched = false, unbound, https, allowedOrigins } = options;
const ws = crossws({ hooks: createWsRpcPeerHooks(rpcGroup, options) });
const sharedUpgradeListener = createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins);
const ownedUpgradeListener = createUpgradeListener(ws, path, true, allowedOrigins);
/** Bind a server's `upgrade` events, tracked so `close()` detaches them. */
const attachments = /* @__PURE__ */ new Set();
function attachTo(target, listener) {
target.on("upgrade", listener);
const detachOne = () => {
target.off("upgrade", listener);
attachments.delete(detachOne);
};
attachments.add(detachOne);
return detachOne;
}
let ready = Promise.resolve();
let ownedServer;
if (unbound) {} else if (server) attachTo(server, sharedUpgradeListener);
else if (https) {
ownedServer = createServer$1(https);
attachTo(ownedServer, ownedUpgradeListener);
ready = listen(ownedServer, port ?? 0, host);
} else {
ownedServer = createServer((_req, res) => {
res.writeHead(426, { "content-type": "text/plain" });
res.end("Upgrade Required");
});
attachTo(ownedServer, ownedUpgradeListener);
ready = listen(ownedServer, port ?? 0, host);
}
const activeServer = server ?? ownedServer;
function detachAll() {
for (const detachOne of [...attachments]) detachOne();
}
return {
ws,
ready,
address: () => activeServer?.address() ?? null,
handleUpgrade: sharedUpgradeListener,
attach: (target) => attachTo(target, sharedUpgradeListener),
detach: detachAll,
async close() {
detachAll();
ws.closeAll(void 0, void 0, true);
if (ownedServer) {
const srv = ownedServer;
await ready.catch(() => {});
if (!srv.listening) return;
await new Promise((r) => srv.close(() => r()));
}
}
};
}
//#endregion
export { isLoopbackHostname as a, isAllowedOrigin as i, createWsOriginRegistry as n, createRpcSessionMeta as o, createWsRpcPeerHooks as r, attachWsRpcTransport as t };
+2
-2
import { s as colors } from "../nostics-CzECRXpE.mjs";
import { r as resolveDevServerPort } from "../_shared-CKZPvSrN.mjs";
import { r as resolveDevServerPort } from "../_shared-CbZ_yhbz.mjs";
import { createBuild } from "./build.mjs";
import { t as createDevServer } from "../dev-Chvlqs7I.mjs";
import { t as createDevServer } from "../dev--rAPzbvO.mjs";
import process from "node:process";

@@ -6,0 +6,0 @@ import cac$1 from "cac";

@@ -1,3 +0,3 @@

import { i as resolveMcpConnectionMeta, r as resolveDevServerPort } from "../_shared-CKZPvSrN.mjs";
import { t as createDevServer } from "../dev-Chvlqs7I.mjs";
import { i as resolveMcpConnectionMeta, r as resolveDevServerPort } from "../_shared-CbZ_yhbz.mjs";
import { t as createDevServer } from "../dev--rAPzbvO.mjs";
export { createDevServer, resolveDevServerPort, resolveMcpConnectionMeta };

@@ -1,2 +0,2 @@

import { n as getInstanceInternals, r as initDevframe } from "../dev-Chvlqs7I.mjs";
import { n as getInstanceInternals, r as initDevframe } from "../dev--rAPzbvO.mjs";
export { getInstanceInternals, initDevframe };

@@ -1,2 +0,2 @@

import { i as createMcpServer, n as mountMcpHttp, r as createMcpFetchHandler } from "../http-8Kpkap4_.mjs";
import { i as createMcpServer, n as mountMcpHttp, r as createMcpFetchHandler } from "../http-DYpK0cUi.mjs";
export { createMcpFetchHandler, createMcpServer, mountMcpHttp };

@@ -725,3 +725,2 @@ import { t as hash } from "../hash-KtDZYXDN.mjs";

* @internal
* implementations; not part of the stable public API.
*/

@@ -728,0 +727,0 @@ function createRpcWireCodec(definitions = EMPTY_WIRE_DEFS) {

@@ -367,3 +367,2 @@ import { Ct as DevframeAgentHost$1, P as DevframeHost, St as AgentToolProviderHandle, Tt as EventEmitter, _t as AgentResourceContent, bt as AgentToolInput, g as DevframeNodeContext, gt as AgentResource, ht as AgentManifest, mt as AgentHandle, vt as AgentResourceInput, wt as DevframeAgentHostEvents, xt as AgentToolProvider, yt as AgentTool } from "../devframe-DJMg0WLe.mjs";

* @internal
* implementations; not part of the stable public API.
*/

@@ -384,3 +383,2 @@ interface RpcWireCodec {

* @internal
* implementations; not part of the stable public API.
*/

@@ -395,3 +393,2 @@ declare function createRpcWireCodec(definitions?: ReadonlyMap<string, Pick<RpcFunctionDefinitionAny, 'jsonSerializable'>>): RpcWireCodec;

* @internal
* implementations; not part of the stable public API.
*/

@@ -398,0 +395,0 @@ declare function peekRpcWireFrame(raw: string): {

@@ -1,9 +0,9 @@

import { n as peekRpcWireFrame, t as createRpcWireCodec } from "../wire-codec-GsoRxIsD.mjs";
import { n as peekRpcWireFrame, t as createRpcWireCodec } from "../wire-codec-0K-o5MYW.mjs";
import { t as diagnostics } from "../diagnostics-DOQPnQmX.mjs";
import { n as coerceAgentPositionalArgs, t as DevframeAgentHost } from "../host-agent-xLCSe65D.mjs";
import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-CKZPvSrN.mjs";
import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-CbZ_yhbz.mjs";
import { t as createH3DevframeHost } from "../host-h3-Crkzm42q.mjs";
import { i as registerDevframeInstance, n as listLiveDevframeInstances } from "../instance-registry-WvQkt42E.mjs";
import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, r as samePath, t as createInstanceShell } from "../instance-shell-XvN5USg4.mjs";
import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, r as samePath, t as createInstanceShell } from "../instance-shell-CMFyszWJ.mjs";
import { t as createContextRpcServer } from "../rpc-core-DMTe-hLo.mjs";
export { DevframeAgentHost, coerceAgentPositionalArgs, createContextRpcServer, createH3DevframeHost, createInstanceShell, createRpcWireCodec, diagnostics, listLiveDevframeInstances, normalizeBasePath, normalizeHttpServerUrl, peekRpcWireFrame, registerDevframeInstance, resolveBasePath, resolveInstanceRegister, samePath };

@@ -1,3 +0,3 @@

import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-CKZPvSrN.mjs";
import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-CbZ_yhbz.mjs";
import { n as internalContextMap, t as getInternalContext } from "../context-xdynGSLP.mjs";
export { getInternalContext, internalContextMap, normalizeBasePath, resolveBasePath };
import { DEVFRAME_AUTH_TOKEN_QUERY_PARAM, DEVFRAME_SSE_SESSION_HEADER } from "../../constants.mjs";
import { t as createRpcWireCodec } from "../../wire-codec-GsoRxIsD.mjs";
import { t as createRpcWireCodec } from "../../wire-codec-0K-o5MYW.mjs";
//#region src/rpc/transports/sse-client.ts

@@ -4,0 +4,0 @@ function NOOP() {}

import { DEVFRAME_SSE_SESSION_HEADER } from "../../constants.mjs";
import { n as peekRpcWireFrame, t as createRpcWireCodec } from "../../wire-codec-GsoRxIsD.mjs";
import { i as isAllowedOrigin, o as createRpcSessionMeta } from "../../ws-server-BiUne4K7.mjs";
import { n as peekRpcWireFrame, t as createRpcWireCodec } from "../../wire-codec-0K-o5MYW.mjs";
import { i as isAllowedOrigin, o as createRpcSessionMeta } from "../../ws-server-BdSLrhxE.mjs";
//#region src/rpc/transports/sse-server.ts

@@ -5,0 +5,0 @@ const SSE_STREAM_HEADERS = {

@@ -1,2 +0,2 @@

import { i as isAllowedOrigin, r as createWsRpcPeerHooks } from "../../ws-server-BiUne4K7.mjs";
import { i as isAllowedOrigin, r as createWsRpcPeerHooks } from "../../ws-server-BdSLrhxE.mjs";
//#region src/rpc/transports/ws-bun.ts

@@ -3,0 +3,0 @@ /**

import { DEVFRAME_AUTH_TOKEN_QUERY_PARAM } from "../../constants.mjs";
import { t as createRpcWireCodec } from "../../wire-codec-GsoRxIsD.mjs";
import { t as createRpcWireCodec } from "../../wire-codec-0K-o5MYW.mjs";
//#region src/rpc/transports/ws-client.ts

@@ -4,0 +4,0 @@ function NOOP() {}

@@ -1,2 +0,2 @@

import { a as isLoopbackHostname, i as isAllowedOrigin, n as createWsOriginRegistry, r as createWsRpcPeerHooks, t as attachWsRpcTransport } from "../../ws-server-BiUne4K7.mjs";
import { a as isLoopbackHostname, i as isAllowedOrigin, n as createWsOriginRegistry, r as createWsRpcPeerHooks, t as attachWsRpcTransport } from "../../ws-server-BdSLrhxE.mjs";
export { attachWsRpcTransport, createWsOriginRegistry, createWsRpcPeerHooks, isAllowedOrigin, isLoopbackHostname };
{
"name": "devframe",
"type": "module",
"version": "0.9.0-beta.9",
"version": "0.9.0-beta.10",
"description": "Framework for building one portable devtool integration that runs in any viewer.",

@@ -51,2 +51,3 @@ "author": "Anthony Fu <anthonyfu117@hotmail.com>",

"./utils/events": "./dist/utils/events.mjs",
"./utils/get-port": "./dist/utils/get-port.mjs",
"./utils/hash": "./dist/utils/hash.mjs",

@@ -53,0 +54,0 @@ "./utils/launch-editor": "./dist/utils/launch-editor.mjs",

import { n as __exportAll } from "./rolldown-runtime-JspESFgx.mjs";
import "./constants.mjs";
import "node:fs/promises";
import { createServer } from "node:net";
import { networkInterfaces } from "node:os";
import { cleanDoubleSlashes, withLeadingSlash, withTrailingSlash, withoutLeadingSlash } from "ufo";
//#region ../../node_modules/.pnpm/get-port-please@3.2.0/node_modules/get-port-please/dist/index.mjs
var dist_exports = /* @__PURE__ */ __exportAll({
checkPort: () => checkPort,
getPort: () => getPort,
getRandomPort: () => getRandomPort,
isSafePort: () => isSafePort,
isUnsafePort: () => isUnsafePort
});
const unsafePorts = /* @__PURE__ */ new Set([
1,
7,
9,
11,
13,
15,
17,
19,
20,
21,
22,
23,
25,
37,
42,
43,
53,
69,
77,
79,
87,
95,
101,
102,
103,
104,
109,
110,
111,
113,
115,
117,
119,
123,
135,
137,
139,
143,
161,
179,
389,
427,
465,
512,
513,
514,
515,
526,
530,
531,
532,
540,
548,
554,
556,
563,
587,
601,
636,
989,
990,
993,
995,
1719,
1720,
1723,
2049,
3659,
4045,
5060,
5061,
6e3,
6566,
6665,
6666,
6667,
6668,
6669,
6697,
10080
]);
function isUnsafePort(port) {
return unsafePorts.has(port);
}
function isSafePort(port) {
return !isUnsafePort(port);
}
var GetPortError = class extends Error {
constructor(message, opts) {
super(message, opts);
this.message = message;
}
name = "GetPortError";
};
function _log(verbose, message) {
if (verbose) console.log(`[get-port] ${message}`);
}
function _generateRange(from, to) {
if (to < from) return [];
const r = [];
for (let index = from; index <= to; index++) r.push(index);
return r;
}
function _tryPort(port, host) {
return new Promise((resolve) => {
const server = createServer();
server.unref();
server.on("error", () => {
resolve(false);
});
server.listen({
port,
host
}, () => {
const { port: port2 } = server.address();
server.close(() => {
resolve(isSafePort(port2) && port2);
});
});
});
}
function _getLocalHosts(additional) {
const hosts = new Set(additional);
for (const _interface of Object.values(networkInterfaces())) for (const config of _interface || []) if (config.address && !config.internal && !config.address.startsWith("fe80::") && !config.address.startsWith("169.254")) hosts.add(config.address);
return [...hosts];
}
async function _findPort(ports, host) {
for (const port of ports) {
const r = await _tryPort(port, host);
if (r) return r;
}
}
function _fmtOnHost(hostname) {
return hostname ? `on host ${JSON.stringify(hostname)}` : "on any host";
}
const HOSTNAME_RE = /^(?!-)[\d.:A-Za-z-]{1,63}(?<!-)$/;
function _validateHostname(hostname, _public, verbose) {
if (hostname && !HOSTNAME_RE.test(hostname)) {
const fallbackHost = _public ? "0.0.0.0" : "127.0.0.1";
_log(verbose, `Invalid hostname: ${JSON.stringify(hostname)}. Using ${JSON.stringify(fallbackHost)} as fallback.`);
return fallbackHost;
}
return hostname;
}
async function getPort(_userOptions = {}) {
if (typeof _userOptions === "number" || typeof _userOptions === "string") _userOptions = { port: Number.parseInt(_userOptions + "") || 0 };
const _port = Number(_userOptions.port ?? process.env.PORT);
const _userSpecifiedAnyPort = Boolean(_userOptions.port || _userOptions.ports?.length || _userOptions.portRange?.length);
const options = {
random: _port === 0,
ports: [],
portRange: [],
alternativePortRange: _userSpecifiedAnyPort ? [] : [3e3, 3100],
verbose: false,
..._userOptions,
port: _port,
host: _validateHostname(_userOptions.host ?? process.env.HOST, _userOptions.public, _userOptions.verbose)
};
if (options.random && !_userSpecifiedAnyPort) return getRandomPort(options.host);
const portsToCheck = [
options.port,
...options.ports,
..._generateRange(...options.portRange)
].filter((port) => {
if (!port) return false;
if (!isSafePort(port)) {
_log(options.verbose, `Ignoring unsafe port: ${port}`);
return false;
}
return true;
});
if (portsToCheck.length === 0) portsToCheck.push(3e3);
let availablePort = await _findPort(portsToCheck, options.host);
if (!availablePort && options.alternativePortRange.length > 0) {
availablePort = await _findPort(_generateRange(...options.alternativePortRange), options.host);
if (portsToCheck.length > 0) {
let message = `Unable to find an available port (tried ${portsToCheck.join("-")} ${_fmtOnHost(options.host)}).`;
if (availablePort) message += ` Using alternative port ${availablePort}.`;
_log(options.verbose, message);
}
}
if (!availablePort && _userOptions.random !== false) {
availablePort = await getRandomPort(options.host);
if (availablePort) _log(options.verbose, `Using random port ${availablePort}`);
}
if (!availablePort) {
const triedRanges = [
options.port,
options.portRange.join("-"),
options.alternativePortRange.join("-")
].filter(Boolean).join(", ");
throw new GetPortError(`Unable to find an available port ${_fmtOnHost(options.host)} (tried ${triedRanges})`);
}
return availablePort;
}
async function getRandomPort(host) {
const port = await checkPort(0, host);
if (port === false) throw new GetPortError(`Unable to find a random port ${_fmtOnHost(host)}`);
return port;
}
async function checkPort(port, host = process.env.HOST, verbose) {
if (!host) host = _getLocalHosts([void 0, "0.0.0.0"]);
if (!Array.isArray(host)) return _tryPort(port, host);
for (const _host of host) {
const _port = await _tryPort(port, _host);
if (_port === false) {
if (port < 1024 && verbose) _log(verbose, `Unable to listen to the privileged port ${port} ${_fmtOnHost(_host)}`);
return false;
}
if (port === 0 && _port !== 0) port = _port;
}
return port;
}
//#endregion
//#region src/adapters/_shared.ts
const DEFAULT_PORT = 9999;
/**
* Resolve the mount base path for a devframe's SPA. Hosted adapters
* (`vite`, `embedded`) default to `/__<id>/` so they don't collide
* with the host app; standalone adapters (`cli`, `build`)
* default to `/` because they own the origin.
*
* The devframe author can override with `basePath` on the definition.
*/
function resolveBasePath(def, kind) {
if (def.basePath) return normalizeBasePath(def.basePath);
return kind === "standalone" ? "/" : `/__${def.id}/`;
}
function normalizeBasePath(base) {
return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base)));
}
/**
* Resolve the listening port for `createDevServer` (and `createHandler`'s
* side-car tiers), honoring the definition's `cli.port` / `cli.portRange` /
* `cli.random` settings. Exposed separately so authors who run their own
* argv parsing can resolve a port up-front (to print it, log it, etc.)
* before starting the server.
*/
async function resolveDevServerPort(def, options = {}) {
const host = options.host ?? def.cli?.host ?? "localhost";
const portOptions = {
port: options.defaultPort ?? def.cli?.port ?? DEFAULT_PORT,
host
};
if (def.cli?.portRange) portOptions.portRange = def.cli.portRange;
if (def.cli?.random) portOptions.random = def.cli.random;
return getPort(portOptions);
}
/**
* Normalize the `cli.mcp` / `mcp` option (`boolean | McpRouteOptions`) into
* concrete options, or `undefined` when the MCP route is disabled.
*/
function resolveMcpConfig(mcp) {
if (!mcp) return void 0;
return mcp === true ? {} : mcp;
}
/**
* Resolve the `mcp` entry a `__connection.json` should advertise for a dev
* server started with the given `mcp` option (falling back to `def.cli?.mcp`,
* exactly like `createDevServer`), or `undefined` when the route is
* disabled.
*
* Hosted bridges that hand-roll their connection meta pass the side-car
* `port`: the advertised path becomes absolute (the side-car mounts at `/`)
* and the client dials `<page-host>:<port><path>`. Without `port` the path
* stays relative, resolved against `__connection.json`'s own location (the
* same-server default).
*/
function resolveMcpConnectionMeta(def, mcp, port) {
const config = resolveMcpConfig(mcp ?? def.cli?.mcp);
if (!config) return void 0;
const route = withoutLeadingSlash(config.path ?? "__mcp");
return port != null ? {
path: withLeadingSlash(route),
port
} : { path: route };
}
//#endregion
export { dist_exports as a, resolveMcpConnectionMeta as i, resolveBasePath as n, resolveDevServerPort as r, normalizeBasePath as t };
import { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs";
import { t as diagnostics } from "./diagnostics-DOQPnQmX.mjs";
import { t as createHostContext } from "./context-CEe4PBz1.mjs";
import { t as resolveStaticAssetsSource } from "./remote-assets-nTTxY2MU.mjs";
import { i as resolveMcpConnectionMeta, n as resolveBasePath, r as resolveDevServerPort, t as normalizeBasePath } from "./_shared-CKZPvSrN.mjs";
import { t as createH3DevframeHost } from "./host-h3-Crkzm42q.mjs";
import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, t as createInstanceShell } from "./instance-shell-XvN5USg4.mjs";
import { open } from "./utils/open.mjs";
import { mountStaticHandler } from "./utils/serve-static.mjs";
import { createServer } from "node:http";
import { resolve } from "pathe";
import process from "node:process";
import { joinURL, withBase } from "ufo";
import { H3, toNodeHandler } from "h3";
//#region src/adapters/initiate.ts
const INSTANCE_INTERNALS = /* @__PURE__ */ new WeakMap();
/** @internal */
function getInstanceInternals(handler) {
return INSTANCE_INTERNALS.get(handler) ?? {};
}
/**
* Serve a devframe through one framework-agnostic, web-standard handler —
* the SPA, `__connection.json` discovery, the WebSocket RPC endpoint, the
* auth gate, and the optional MCP route, all under a single mount base.
* Mount `handler` on any framework's catch-all route (or `nodeMiddleware` on
* a connect stack) and the devframe is live inside that app.
*
* The factory is synchronous and kicks off initialization eagerly;
* `handler`/`nodeMiddleware` await readiness internally. Nothing binds a port
* on its own: the WebSocket resolves in precedence order — `ws.port` (pinned
* side-car) > `server` (shared upgrade at `<base>__ws`) > `ws.sidecar`
* (auto-port side-car) > the host driving upgrades itself through
* {@link DevframeInstance.attach} — while `ws.url`, when set, overrides the
* advertised* endpoint (the tunnel pattern) and on its own hands the whole
* transport to an external server. `__connection.json` reflects whichever
* combination is active.
*/
function initDevframe(def, options) {
const base = normalizeBasePath(options.base);
const distDir = options.distDir === false ? void 0 : options.distDir ?? def.cli?.distDir;
const app = options.app ?? new H3();
const shell = createInstanceShell({
base,
app,
host: options.host ?? def.cli?.host ?? "localhost",
origin: options.origin,
auth: options.auth !== void 0 ? options.auth : def.cli?.auth,
server: options.server,
ws: options.ws ?? def.cli?.ws,
sse: options.sse ?? def.cli?.sse,
allowedOrigins: options.allowedOrigins,
destroyUnmatchedUpgrades: options.destroyUnmatchedUpgrades,
onPeerConnect: options.onPeerConnect,
onPeerDisconnect: options.onPeerDisconnect,
register: resolveInstanceRegister(options.register, {
id: def.id,
name: def.name
}),
resolveSidecarPort: (sidecarHost) => resolveDevServerPort(def, { host: sidecarHost }),
onMetaUnavailable: () => {
throw diagnostics.DF0054({ id: def.id });
},
async init(api) {
const h3Host = createH3DevframeHost({
origin: () => api.origin() ?? "http://localhost",
appName: def.id,
mount: (mountBase, dir) => {
mountStaticHandler(app, mountBase, dir);
}
});
const hostImpl = options.getStorageDir ? {
...h3Host,
getStorageDir: options.getStorageDir
} : h3Host;
const context = await createHostContext({
cwd: process.cwd(),
mode: "dev",
host: hostImpl
});
const setupInfo = { flags: options.flags ?? {} };
await def.setup(context, setupInfo);
const mcpOption = options.mcp ?? def.cli?.mcp;
const mcpMeta = resolveMcpConnectionMeta(def, mcpOption);
let mcpDispose;
if (mcpMeta) {
const mcpConfig = mcpOption === true || mcpOption === void 0 ? {} : mcpOption;
const mcpPath = joinURL(base, mcpMeta.path);
let mountMcpHttp;
try {
({mountMcpHttp} = await import("./http-8Kpkap4_.mjs").then((n) => n.t));
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw diagnostics.DF0017({
transport: "http",
reason,
cause: error
});
}
mcpDispose = mountMcpHttp(app, context, mcpPath, {
serverName: `${def.id} (devframe)`,
serverVersion: def.version ?? "0.0.0",
exposeSharedState: true,
allowedOrigins: mcpConfig.allowedOrigins
}).dispose;
}
return {
context,
...mcpMeta ? { mcp: mcpMeta } : {},
...mcpDispose ? { dispose: mcpDispose } : {}
};
},
mount(context, meta) {
app.use(joinURL(base, DEVFRAME_CONNECTION_META_FILENAME), () => meta);
if (distDir) {
const source = resolveStaticAssetsSource(distDir, context.host.getStorageDir("project"));
mountStaticHandler(app, base, typeof source === "string" ? resolve(source) : source);
}
}
});
const instance = {
base: shell.base,
handler: shell.handler,
nodeMiddleware: shell.nodeMiddleware,
attach: shell.attach,
handleUpgrade: shell.handleUpgrade,
ready: shell.ready,
context: shell.context,
connectionMeta: shell.connectionMeta,
close: shell.close
};
INSTANCE_INTERNALS.set(instance, shell.internals);
return instance;
}
//#endregion
//#region src/adapters/dev.ts
/**
* Start a devframe dev server for a {@link DevframeDefinition} —
* h3 + WebSocket RPC + (optionally) the author's SPA mounted at the
* resolved base path.
*
* When `distDir` is omitted (and `def.cli?.distDir` is unset) the
* server runs in **bridge mode**: only `__connection.json` and the WS
* endpoint are mounted, with no SPA mount. The SPA is expected to be
* hosted elsewhere (e.g. by a parent Vite/Nuxt dev server) — see
* `devframeViteBridge` from `@devframes/vite`.
*
* Returns the underlying {@link StartedServer} handle so callers can
* close it gracefully (SIGINT, hot-reload, test teardown).
*
* Use this directly when integrating devframe into an existing CLI
* framework (commander, yargs, hand-rolled CAC). For the all-in-one
* `dev` / `build` / `mcp` shell, reach for {@link createCac} instead.
*/
async function createDevServer(def, options = {}) {
if (def.capabilities?.dev === false && !options.force) throw diagnostics.DF0058({ id: def.id });
const host = options.host ?? def.cli?.host ?? "localhost";
const requestedPort = options.port ?? await resolveDevServerPort(def, { host });
const flags = options.flags ?? {};
const basePath = options.basePath ? normalizeBasePath(options.basePath) : resolveBasePath(def, "standalone");
const app = options.app ?? new H3();
const server = createServer(toNodeHandler(app));
try {
await new Promise((resolveListen, rejectListen) => {
const onError = (error) => rejectListen(error);
server.once("error", onError);
server.listen(requestedPort, host, () => {
server.removeListener("error", onError);
resolveListen();
});
});
} catch (error) {
throw diagnostics.DF0052({
host,
port: requestedPort,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
const address = server.address();
const port = typeof address === "object" && address ? address.port : requestedPort;
const origin = normalizeHttpServerUrl(host, port);
const devframe = initDevframe(def, {
base: basePath,
distDir: options.distDir,
app,
server,
host,
origin,
ws: options.ws,
allowedOrigins: options.allowedOrigins,
sse: options.sse,
auth: flags.auth === false ? false : options.auth,
mcp: options.mcp,
flags,
onPeerConnect: options.onPeerConnect,
onPeerDisconnect: options.onPeerDisconnect,
register: true,
destroyUnmatchedUpgrades: true
});
try {
await devframe.ready;
} catch (error) {
await new Promise((resolveClose) => server.close(() => resolveClose()));
throw error;
}
const internals = getInstanceInternals(devframe);
const transport = internals.started;
await options.onReady?.({
origin,
port,
app
});
await maybeOpenBrowser(def, flags, `${origin}${basePath}`, options.openBrowser, internals.authHandler);
return {
origin,
port,
app,
ws: transport.ws,
rpcGroup: transport.rpcGroup,
connectionMeta: transport.connectionMeta,
async close() {
await devframe.close();
await new Promise((resolveClose) => server.close(() => resolveClose()));
}
};
}
async function maybeOpenBrowser(def, flags, origin, override, authHandler) {
const flagsOpen = flags.open;
const cliOpen = def.cli?.open;
const resolved = override ?? flagsOpen ?? cliOpen;
if (resolved === void 0 || resolved === false) return;
const target = typeof resolved === "string" ? withBase(resolved, origin) : origin;
const authorizedTarget = authHandler?.buildOpenUrl?.(target) ?? target;
try {
await open(authorizedTarget);
} catch {}
}
//#endregion
export { getInstanceInternals as n, initDevframe as r, createDevServer as t };
import { n as __exportAll } from "./rolldown-runtime-JspESFgx.mjs";
import { t as Diagnostic } from "./nostics-CzECRXpE.mjs";
import { i as isAllowedOrigin } from "./ws-server-BiUne4K7.mjs";
import { t as diagnostics } from "./diagnostics-DOQPnQmX.mjs";
import { t as createHostContext } from "./context-CEe4PBz1.mjs";
import { t as toAgentToolName } from "./agent-tool-name-EgfoFO8C.mjs";
import { randomUUID } from "node:crypto";
import { join } from "pathe";
import process from "node:process";
import { homedir } from "node:os";
import { defineHandler } from "h3";
import { Server, WebStandardStreamableHTTPServerTransport, isInitializeRequest } from "@modelcontextprotocol/server";
//#region src/adapters/mcp/stringify.ts
/**
* JSON-coercing serializer for MCP text payloads.
*
* MCP carries tool results and resource reads as plain text over a
* JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
* format the WS RPC transport falls back to for non-JSON values. Instead,
* we coerce common non-JSON types into JSON-friendly forms so the LLM
* client sees something useful instead of `[object Object]`.
*
* Coercions:
* - `BigInt` → `"123n"`
* - `Date` → ISO string (via the native `toJSON`)
* - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
* - `Set` → `{ __type: 'Set', entries: [v, …] }`
* - `Error` → `{ name, message, stack, cause? }` (cause recurses)
* - `Function` → `"[Function: name]"`
* - `Symbol` → `value.toString()`
* - cycles → `"[Circular]"`
*/
function stringifyForMcp(value) {
if (value === void 0) return "undefined";
if (typeof value === "string") return value;
const seen = /* @__PURE__ */ new WeakSet();
return JSON.stringify(value, (_key, val) => {
if (typeof val === "bigint") return `${val}n`;
if (val instanceof Error) {
const out = {
name: val.name,
message: val.message,
stack: val.stack
};
if (val.cause !== void 0) out.cause = val.cause;
return out;
}
if (val instanceof Map) return {
__type: "Map",
entries: [...val.entries()]
};
if (val instanceof Set) return {
__type: "Set",
entries: [...val]
};
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
if (typeof val === "symbol") return val.toString();
if (val !== null && typeof val === "object") {
if (seen.has(val)) return "[Circular]";
seen.add(val);
}
return val;
}, 2);
}
/**
* Format a thrown value for an MCP `isError` text payload.
*
* A nostics `Diagnostic` (every coded devframe error) becomes structured
* JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
* the actionable next step (`fix`) and the docs URL instead of a bare
* message string. Other errors surface `Error.name`/`message`, plus one
* level of `cause.message` so context isn't dropped silently.
*/
function formatMcpError(error) {
if (error instanceof Diagnostic) return JSON.stringify({ error: {
code: error.code,
message: error.message,
...error.fix ? { fix: error.fix } : {},
...error.docs ? { docs: error.docs } : {}
} }, null, 2);
if (!(error instanceof Error)) return String(error);
const cause = error.cause;
const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
return `${error.name}: ${error.message}${causeText}`;
}
//#endregion
//#region src/adapters/mcp/to-json-schema.ts
const FALLBACK_OBJECT_SCHEMA = Object.freeze({
type: "object",
additionalProperties: true
});
/**
* Convert a Standard Schema to JSON Schema for the agent/MCP surface.
*
* Devframe stays validator-neutral, so conversion uses the schema's own
* [Standard JSON Schema](https://standardschema.dev/) converter
* (`~standard.jsonSchema`) when the validator provides one — zod 4 does,
* for example. Validators without a native converter (e.g. valibot) degrade
* to a permissive object schema rather than pulling in a converter library.
*/
function safeToJsonSchema(schema) {
const standard = schema["~standard"];
if (standard.jsonSchema) try {
return standard.jsonSchema.input({ target: "draft-2020-12" });
} catch {
return FALLBACK_OBJECT_SCHEMA;
}
return FALLBACK_OBJECT_SCHEMA;
}
/**
* JSON Schema for an RPC return value on the agent/MCP surface.
* @internal
*/
function returnToJsonSchema(schema) {
if (!schema) return void 0;
return safeToJsonSchema(schema);
}
/**
* JSON Schema for an RPC function's positional args on the agent/MCP
* surface. Each positional arg is advertised under `arg0` / `arg1` / … —
* matching how the agent bridge coerces the incoming object payload back
* into positional arguments.
*
* Returns `{ type: 'object', properties: {} }` when there are no args.
* @internal
*/
function argsToJsonSchema(args) {
if (!args || args.length === 0) return {
schema: {
type: "object",
properties: {}
},
unwrapped: false
};
const properties = {};
const required = [];
for (let i = 0; i < args.length; i++) {
const key = `arg${i}`;
properties[key] = safeToJsonSchema(args[i]);
required.push(key);
}
return {
schema: {
type: "object",
properties,
required,
additionalProperties: false
},
unwrapped: false
};
}
//#endregion
//#region src/adapters/mcp/build-server.ts
/**
* Wire an MCP {@link Server} to a devframe context. Returns the server
* plus a disposal function for the subscriptions it sets up. The
* transport is the caller's responsibility — `createMcpServer` connects
* stdio; tests can connect an {@link InMemoryTransport} instead.
*
* @internal
*/
function buildMcpServerFromContext(ctx, options) {
const server = new Server({
name: options.serverName,
version: options.serverVersion
}, { capabilities: {
tools: { listChanged: true },
resources: { listChanged: true }
} });
registerToolHandlers(server, ctx, options.exposeSharedState);
registerResourceHandlers(server, ctx, options.exposeSharedState);
const notify = (method) => {
server.notification({ method }).catch(() => {});
};
const offManifest = ctx.agent.events.on("agent:manifest:changed", () => {
notify("notifications/tools/list_changed");
notify("notifications/resources/list_changed");
});
const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
notify("notifications/resources/list_changed");
});
return {
server,
dispose: () => {
offManifest();
offKeyAdded();
}
};
}
/**
* Build an MCP server over the agent surface of a devframe definition.
* Currently supports `stdio` transport only.
*/
async function createMcpServer(definition, options = {}) {
const transport = options.transport ?? "stdio";
if (transport !== "stdio") throw diagnostics.DF0017({
transport,
reason: "Only stdio transport is supported in this release."
});
const ctx = await createHostContext({
cwd: process.cwd(),
mode: "dev",
host: {
mountStatic: () => {},
resolveOrigin: () => "mcp://devframe",
getStorageDir: (scope) => {
if (scope === "workspace") return join(process.cwd(), ".devframe");
if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
return join(homedir(), `.${definition.id}/devframe`);
}
}
});
await definition.setup(ctx);
const { server, dispose } = buildMcpServerFromContext(ctx, {
serverName: options.serverName ?? `${definition.id} (devframe)`,
serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
exposeSharedState: options.exposeSharedState ?? true
});
const { startStdioTransport } = await import("./transports-vhizgqXM.mjs");
let stop;
try {
stop = await startStdioTransport(server);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw diagnostics.DF0017({
transport,
reason,
cause: error
});
}
options.onReady?.({ transport: "stdio" });
return { async stop() {
dispose();
await stop();
} };
}
/**
* Id of the built-in shared-state read tool — namespaced like every other
* built-in (`devframe:<area>:<fn>`). Tool-shaped access matters because many
* MCP clients only consume tools — the parallel `devframe://state/<key>`
* resource projection stays for the clients that do read resources.
*/
const READ_STATE_TOOL = "devframe:state:read";
/** Wire name of the built-in shared-state read tool: `devframe_state_read`. */
const READ_STATE_NAME = toAgentToolName(READ_STATE_TOOL);
function sharedStateFilter(exposeSharedState) {
if (exposeSharedState === false) return void 0;
return typeof exposeSharedState === "function" ? exposeSharedState : () => true;
}
function readStateToolProjection() {
return {
name: READ_STATE_NAME,
title: "Read shared state",
description: "Read this devtool's live shared state. Call without arguments to list the available keys, then with a key to get that value as JSON. Safe to call freely.",
inputSchema: {
type: "object",
properties: { key: {
type: "string",
description: "A shared-state key from the key list. Omit to list all keys."
} }
},
annotations: {
title: "Read shared state",
readOnlyHint: true,
destructiveHint: false
}
};
}
async function readStateResult(ctx, filter, key) {
const keys = ctx.rpc.sharedState.keys().filter(filter);
if (key === void 0) return { keys };
if (!keys.includes(key)) throw diagnostics.DF0048({ key });
return {
key,
value: (await ctx.rpc.sharedState.get(key)).value()
};
}
function registerToolHandlers(server, ctx, exposeSharedState) {
const stateFilter = sharedStateFilter(exposeSharedState);
const warnedCollisions = /* @__PURE__ */ new Set();
/**
* Resolve a wire tool name back to the registered {@link AgentTool}.
* Wire-name matching runs first, in manifest order — the same tool the
* list projection advertises under that name — with a raw-id fallback so
* a colon-namespaced id keeps working as a call name.
*/
const resolveTool = (name) => {
return ctx.agent.list().tools.find((tool) => toAgentToolName(tool.id) === name) ?? ctx.agent.getTool(name);
};
server.setRequestHandler("tools/list", async () => {
const byName = /* @__PURE__ */ new Map();
for (const tool of ctx.agent.list().tools) {
const name = toAgentToolName(tool.id);
const existing = byName.get(name);
if (existing) {
if (!warnedCollisions.has(`${name}|${tool.id}`)) {
warnedCollisions.add(`${name}|${tool.id}`);
diagnostics.DF0047({
name,
id: tool.id,
existing: existing.id
});
}
continue;
}
byName.set(name, tool);
}
const tools = [...byName.entries()].map(([name, tool]) => projectTool(name, tool, ctx));
if (stateFilter && !byName.has(READ_STATE_NAME)) tools.push(readStateToolProjection());
return { tools };
});
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
try {
const tool = resolveTool(name);
if (stateFilter && !tool && (name === READ_STATE_NAME || name === READ_STATE_TOOL)) {
const key = args?.key;
const result = await readStateResult(ctx, stateFilter, key);
return {
content: [{
type: "text",
text: stringifyForMcp(result)
}],
structuredContent: result
};
}
const outputSchema = tool ? usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx)) : void 0;
const result = await ctx.agent.invoke(tool?.id ?? name, args ?? {});
return {
content: [{
type: "text",
text: stringifyForMcp(result)
}],
...outputSchema ? { structuredContent: result } : {}
};
} catch (error) {
return {
isError: true,
content: [{
type: "text",
text: `Error invoking "${name}": ${formatMcpError(error)}`
}]
};
}
});
}
function registerResourceHandlers(server, ctx, exposeSharedState) {
server.setRequestHandler("resources/list", async () => {
const resources = ctx.agent.list().resources.map((resource) => ({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType
}));
if (exposeSharedState !== false) {
const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
for (const key of ctx.rpc.sharedState.keys()) {
if (!filter(key)) continue;
resources.push({
uri: `devframe://state/${encodeURIComponent(key)}`,
name: key,
description: `Shared state: ${key}`,
mimeType: "application/json"
});
}
}
return { resources };
});
server.setRequestHandler("resources/read", async (request) => {
const { uri } = request.params;
const parsed = parseResourceUri(uri);
if (parsed.kind === "resource") {
const content = await ctx.agent.read(parsed.id);
return { contents: [{
uri,
mimeType: content.mimeType ?? "application/json",
text: content.text ?? stringifyForMcp(content.json)
}] };
}
if (parsed.kind === "state") return { contents: [{
uri,
mimeType: "application/json",
text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
}] };
throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
});
}
/**
* MCP constrains a tool's `outputSchema` to a JSON Schema of `type:
* "object"` — clients (the SDK included) reject anything else. Non-object
* return schemas (e.g. a schema for `void` / a bare string) simply project
* no output schema; the text content still carries the result.
*/
function usableOutputSchema(schema) {
return schema && typeof schema === "object" && schema.type === "object" ? schema : void 0;
}
function projectTool(name, tool, ctx) {
const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
const outputSchema = usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx));
return {
name,
title: tool.title,
description: tool.description,
inputSchema,
...outputSchema ? { outputSchema } : {},
annotations: {
title: tool.title,
readOnlyHint: tool.safety === "read",
destructiveHint: tool.safety === "destructive"
}
};
}
function computeInputSchema(tool, ctx) {
if (tool.kind === "tool") return argsToJsonSchema(tool.args).schema;
if (tool.kind !== "rpc" || !tool.rpcName) return {
type: "object",
properties: {}
};
const def = ctx.rpc.definitions.get(tool.rpcName);
if (!def) return {
type: "object",
properties: {}
};
const args = def.args;
return argsToJsonSchema(args).schema;
}
function computeOutputSchema(tool, ctx) {
if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
const def = ctx.rpc.definitions.get(tool.rpcName);
if (!def) return void 0;
return returnToJsonSchema(def.returns);
}
function parseResourceUri(uri) {
const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
if (!match) return { kind: "unknown" };
const [, kind, rest] = match;
const decoded = decodeURIComponent(rest);
if (kind === "resource") return {
kind: "resource",
id: decoded
};
return {
kind: "state",
key: decoded
};
}
//#endregion
//#region src/adapters/mcp/fetch.ts
/**
* Build a framework-agnostic MCP Streamable-HTTP endpoint over a devframe
* context: a web-standard `Request → Response` handler any host can mount —
* h3 (see `mountMcpHttp`), a Next.js App Router route, or any other
* fetch-shaped server.
*
* Each MCP session gets its own {@link WebStandardStreamableHTTPServerTransport}
* and MCP server (built from the shared, live `ctx` via
* `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
* `initialize` POST spins up a session; later requests route to it; a `DELETE`
* (or client disconnect) tears it down. The origin gate guards every request:
* loopback-default DNS-rebinding protection that — unlike the WS upgrade's
* `isAllowedOrigin` — also rejects `Origin`-less requests, so a route-based
* endpoint isn't reachable by an arbitrary local process.
*/
function createMcpFetchHandler(ctx, options) {
const sessions = /* @__PURE__ */ new Map();
const allowedOrigins = options.allowedOrigins;
function drop(sessionId) {
const session = sessions.get(sessionId);
if (!session) return;
sessions.delete(sessionId);
session.dispose();
}
async function createSession() {
let session;
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
sessions.set(id, session);
},
onsessionclosed: (id) => {
drop(id);
}
});
const { server, dispose } = buildMcpServerFromContext(ctx, {
serverName: options.serverName,
serverVersion: options.serverVersion,
exposeSharedState: options.exposeSharedState
});
session = {
transport,
dispose: async () => {
dispose();
await server.close();
}
};
transport.onclose = () => {
if (transport.sessionId) drop(transport.sessionId);
};
await server.connect(transport);
return session;
}
async function handle(req) {
const origin = req.headers.get("origin") ?? void 0;
if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
const sessionId = req.headers.get("mcp-session-id") ?? void 0;
let session = sessionId ? sessions.get(sessionId) : void 0;
if (!session && req.method === "POST") {
let body;
try {
body = await req.json();
} catch {
body = void 0;
}
if (!sessionId && isInitializeRequest(body)) session = await createSession();
else return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: no valid session ID and not an initialize request", { status: sessionId ? 404 : 400 });
return session.transport.handleRequest(req, { parsedBody: body });
}
if (!session) return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: missing MCP session ID", { status: sessionId ? 404 : 400 });
return session.transport.handleRequest(req);
}
return {
fetch: handle,
dispose: async () => {
const live = [...sessions.values()];
sessions.clear();
await Promise.all(live.map((session) => session.dispose()));
}
};
}
//#endregion
//#region src/adapters/mcp/http.ts
var http_exports = /* @__PURE__ */ __exportAll({ mountMcpHttp: () => mountMcpHttp });
/**
* Mount an MCP Streamable-HTTP endpoint on an h3 app at `path` — the h3
* binding over {@link createMcpFetchHandler}, which owns the sessions, the
* origin gate, and the transport plumbing.
*
* The handler is web-standard — it takes the h3 event's web `Request` and
* returns a web `Response` (an SSE `ReadableStream` body for the
* server→client stream). We copy that response onto `event.res` and return
* its body rather than returning the `Response` object directly, so a
* legitimate MCP 404 (unknown session) isn't swallowed by h3's
* "Response-with-404 falls through to the next handler" rule (which would
* otherwise hand the request to the SPA static catch-all).
*/
function mountMcpHttp(app, ctx, path, options) {
const handler = createMcpFetchHandler(ctx, options);
app.use(path, defineHandler(async (event) => respond(event, await handler.fetch(event.req))));
return { dispose: handler.dispose };
}
/**
* Copy a web `Response` from the MCP transport onto the h3 event's response
* and return its body. Returning the body (a `ReadableStream` or `null`)
* rather than the `Response` object avoids h3's 404-fall-through behavior.
*/
function respond(event, response) {
event.res.status = response.status;
event.res.statusText = response.statusText;
response.headers.forEach((value, key) => {
event.res.headers.set(key, value);
});
return response.body ?? "";
}
//#endregion
export { createMcpServer as i, mountMcpHttp as n, createMcpFetchHandler as r, http_exports as t };
import "./constants.mjs";
import { t as diagnostics } from "./diagnostics-DOQPnQmX.mjs";
import { t as getInternalContext } from "./context-xdynGSLP.mjs";
import { createInteractiveAuth } from "./recipes/interactive-auth.mjs";
import { createServer } from "node:http";
import process from "node:process";
import { isIP } from "node:net";
import { joinURL, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo";
import { H3, defineHandler, toNodeHandler } from "h3";
//#region src/node/utils.ts
const NON_DIALABLE_HOSTS = /* @__PURE__ */ new Set([
"0.0.0.0",
"127.0.0.1",
"::",
"0000:0000:0000:0000:0000:0000:0000:0000",
""
]);
/** Map a bind host to a host a client can actually connect to. */
function toDialableHost(host) {
return NON_DIALABLE_HOSTS.has(host) ? "localhost" : host;
}
/** Format a bind host for use in a URL authority (dialable, IPv6-bracketed). */
function formatHostForUrl(host) {
const dialable = toDialableHost(host);
return isIP(dialable) === 6 ? `[${dialable}]` : dialable;
}
function normalizeHttpServerUrl(host, port) {
return `http://${formatHostForUrl(host)}:${port}`;
}
//#endregion
//#region src/node/instance-shell.ts
/**
* Compose an h3 + WebSocket RPC server for a devframe context — the low-level
* "listen on a port (or share one) + attach the WS transport" binding the
* side-car and shared-server tiers below are built on. Owns and listens on a
* fresh `node:http` server unless `server` is supplied, in which case it only
* attaches the upgrade listener and leaves that server's lifecycle to its
* owner.
*/
async function bindHttpAndWs(options) {
const { context, port, core } = options;
const bindHost = options.host;
const app = new H3();
const ownsHttpServer = !options.server;
const httpServer = options.server ?? createServer(toNodeHandler(app));
const rpcHost = context.rpc;
const websocket = options.websocket !== false;
let ws;
let closeWs = async () => {};
if (websocket) {
const { attachWsRpcTransport } = await import("./rpc/transports/ws-server.mjs");
const transport = attachWsRpcTransport(core.rpcGroup, {
server: httpServer,
path: options.path,
destroyUnmatched: options.destroyUnmatched ?? ownsHttpServer,
allowedOrigins: options.allowedOrigins,
onConnected: core.onConnected,
onDisconnected: core.onDisconnected
});
ws = transport.ws;
closeWs = transport.close;
}
if (ownsHttpServer) try {
await new Promise((resolve, reject) => {
const onError = (error) => reject(error);
httpServer.once("error", onError);
httpServer.listen(port, bindHost, () => {
httpServer.removeListener("error", onError);
resolve();
});
});
} catch (error) {
await closeWs().catch(() => {});
throw diagnostics.DF0052({
host: bindHost,
port,
reason: error instanceof Error ? error.message : String(error),
cause: error
});
}
const address = httpServer.address();
const resolvedPort = typeof address === "object" && address ? address.port : port;
const origin = normalizeHttpServerUrl(bindHost, resolvedPort);
const internal = getInternalContext(context);
const wsUrl = `ws://${formatHostForUrl(bindHost)}:${resolvedPort}${options.path ?? ""}`;
if (websocket) internal.setWsEndpoint({ url: wsUrl });
function connectionMeta() {
const jsonSerializableMethods = [];
for (const def of rpcHost.definitions.values()) if (def.jsonSerializable === true) jsonSerializableMethods.push(def.name);
return {
backend: "websocket",
websocket: { path: options.path },
jsonSerializableMethods
};
}
return {
origin,
port: resolvedPort,
app,
ws,
rpcGroup: core.rpcGroup,
connectionMeta,
async close() {
await closeWs();
if (ownsHttpServer) await new Promise((r) => httpServer.close(() => r()));
if (websocket && getInternalContext(context).wsEndpoint?.url === wsUrl) getInternalContext(context).setWsEndpoint(void 0);
}
};
}
/**
* Translate the public `register?: boolean | Partial<DevframeInstanceRecord>`
* option into a shell {@link InstanceRegisterConfig}, or `undefined` when
* registration is opted out. The object form supplies record overrides on top
* of the caller-provided identity defaults.
*/
function resolveInstanceRegister(option, defaults) {
if (!option) return void 0;
return {
id: defaults.id,
...defaults.name !== void 0 ? { name: defaults.name } : {},
...defaults.rootDir !== void 0 ? { rootDir: defaults.rootDir } : {},
...typeof option === "object" ? { overrides: option } : {}
};
}
/** Compare two URL paths ignoring a trailing slash. */
function samePath(a, b) {
return withoutTrailingSlash(a) === withoutTrailingSlash(b);
}
/**
* Copy a web `Response` from a fetch-style transport handler onto the h3
* event's response and return its body — mirroring the MCP route's bridge.
* Returning the body (a `ReadableStream`, or `''` for an empty one — h3
* middleware only falls through on `undefined`) terminates the chain with
* the status/headers set here instead of continuing to the SPA catch-all.
*/
function respondWith(event, response) {
event.res.status = response.status;
event.res.statusText = response.statusText;
response.headers.forEach((value, key) => {
event.res.headers.set(key, value);
});
return response.body ?? "";
}
/**
* The shared machinery behind `initDevframe` and `initHub`: one mount base,
* one h3 app, one lazily-derived public origin (and the auth banner that waits
* for it), one WebSocket binding, and the fetch / connect-middleware pair that
* serves them. Each factory supplies only what makes it itself — its context,
* its routes, its diagnostics — through `init` / `mount`.
*
* Nothing here listens on a port unless a side-car was explicitly requested:
* the default tier leaves the socket `unbound`, so a host chains it onto its
* own server through {@link InstanceShell.attach} /
* {@link InstanceShell.handleUpgrade}.
*
* @internal
*/
function createInstanceShell(options) {
const base = options.base;
const baseNoSlash = withoutTrailingSlash(base);
const app = options.app ?? new H3();
const wsDisabled = options.ws === false;
const ws = options.ws === false ? {} : options.ws ?? {};
const route = withoutLeadingSlash(ws.route ?? "__ws");
/** Where an upgrade lands on the host's own origin. */
const routePath = joinURL(base, route);
/** What `__connection.json` advertises for a same-origin socket. */
const advertisedPath = options.absoluteWsPath ? routePath : route;
const sidecarRequested = ws.port != null || ws.sidecar === true;
const tier = wsDisabled ? "disabled" : sidecarRequested ? "sidecar" : options.server ? "server" : ws.url ? "external" : "unbound";
const sseEnabled = options.sse !== false && tier !== "external";
const sseRoute = withoutLeadingSlash((typeof options.sse === "object" ? options.sse.route : void 0) ?? "__sse");
const sseRoutePath = joinURL(base, sseRoute);
const advertisedSsePath = options.absoluteWsPath ? sseRoutePath : sseRoute;
let derivedOrigin;
function currentOrigin() {
return (typeof options.origin === "function" ? options.origin() : options.origin) || derivedOrigin;
}
let authHandler;
let bannerPrinted = false;
function maybePrintBanner() {
if (bannerPrinted || !authHandler || !currentOrigin()) return;
bannerPrinted = true;
authHandler.printBanner();
}
let meta;
let registration;
let registerPromise;
/**
* Publish the instance in the global registry the moment both its origin
* and connection meta are known — at init end for a pinned origin, or on
* the first request for a derived one. Registration never throws (the
* registry writer degrades to a coded warning), so failures never surface.
*/
function maybeRegister() {
const cfg = options.register;
const origin = currentOrigin();
if (!cfg || registerPromise || !origin || !meta) return;
const resolvedMeta = meta;
registerPromise = import("./instance-registry-WvQkt42E.mjs").then((n) => n.t).then(({ registerDevframeInstance }) => {
let port = 0;
try {
const url = new URL(origin);
port = Number(url.port) || (url.protocol === "https:" ? 443 : 80);
} catch {}
registration = registerDevframeInstance({
pid: process.pid,
port,
origin,
basePath: base,
id: cfg.id,
...cfg.name !== void 0 ? { name: cfg.name } : {},
rootDir: cfg.rootDir ?? process.cwd(),
mcp: resolvedMeta.mcp ? { path: joinURL(base, resolvedMeta.mcp.path) } : null,
startedAt: Date.now(),
...cfg.overrides
});
}).catch(() => {});
}
function noteOrigin(origin) {
derivedOrigin ??= origin;
maybePrintBanner();
maybeRegister();
}
let started;
let transport;
let dispose;
let ctx;
const api = {
base,
app,
origin: currentOrigin,
connectionMeta: () => meta
};
/**
* Auth resolution: gate by default, `false` opts out, a handler object
* installs a custom scheme. The `external` tier has no local transport to
* gate — the server behind `ws.url` owns auth — so it resolves to nothing.
*/
function resolveAuth() {
if (options.auth === false) return false;
if (typeof options.auth === "object") {
authHandler = options.auth;
return options.auth;
}
authHandler = createInteractiveAuth(ctx);
return authHandler;
}
/**
* The context's RPC core (birpc group, session lifecycle, auth gate) —
* one per instance, shared by every transport binding (WS and SSE), so a
* WS peer and an SSE session live in the same session/broadcast space.
* Built lazily: an `unbound` host that never wires a transport pays
* nothing for it, not even the imports. `resolvedAuth` and `ctx` are
* assigned during `init()` before any caller can reach this.
*/
let resolvedAuth = false;
let corePromise;
function ensureCore() {
corePromise ??= import("./rpc-core-DMTe-hLo.mjs").then((n) => n.n).then(({ createContextRpcServer }) => createContextRpcServer({
context: ctx,
auth: resolvedAuth,
onPeerConnect: options.onPeerConnect,
onPeerDisconnect: options.onPeerDisconnect
}));
return corePromise;
}
/**
* The SSE transport, built on the first request to its route so an
* instance nobody dials over SSE never loads it.
*/
let ssePromise;
function ensureSse() {
ssePromise ??= (async () => {
const [core, { attachSseRpcTransport }] = await Promise.all([ensureCore(), import("./rpc/transports/sse-server.mjs")]);
return attachSseRpcTransport(core.rpcGroup, {
allowedOrigins: options.allowedOrigins,
onConnected: core.onConnected,
onDisconnected: core.onDisconnected
});
})();
return ssePromise;
}
/**
* A side-car server on its own port. `getPort` probes and the bind can
* still race (or disagree across the v4/v6 duals of `localhost`), so an
* auto-port side-car retries on a fresh random port instead of failing
* init; a pinned `ws.port` is honored as given and fails loudly.
*/
async function startSidecar(core) {
const sidecarHost = options.host ?? "localhost";
const start = (port) => bindHttpAndWs({
context: ctx,
core,
host: sidecarHost,
port,
path: withLeadingSlash(route),
allowedOrigins: options.allowedOrigins
});
if (ws.port != null) return await start(ws.port);
const { getPort } = await import("./_shared-CKZPvSrN.mjs").then((n) => n.a);
let lastError;
for (let attempt = 0; attempt < 3; attempt++) {
const port = attempt === 0 && options.resolveSidecarPort ? await options.resolveSidecarPort(sidecarHost) : await getPort({
random: true,
host: sidecarHost
});
try {
return await start(port);
} catch (error) {
lastError = error;
}
}
throw lastError;
}
async function init() {
const result = await options.init(api);
ctx = result.context;
dispose = result.dispose;
resolvedAuth = tier === "external" ? false : resolveAuth();
let websocketMeta;
if (tier === "sidecar") {
started = await startSidecar(await ensureCore());
websocketMeta = {
port: started.port,
path: route
};
} else if (tier === "server") {
started = await bindHttpAndWs({
context: ctx,
core: await ensureCore(),
host: options.host ?? "localhost",
port: 0,
server: options.server,
path: routePath,
allowedOrigins: options.allowedOrigins,
destroyUnmatched: options.destroyUnmatchedUpgrades
});
websocketMeta = { path: advertisedPath };
} else if (tier === "external") websocketMeta = ws.url;
else if (tier === "unbound") websocketMeta = { path: advertisedPath };
if (!wsDisabled && ws.url) websocketMeta = ws.url;
if (sseEnabled) app.use(sseRoutePath, defineHandler(async (event) => respondWith(event, await (await ensureSse()).handler(event.req))));
meta = {
backend: wsDisabled ? sseEnabled ? "sse" : "none" : "websocket",
...websocketMeta !== void 0 ? { websocket: websocketMeta } : {},
...sseEnabled ? { sse: { path: advertisedSsePath } } : {},
...result.mcp ? { mcp: result.mcp } : {}
};
if (Object.keys(ctx.staticConfig).length > 0) meta.configs = ctx.staticConfig;
await options.mount?.(ctx, meta, api);
maybePrintBanner();
maybeRegister();
}
const initPromise = init();
initPromise.catch(() => {});
const contextPromise = initPromise.then(() => ctx);
contextPromise.catch(() => {});
/**
* The `unbound` tier: the RPC core and its crossws adapter, bound to
* nothing. Built on the first `attach` / `handleUpgrade` — a host that
* never wires the socket (or whose runtime brings its own WS transport)
* pays nothing for it, not even the adapter's imports.
*/
let transportPromise;
function ensureTransport() {
transportPromise ??= initPromise.then(async () => {
const [core, { attachWsRpcTransport }] = await Promise.all([ensureCore(), import("./rpc/transports/ws-server.mjs")]);
transport = attachWsRpcTransport(core.rpcGroup, {
unbound: true,
path: routePath,
allowedOrigins: options.allowedOrigins,
onConnected: core.onConnected,
onDisconnected: core.onDisconnected
});
return transport;
});
return transportPromise;
}
async function handleRequest(request) {
await initPromise;
noteOrigin(new URL(request.url).origin);
const response = await app.fetch(request);
if (response.status === 404) return new Response(null, { status: 404 });
return response;
}
let nodeHandler;
function nodeMiddleware(req, res, next) {
let pathname = req.url ?? "/";
try {
pathname = new URL(pathname, "http://localhost").pathname;
} catch {}
if (!(samePath(pathname, baseNoSlash) || pathname.startsWith(base))) {
if (next) {
next();
return;
}
res.statusCode = 404;
res.end();
return;
}
initPromise.then(async () => {
const host = req.headers.host;
if (host) {
const encrypted = req.socket.encrypted;
noteOrigin(`${encrypted ? "https" : "http"}://${host}`);
}
if (!nodeHandler) {
const { toNodeHandler } = await import("h3/node");
nodeHandler = toNodeHandler(app);
}
return nodeHandler(req, res);
}).catch((err) => {
if (next) {
next(err);
return;
}
res.statusCode = 500;
res.end();
});
}
/** The `unbound` tier is the only one whose socket the host may drive. */
function assertUnbound() {
if (tier === "disabled") throw diagnostics.DF0057();
if (tier === "external") throw diagnostics.DF0056({ url: ws.url });
if (tier !== "unbound") throw diagnostics.DF0055({ tier });
}
/**
* Publish the socket's absolute URL on the context, so surfaces that hand
* out a complete endpoint (the hub's remote docks) work on this tier too.
* {@link bindHttpAndWs} does the same for the tiers it owns.
*/
function publishWsEndpoint(server) {
const record = () => {
const address = server.address();
if (typeof address !== "object" || !address) return;
const host = options.host ?? (address.address === "::" || address.address === "0.0.0.0" ? "localhost" : address.address);
getInternalContext(ctx).setWsEndpoint({ url: `ws://${formatHostForUrl(host)}:${address.port}${routePath}` });
};
if (server.listening) record();
else server.once("listening", record);
}
function handleUpgrade(req, socket, head) {
assertUnbound();
if (transport) {
transport.handleUpgrade(req, socket, head);
return;
}
ensureTransport().then((live) => live.handleUpgrade(req, socket, head)).catch(() => socket.destroy());
}
function attach(server) {
assertUnbound();
server.on("upgrade", handleUpgrade);
ensureTransport().then(() => publishWsEndpoint(server)).catch(() => {});
return () => server.off("upgrade", handleUpgrade);
}
return {
base,
handler: handleRequest,
nodeMiddleware,
ready: initPromise,
context: contextPromise,
connectionMeta: () => meta ?? options.onMetaUnavailable(),
handleUpgrade,
attach,
async close() {
await initPromise.catch(() => {});
await registerPromise?.catch(() => {});
registration?.unregister();
await dispose?.();
await ssePromise?.then((live) => live.close()).catch(() => {});
await started?.close();
await transportPromise?.then((live) => live.close()).catch(() => {});
},
internals: {
get started() {
return started;
},
get authHandler() {
return authHandler;
}
}
};
}
//#endregion
export { normalizeHttpServerUrl as i, resolveInstanceRegister as n, samePath as r, createInstanceShell as t };
import { n as strictJsonStringify } from "./serialization-BGzEwAdr.mjs";
import { n as structuredCloneStringify, t as structuredCloneParse } from "./structured-clone-CbAV5rFI.mjs";
//#region src/rpc/wire-codec.ts
const EMPTY_WIRE_DEFS = /* @__PURE__ */ new Map();
/**
* Build the per-connection wire codec every live transport (WS server, WS
* client, SSE server, SSE client) shares: per-method dispatch between strict
* JSON (methods declared `jsonSerializable: true`) and `s:`-prefixed
* structured-clone (everything else, including all error envelopes), with a
* request-id → method map so a response independently picks the same
* encoder as its request. One codec per connection — request-id spaces
* don't collide across connections.
*
* @internal
* implementations; not part of the stable public API.
*/
function createRpcWireCodec(definitions = EMPTY_WIRE_DEFS) {
const pendingRequestMethods = /* @__PURE__ */ new Map();
return {
serialize: (msg) => {
let method;
if (msg.t === "q") method = msg.m;
else {
method = pendingRequestMethods.get(msg.i);
pendingRequestMethods.delete(msg.i);
}
if (!(msg.t === "s" && "e" in msg) && !!method && definitions.get(method)?.jsonSerializable === true) return strictJsonStringify(msg, method ?? "");
return `s:${structuredCloneStringify(msg)}`;
},
deserialize: (raw) => {
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
if (msg.t === "q" && msg.i && msg.m) pendingRequestMethods.set(msg.i, msg.m);
return msg;
}
};
}
/**
* Peek at a wire frame's birpc envelope without engaging a codec's
* request-id bookkeeping — used by the SSE transport to route a frame
* (park a POST for its response / answer with a bare 202) before it is
* handed to birpc proper.
*
* @internal
* implementations; not part of the stable public API.
*/
function peekRpcWireFrame(raw) {
try {
const msg = raw.startsWith("s:") ? structuredCloneParse(raw.slice(2)) : JSON.parse(raw);
return {
t: msg?.t,
i: msg?.i
};
} catch {
return {};
}
}
//#endregion
export { peekRpcWireFrame as n, createRpcWireCodec as t };
import "./constants.mjs";
import { t as createRpcWireCodec } from "./wire-codec-GsoRxIsD.mjs";
import { n as randomToken, r as timingSafeEqual } from "./crypto-token-XCqTSMg9.mjs";
import { createServer } from "node:http";
import { createServer as createServer$1 } from "node:https";
import crossws from "crossws/adapters/node";
//#region src/rpc/transports/session.ts
let sessionId = 0;
/**
* Mint the per-connection session meta every transport binding shares —
* one id space across transports, so session bookkeeping (streaming
* subscriptions, shared-state sync, auth trust) never collides between a
* WS peer and an SSE session on the same server.
*/
function createRpcSessionMeta() {
return {
id: sessionId++,
subscribedStates: /* @__PURE__ */ new Set()
};
}
//#endregion
//#region src/rpc/transports/ws-server.ts
/**
* Create a live, token-protected origin allowlist for external browser
* viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
* `registerFromUrl()` in the connection metadata handler to authorize a
* viewer without sharing a mutable array or disabling DNS-rebinding protection.
*/
function createWsOriginRegistry(options = {}) {
const token = randomToken();
const origins = new Set(options.allowedOrigins ?? []);
function normalizeOrigin(origin) {
if (!origin) return;
try {
const url = new URL(origin);
const normalized = url.origin === "null" ? `${url.protocol}//${url.host}` : url.origin;
return origin === normalized ? normalized : void 0;
} catch {}
}
function registerOrigin(origin, candidateToken) {
const normalized = normalizeOrigin(origin);
if (!normalized || !candidateToken || !timingSafeEqual(token, candidateToken)) return false;
if (options.validateOrigin && !options.validateOrigin(normalized)) return false;
origins.add(normalized);
return true;
}
return {
token,
registerFromUrl(url) {
let parsed;
try {
parsed = new URL(url, "http://localhost");
} catch {
return;
}
const origin = parsed.searchParams.get("devframe_viewer_origin") ?? void 0;
return registerOrigin(origin, parsed.searchParams.get("devframe_viewer_origin_token") ?? void 0) ? origin : void 0;
},
isAllowed(origin) {
return isAllowedOrigin(origin, [...origins]);
}
};
}
const EMPTY_DEFS = /* @__PURE__ */ new Map();
function NOOP() {}
function listen(server, port, host) {
return new Promise((resolve, reject) => {
const onError = (error) => reject(error);
server.once("error", onError);
try {
server.listen(port, host, () => {
server.off("error", onError);
resolve();
});
} catch (error) {
server.off("error", onError);
reject(error);
}
});
}
/** Compare two URL paths ignoring a trailing slash. */
function pathMatches(a, b) {
const strip = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
return strip(a) === strip(b);
}
function isLoopbackHostname(hostname) {
const h = hostname.replace(/^\[|\]$/g, "");
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost") || h.startsWith("127.");
}
/**
* Default origin policy for a localhost dev tool: allow requests with no
* `Origin` header (native, non-browser clients), allow any loopback origin
* (so cross-port localhost dev setups keep working), and allow explicitly
* configured origins. Everything else — a real remote page in the dev's
* browser — is rejected.
*/
function isAllowedOrigin(origin, allowedOrigins) {
if (!origin) return true;
if (allowedOrigins.includes(origin)) return true;
try {
return isLoopbackHostname(new URL(origin).hostname);
} catch {
return false;
}
}
function isWsOriginRegistry(value) {
return !!value && !Array.isArray(value);
}
/**
* Build the `upgrade` listener that hands a request to the crossws adapter,
* optionally filtered to a single `path`. Non-matching requests are left
* untouched so other upgrade listeners (e.g. a Vite dev server's HMR socket)
* can claim them, unless `destroyUnmatched` is set.
*/
function createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins) {
return (req, socket, head) => {
socket.on("error", () => {});
if (path) {
let pathname = req.url ?? "/";
try {
pathname = new URL(req.url ?? "/", "http://localhost").pathname;
} catch {}
if (!pathMatches(pathname, path)) {
if (destroyUnmatched) {
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
socket.destroy();
}
return;
}
}
const originAllowed = isWsOriginRegistry(allowedOrigins) ? allowedOrigins.isAllowed(req.headers.origin) : isAllowedOrigin(req.headers.origin, allowedOrigins || []);
if (allowedOrigins !== false && !originAllowed) {
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
socket.destroy();
return;
}
ws.handleUpgrade(req, socket, head);
};
}
/**
* The per-peer lifecycle hooks driving a devframe RPC WebSocket, shaped for
* any [crossws](https://crossws.h3.dev) adapter. {@link attachWsRpcTransport}
* feeds them to the Node adapter; runtime-specific attachments (e.g. Bun's
* fetch-upgrade adapter) reuse the same hooks so every transport speaks the
* identical wire protocol — one birpc channel per peer, per-method
* `jsonSerializable` dispatch between strict JSON and structured-clone.
*/
function createWsRpcPeerHooks(rpcGroup, options = {}) {
const { onConnected = NOOP, onDisconnected = NOOP, definitions = EMPTY_DEFS, serialize: serializeOverride, deserialize: deserializeOverride } = options;
const states = /* @__PURE__ */ new WeakMap();
return {
open: (peer) => {
const meta = createRpcSessionMeta();
meta.peer = peer;
const connection = {
id: meta.id,
transport: "websocket",
request: peer.request,
send: (data) => peer.send(data),
close: (code, reason) => peer.close(code, reason),
peer
};
const codec = createRpcWireCodec(definitions);
const state = {
meta,
connection,
channel: void 0
};
const channel = {
post: (data) => {
peer.send(data);
},
on: (fn) => {
state.onMessage = fn;
},
serialize: serializeOverride ?? codec.serialize,
deserialize: deserializeOverride ?? codec.deserialize,
meta
};
state.channel = channel;
states.set(peer, state);
rpcGroup.updateChannels((channels) => {
channels.push(channel);
});
onConnected(connection, meta);
},
message: (peer, message) => {
states.get(peer)?.onMessage?.(message.text());
},
close: (peer) => {
const state = states.get(peer);
if (!state) return;
states.delete(peer);
rpcGroup.updateChannels((channels) => {
const index = channels.indexOf(state.channel);
if (index >= 0) channels.splice(index, 1);
});
onDisconnected(state.connection, state.meta);
}
};
}
/**
* Attach a WebSocket transport to an existing RPC group, powered by
* [crossws](https://crossws.h3.dev). Either attach to an existing HTTP(S)
* `server` (sharing its port, optionally scoped to a `path`), or let this
* helper create a standalone server from `port` / `host` / `https`.
*
* Returns the crossws node adapter, standalone-server readiness/address
* accessors, `detach` (remove the upgrade listener from a shared `server`),
* and `close` (full deterministic teardown).
*/
function attachWsRpcTransport(rpcGroup, options = {}) {
const { server, port, host = "localhost", path, destroyUnmatched = false, unbound, https, allowedOrigins } = options;
const ws = crossws({ hooks: createWsRpcPeerHooks(rpcGroup, options) });
const sharedUpgradeListener = createUpgradeListener(ws, path, destroyUnmatched, allowedOrigins);
const ownedUpgradeListener = createUpgradeListener(ws, path, true, allowedOrigins);
/** Bind a server's `upgrade` events, tracked so `close()` detaches them. */
const attachments = /* @__PURE__ */ new Set();
function attachTo(target, listener) {
target.on("upgrade", listener);
const detachOne = () => {
target.off("upgrade", listener);
attachments.delete(detachOne);
};
attachments.add(detachOne);
return detachOne;
}
let ready = Promise.resolve();
let ownedServer;
if (unbound) {} else if (server) attachTo(server, sharedUpgradeListener);
else if (https) {
ownedServer = createServer$1(https);
attachTo(ownedServer, ownedUpgradeListener);
ready = listen(ownedServer, port ?? 0, host);
} else {
ownedServer = createServer((_req, res) => {
res.writeHead(426, { "content-type": "text/plain" });
res.end("Upgrade Required");
});
attachTo(ownedServer, ownedUpgradeListener);
ready = listen(ownedServer, port ?? 0, host);
}
const activeServer = server ?? ownedServer;
function detachAll() {
for (const detachOne of [...attachments]) detachOne();
}
return {
ws,
ready,
address: () => activeServer?.address() ?? null,
handleUpgrade: sharedUpgradeListener,
attach: (target) => attachTo(target, sharedUpgradeListener),
detach: detachAll,
async close() {
detachAll();
ws.closeAll(void 0, void 0, true);
if (ownedServer) {
const srv = ownedServer;
await ready.catch(() => {});
if (!srv.listening) return;
await new Promise((r) => srv.close(() => r()));
}
}
};
}
//#endregion
export { isLoopbackHostname as a, isAllowedOrigin as i, createWsOriginRegistry as n, createRpcSessionMeta as o, createWsRpcPeerHooks as r, attachWsRpcTransport as t };