| import { F as DevframeDefinition, G as McpRouteOptions, I as DevframeDeploymentKind, t as ConnectionMeta } from "./context-c0OlSc12.mjs"; | ||
| //#region src/adapters/_shared.d.ts | ||
| /** | ||
| * 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. | ||
| */ | ||
| declare function resolveBasePath(def: DevframeDefinition, kind: DevframeDeploymentKind): string; | ||
| declare function normalizeBasePath(base: string): string; | ||
| interface ResolveDevServerPortOptions { | ||
| /** Bind host (passed to `get-port-please` for in-use detection). */ | ||
| host?: string; | ||
| /** Override the preferred port. Default: `def.cli?.port ?? 9999`. */ | ||
| defaultPort?: number; | ||
| } | ||
| /** | ||
| * 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. | ||
| */ | ||
| declare function resolveDevServerPort(def: DevframeDefinition, options?: ResolveDevServerPortOptions): Promise<number>; | ||
| /** | ||
| * 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). | ||
| */ | ||
| declare function resolveMcpConnectionMeta(def: DevframeDefinition, mcp: boolean | McpRouteOptions | undefined, port?: number): ConnectionMeta['mcp']; | ||
| //#endregion | ||
| export { resolveMcpConnectionMeta as a, resolveDevServerPort as i, normalizeBasePath as n, resolveBasePath as r, ResolveDevServerPortOptions as t }; |
| import { i as defineDiagnostics } from "./nostics-CzECRXpE.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-CD8nlgll.mjs"; | ||
| import { RpcFunctionsCollectorBase } from "./rpc/index.mjs"; | ||
| import { defineRpcFunction } from "./index.mjs"; | ||
| import { t as DEVFRAME_EVENTS } from "./events-DzolaBEs.mjs"; | ||
| import { DEVFRAME_SERVICES_STATE_KEY } from "./constants.mjs"; | ||
| import { t as diagnostics$1 } from "./diagnostics-BXWW3VK-.mjs"; | ||
| import { r as createEventEmitter, t as DevframeAgentHost } from "./host-agent-DbPWo0Bl.mjs"; | ||
| import { n as createDebug, t as resolveStaticAssetsSource } from "./remote-assets-DezQmPQU.mjs"; | ||
| import { t as createStorage } from "./storage-BXNVhKyR.mjs"; | ||
| import { createRequire } from "node:module"; | ||
| import { createSharedState } from "devframe/utils/shared-state"; | ||
| import process from "node:process"; | ||
| import { pathToFileURL } from "node:url"; | ||
| import { isAbsolute, join } from "pathe"; | ||
| import { existsSync } from "node:fs"; | ||
| //#region src/node/host-diagnostics.ts | ||
| var DevframeDiagnosticsHost = class { | ||
| context; | ||
| _registry = {}; | ||
| logger = new Proxy({}, { get: (_, code) => this._registry[code] }); | ||
| defineDiagnostics = defineDiagnostics; | ||
| constructor(context, initialDefinitions = []) { | ||
| this.context = context; | ||
| for (const d of initialDefinitions) this.register(d); | ||
| } | ||
| register(diagnostics) { | ||
| Object.assign(this._registry, diagnostics); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/rpc-shared-state.ts | ||
| const debug$2 = createDebug("devframe:rpc:state:changed"); | ||
| const debugSubscribe = createDebug("devframe:rpc:state:subscribe"); | ||
| function createRpcSharedStateServerHost(rpc) { | ||
| const sharedState = /* @__PURE__ */ new Map(); | ||
| const stateDisposers = /* @__PURE__ */ new Map(); | ||
| const keyAddedListeners = /* @__PURE__ */ new Set(); | ||
| function registerSharedState(key, state) { | ||
| const offs = []; | ||
| offs.push(state.on("updated", (fullState, patches, syncId) => { | ||
| if (patches) { | ||
| debug$2("patch", { | ||
| key, | ||
| syncId | ||
| }); | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.clientStatePatch, | ||
| args: [ | ||
| key, | ||
| patches, | ||
| syncId | ||
| ], | ||
| filter: (client) => client.$meta.subscribedStates.has(key) | ||
| }); | ||
| } else { | ||
| debug$2("updated", { | ||
| key, | ||
| syncId | ||
| }); | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.clientStateUpdated, | ||
| args: [ | ||
| key, | ||
| fullState, | ||
| syncId | ||
| ], | ||
| filter: (client) => client.$meta.subscribedStates.has(key) | ||
| }); | ||
| } | ||
| })); | ||
| return () => { | ||
| for (const off of offs) off(); | ||
| }; | ||
| } | ||
| const host = { | ||
| get: async (key, options) => { | ||
| if (sharedState.has(key)) return sharedState.get(key); | ||
| if (options?.initialValue === void 0 && options?.sharedState === void 0) throw diagnostics$1.DF0013({ key }); | ||
| debug$2("new-state", key); | ||
| const state = options.sharedState ?? createSharedState({ | ||
| initialValue: options.initialValue, | ||
| enablePatches: false | ||
| }); | ||
| stateDisposers.set(key, registerSharedState(key, state)); | ||
| sharedState.set(key, state); | ||
| for (const fn of keyAddedListeners) fn(key); | ||
| return state; | ||
| }, | ||
| keys() { | ||
| return Array.from(sharedState.keys()); | ||
| }, | ||
| onKeyAdded(fn) { | ||
| keyAddedListeners.add(fn); | ||
| return () => { | ||
| keyAddedListeners.delete(fn); | ||
| }; | ||
| }, | ||
| delete(key) { | ||
| const dispose = stateDisposers.get(key); | ||
| if (!dispose) return false; | ||
| dispose(); | ||
| stateDisposers.delete(key); | ||
| sharedState.delete(key); | ||
| return true; | ||
| } | ||
| }; | ||
| rpc.register({ | ||
| name: "devframe:rpc:server-state:subscribe", | ||
| type: "event", | ||
| handler(key) { | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (!session) return; | ||
| debugSubscribe("subscribe", { | ||
| key, | ||
| session: session.meta.id | ||
| }); | ||
| session.meta.subscribedStates.add(key); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:rpc:server-state:get", | ||
| type: "query", | ||
| handler: async (key) => { | ||
| if (!sharedState.has(key)) return void 0; | ||
| return (await host.get(key)).value(); | ||
| }, | ||
| dump: () => ({ inputs: host.keys().map((key) => [key]) }) | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:rpc:server-state:set", | ||
| type: "query", | ||
| handler: async (key, value, syncId) => { | ||
| (await host.get(key, { initialValue: value })).mutate(() => value, syncId); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:rpc:server-state:patch", | ||
| type: "query", | ||
| handler: async (key, patches, syncId) => { | ||
| if (!sharedState.has(key)) return; | ||
| (await host.get(key)).patch(patches, syncId); | ||
| } | ||
| }); | ||
| return host; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/nanoid.ts | ||
| const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; | ||
| function nanoid(size = 21) { | ||
| let id = ""; | ||
| let i = size; | ||
| while (i--) id += urlAlphabet[Math.random() * 64 | 0]; | ||
| return id; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/streaming-channel.ts | ||
| const DEFAULT_HIGH_WATER_MARK = 256; | ||
| var StreamClosedError = class extends Error { | ||
| name = "StreamClosedError"; | ||
| }; | ||
| /** | ||
| * Build a server-side stream sink. RPC-agnostic — the RPC host wires | ||
| * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay | ||
| * for late or reconnecting subscribers. | ||
| */ | ||
| function createStreamSink(options = {}) { | ||
| const id = options.id ?? nanoid(); | ||
| const replayWindow = Math.max(0, options.replayWindow ?? 0); | ||
| const events = createEventEmitter(); | ||
| const controller = new AbortController(); | ||
| const buffer = []; | ||
| let closed = false; | ||
| let lastSeq = 0; | ||
| function write(chunk) { | ||
| if (closed) throw new StreamClosedError(`Cannot write to a closed stream "${id}"`); | ||
| lastSeq += 1; | ||
| if (replayWindow > 0) { | ||
| buffer.push({ | ||
| seq: lastSeq, | ||
| chunk | ||
| }); | ||
| if (buffer.length > replayWindow) buffer.splice(0, buffer.length - replayWindow); | ||
| } | ||
| events.emit("chunk", lastSeq, chunk); | ||
| } | ||
| function error(reason) { | ||
| if (closed) return; | ||
| closed = true; | ||
| const payload = toErrorPayload(reason); | ||
| controller.abort(reason); | ||
| events.emit("end", payload); | ||
| } | ||
| function close() { | ||
| if (closed) return; | ||
| closed = true; | ||
| if (!controller.signal.aborted) controller.abort("stream closed"); | ||
| events.emit("end", void 0); | ||
| } | ||
| function abort(reason) { | ||
| if (closed) return; | ||
| if (!controller.signal.aborted) controller.abort(reason ?? "aborted"); | ||
| } | ||
| const writable = new WritableStream({ | ||
| write(chunk) { | ||
| write(chunk); | ||
| }, | ||
| close() { | ||
| close(); | ||
| }, | ||
| abort(reason) { | ||
| error(reason); | ||
| } | ||
| }); | ||
| return { | ||
| id, | ||
| signal: controller.signal, | ||
| get closed() { | ||
| return closed; | ||
| }, | ||
| get lastSeq() { | ||
| return lastSeq; | ||
| }, | ||
| write, | ||
| error, | ||
| close, | ||
| abort, | ||
| writable, | ||
| events, | ||
| buffer | ||
| }; | ||
| } | ||
| /** | ||
| * Build a client-side stream reader. RPC-agnostic — the RPC host calls | ||
| * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the | ||
| * terminal frame. Consumers iterate with `for await` or pipe `readable`. | ||
| */ | ||
| function createStreamReader(options = {}) { | ||
| const id = options.id ?? nanoid(); | ||
| const highWaterMark = Math.max(1, options.highWaterMark ?? DEFAULT_HIGH_WATER_MARK); | ||
| const queue = []; | ||
| let lastSeenSeq = 0; | ||
| let done = false; | ||
| let cancelled = false; | ||
| let endError; | ||
| let pending; | ||
| let pullController; | ||
| let readableInstance; | ||
| function drainNext() { | ||
| if (!pending) return; | ||
| if (queue.length > 0) { | ||
| const value = queue.shift(); | ||
| const r = pending; | ||
| pending = void 0; | ||
| r.resolve({ | ||
| value, | ||
| done: false | ||
| }); | ||
| return; | ||
| } | ||
| if (done) { | ||
| const r = pending; | ||
| pending = void 0; | ||
| if (endError) { | ||
| const err = new Error(endError.message); | ||
| err.name = endError.name; | ||
| r.reject(err); | ||
| } else r.resolve({ | ||
| value: void 0, | ||
| done: true | ||
| }); | ||
| } | ||
| } | ||
| function feedReadable() { | ||
| if (!pullController) return; | ||
| while (queue.length > 0) { | ||
| const v = queue.shift(); | ||
| try { | ||
| pullController.enqueue(v); | ||
| } catch { | ||
| break; | ||
| } | ||
| } | ||
| if (done && pullController) { | ||
| try { | ||
| if (endError) { | ||
| const err = new Error(endError.message); | ||
| err.name = endError.name; | ||
| pullController.error(err); | ||
| } else pullController.close(); | ||
| } catch {} | ||
| pullController = void 0; | ||
| } | ||
| } | ||
| function push(seq, chunk) { | ||
| if (done || cancelled) return; | ||
| if (seq <= lastSeenSeq) return; | ||
| lastSeenSeq = seq; | ||
| queue.push(chunk); | ||
| if (queue.length > highWaterMark) { | ||
| const overflow = queue.length - highWaterMark; | ||
| queue.splice(0, overflow); | ||
| options.onOverflow?.(overflow); | ||
| } | ||
| drainNext(); | ||
| if (readableInstance) feedReadable(); | ||
| } | ||
| function end(error) { | ||
| if (done) return; | ||
| done = true; | ||
| endError = error; | ||
| drainNext(); | ||
| if (readableInstance) feedReadable(); | ||
| } | ||
| function cancel() { | ||
| if (cancelled || done) return; | ||
| cancelled = true; | ||
| options.onCancel?.(); | ||
| end(void 0); | ||
| } | ||
| function getReadable() { | ||
| if (readableInstance) return readableInstance; | ||
| readableInstance = new ReadableStream({ | ||
| start(controller) { | ||
| pullController = controller; | ||
| feedReadable(); | ||
| }, | ||
| cancel() { | ||
| cancel(); | ||
| } | ||
| }); | ||
| return readableInstance; | ||
| } | ||
| return { | ||
| id, | ||
| get cancelled() { | ||
| return cancelled; | ||
| }, | ||
| get done() { | ||
| return done; | ||
| }, | ||
| get lastSeenSeq() { | ||
| return lastSeenSeq; | ||
| }, | ||
| get readable() { | ||
| return getReadable(); | ||
| }, | ||
| cancel, | ||
| _push: push, | ||
| _end: end, | ||
| [Symbol.asyncIterator]() { | ||
| return { | ||
| next() { | ||
| if (queue.length > 0) return Promise.resolve({ | ||
| value: queue.shift(), | ||
| done: false | ||
| }); | ||
| if (done) { | ||
| if (endError) { | ||
| const err = new Error(endError.message); | ||
| err.name = endError.name; | ||
| return Promise.reject(err); | ||
| } | ||
| return Promise.resolve({ | ||
| value: void 0, | ||
| done: true | ||
| }); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| pending = { | ||
| resolve, | ||
| reject | ||
| }; | ||
| }); | ||
| }, | ||
| return() { | ||
| cancel(); | ||
| return Promise.resolve({ | ||
| value: void 0, | ||
| done: true | ||
| }); | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| function toErrorPayload(reason) { | ||
| if (reason instanceof Error) return { | ||
| name: reason.name || "Error", | ||
| message: reason.message | ||
| }; | ||
| if (typeof reason === "string") return { | ||
| name: "Error", | ||
| message: reason | ||
| }; | ||
| try { | ||
| return { | ||
| name: "Error", | ||
| message: JSON.stringify(reason) | ||
| }; | ||
| } catch { | ||
| return { | ||
| name: "Error", | ||
| message: String(reason) | ||
| }; | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/node/rpc-streaming.ts | ||
| const debug$1 = createDebug("devframe:rpc:streaming"); | ||
| const STREAM_KEY_SEPARATOR = ""; | ||
| function streamKey(channel, id) { | ||
| return `${channel}${STREAM_KEY_SEPARATOR}${id}`; | ||
| } | ||
| /** | ||
| * Build the server-side streaming host. Mirrors the layout of | ||
| * `createRpcSharedStateServerHost` — registers a fixed set of internal | ||
| * RPC methods (`subscribe` / `unsubscribe` / `cancel`) once, then per-channel | ||
| * state lives in a `Map<channelName, ChannelState>`. | ||
| */ | ||
| function createRpcStreamingServerHost(rpc) { | ||
| const channels = /* @__PURE__ */ new Map(); | ||
| function findStream(channelName, id) { | ||
| return channels.get(channelName)?.streams.get(id); | ||
| } | ||
| function freeStreamNow(state, id) { | ||
| const record = state.streams.get(id); | ||
| if (!record) return; | ||
| if (record.retentionTimer) { | ||
| clearTimeout(record.retentionTimer); | ||
| record.retentionTimer = void 0; | ||
| } | ||
| for (const off of record.unbinders) off(); | ||
| state.streams.delete(id); | ||
| debug$1("freed", state.name, id); | ||
| } | ||
| function maybeFreeStream(state, id) { | ||
| const record = state.streams.get(id); | ||
| if (!record) return; | ||
| if (!record.sink.closed || record.subscribers.size > 0) return; | ||
| const retention = state.options.closedStreamRetention; | ||
| if (retention <= 0) { | ||
| freeStreamNow(state, id); | ||
| return; | ||
| } | ||
| if (record.retentionTimer) return; | ||
| record.retentionTimer = setTimeout(freeStreamNow, retention, state, id); | ||
| } | ||
| function cancelRetention(record) { | ||
| if (record.retentionTimer) { | ||
| clearTimeout(record.retentionTimer); | ||
| record.retentionTimer = void 0; | ||
| } | ||
| } | ||
| rpc.register({ | ||
| name: "devframe:streaming:subscribe", | ||
| type: "event", | ||
| handler(channelName, id, opts) { | ||
| const state = channels.get(channelName); | ||
| if (!state) { | ||
| diagnostics$1.DF0030({ | ||
| channel: channelName, | ||
| id | ||
| }, { method: "error" }); | ||
| return; | ||
| } | ||
| const record = state.streams.get(id); | ||
| if (!record) { | ||
| diagnostics$1.DF0030({ | ||
| channel: channelName, | ||
| id | ||
| }, { method: "error" }); | ||
| return; | ||
| } | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (!session) return; | ||
| const key = streamKey(channelName, id); | ||
| session.meta.subscribedStreams ??= /* @__PURE__ */ new Set(); | ||
| session.meta.subscribedStreams.add(key); | ||
| record.subscribers.add(session.meta); | ||
| cancelRetention(record); | ||
| const afterSeq = opts?.afterSeq ?? 0; | ||
| for (const buffered of record.sink.buffer) if (buffered.seq > afterSeq) rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingChunk, | ||
| args: [ | ||
| channelName, | ||
| id, | ||
| buffered.seq, | ||
| buffered.chunk | ||
| ], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => client.$meta === session.meta | ||
| }); | ||
| if (record.sink.closed) rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingEnd, | ||
| args: [ | ||
| channelName, | ||
| id, | ||
| void 0 | ||
| ], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => client.$meta === session.meta | ||
| }); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:streaming:unsubscribe", | ||
| type: "event", | ||
| handler(channelName, id) { | ||
| const state = channels.get(channelName); | ||
| const record = state?.streams.get(id); | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (!session) return; | ||
| session.meta.subscribedStreams?.delete(streamKey(channelName, id)); | ||
| if (state && record) { | ||
| record.subscribers.delete(session.meta); | ||
| maybeFreeStream(state, id); | ||
| } | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:streaming:cancel", | ||
| type: "event", | ||
| handler(channelName, id) { | ||
| const record = findStream(channelName, id); | ||
| if (!record) return; | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (!session) return; | ||
| record.subscribers.delete(session.meta); | ||
| session.meta.subscribedStreams?.delete(streamKey(channelName, id)); | ||
| if (record.subscribers.size === 0) record.sink.abort("cancelled by client"); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:streaming:upload-chunk", | ||
| type: "event", | ||
| handler(channelName, id, seq, chunk) { | ||
| const record = channels.get(channelName)?.inbound.get(id); | ||
| if (!record) { | ||
| diagnostics$1.DF0030({ | ||
| channel: channelName, | ||
| id | ||
| }, { method: "error" }); | ||
| return; | ||
| } | ||
| if (!record.uploaderMeta) { | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (session) { | ||
| record.uploaderMeta = session.meta; | ||
| session.meta.uploadingStreams ??= /* @__PURE__ */ new Set(); | ||
| session.meta.uploadingStreams.add(streamKey(channelName, id)); | ||
| } | ||
| } | ||
| record.reader._push(seq, chunk); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:streaming:upload-end", | ||
| type: "event", | ||
| handler(channelName, id, error) { | ||
| const state = channels.get(channelName); | ||
| const record = state?.inbound.get(id); | ||
| if (!record) return; | ||
| record.reader._end(error); | ||
| if (record.uploaderMeta) record.uploaderMeta.uploadingStreams?.delete(streamKey(channelName, id)); | ||
| state?.inbound.delete(id); | ||
| } | ||
| }); | ||
| function createChannel(name, opts = {}) { | ||
| if (channels.has(name)) throw diagnostics$1.DF0032({ channel: name }); | ||
| const replayWindow = opts.replayWindow ?? 0; | ||
| const state = { | ||
| name, | ||
| options: { | ||
| replayWindow, | ||
| closedStreamRetention: opts.closedStreamRetention ?? (replayWindow > 0 ? 3e4 : 0) | ||
| }, | ||
| streams: /* @__PURE__ */ new Map(), | ||
| inbound: /* @__PURE__ */ new Map() | ||
| }; | ||
| channels.set(name, state); | ||
| function start(startOpts = {}) { | ||
| const sink = createStreamSink({ | ||
| id: startOpts.id, | ||
| replayWindow: state.options.replayWindow | ||
| }); | ||
| const record = { | ||
| sink, | ||
| subscribers: /* @__PURE__ */ new Set(), | ||
| unbinders: [] | ||
| }; | ||
| state.streams.set(sink.id, record); | ||
| record.unbinders.push(sink.events.on("chunk", (seq, chunk) => { | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingChunk, | ||
| args: [ | ||
| name, | ||
| sink.id, | ||
| seq, | ||
| chunk | ||
| ], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => record.subscribers.has(client.$meta) | ||
| }); | ||
| })); | ||
| record.unbinders.push(sink.events.on("end", (error) => { | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingEnd, | ||
| args: [ | ||
| name, | ||
| sink.id, | ||
| error | ||
| ], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => record.subscribers.has(client.$meta) | ||
| }); | ||
| maybeFreeStream(state, sink.id); | ||
| })); | ||
| return sink; | ||
| } | ||
| async function pipeFrom(readable, startOpts = {}) { | ||
| const sink = start(startOpts); | ||
| readable.pipeTo(sink.writable, { signal: sink.signal }).catch(() => {}); | ||
| return sink; | ||
| } | ||
| function get(id) { | ||
| return state.streams.get(id)?.sink; | ||
| } | ||
| function ids() { | ||
| return Array.from(state.streams.keys()); | ||
| } | ||
| function openInbound(inboundOpts = {}) { | ||
| let inboundRecord; | ||
| const reader = createStreamReader({ | ||
| id: inboundOpts.id, | ||
| onCancel() { | ||
| const targetMeta = inboundRecord?.uploaderMeta; | ||
| if (!targetMeta) return; | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingUploadCancel, | ||
| args: [name, reader.id], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => client.$meta === targetMeta | ||
| }); | ||
| } | ||
| }); | ||
| inboundRecord = { reader }; | ||
| state.inbound.set(reader.id, inboundRecord); | ||
| debug$1("opened-inbound", name, reader.id); | ||
| return reader; | ||
| } | ||
| return { | ||
| name, | ||
| start, | ||
| pipeFrom, | ||
| get, | ||
| ids, | ||
| openInbound | ||
| }; | ||
| } | ||
| function parseKey(key) { | ||
| const sepIdx = key.indexOf(STREAM_KEY_SEPARATOR); | ||
| if (sepIdx < 0) return void 0; | ||
| return { | ||
| channelName: key.slice(0, sepIdx), | ||
| id: key.slice(sepIdx + 1) | ||
| }; | ||
| } | ||
| return { | ||
| create: createChannel, | ||
| _onSessionDisconnected(meta) { | ||
| if (meta.subscribedStreams) { | ||
| for (const key of meta.subscribedStreams) { | ||
| const parsed = parseKey(key); | ||
| if (!parsed) continue; | ||
| const state = channels.get(parsed.channelName); | ||
| const record = state?.streams.get(parsed.id); | ||
| if (!state || !record) continue; | ||
| record.subscribers.delete(meta); | ||
| if (record.subscribers.size === 0 && !record.sink.closed) record.sink.abort("all subscribers disconnected"); | ||
| maybeFreeStream(state, parsed.id); | ||
| } | ||
| meta.subscribedStreams.clear(); | ||
| } | ||
| if (meta.uploadingStreams) { | ||
| for (const key of meta.uploadingStreams) { | ||
| const parsed = parseKey(key); | ||
| if (!parsed) continue; | ||
| const state = channels.get(parsed.channelName); | ||
| const record = state?.inbound.get(parsed.id); | ||
| if (!state || !record) continue; | ||
| record.reader._end({ | ||
| name: "UploadDisconnected", | ||
| message: "Uploader disconnected before completing the stream" | ||
| }); | ||
| state.inbound.delete(parsed.id); | ||
| } | ||
| meta.uploadingStreams.clear(); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/node/host-functions.ts | ||
| const debugBroadcast = createDebug("devframe:rpc:broadcast"); | ||
| /** | ||
| * Concrete implementation backing `ctx.rpc`. Internal: consumers should | ||
| * depend on the structural {@link RpcFunctionsHost} type, never this class. | ||
| * Its `@internal` members (`_rpcGroup`, `_asyncStorage`, | ||
| * `_emitSessionDisconnected`) are wired by `createContextRpcServer` and must not | ||
| * widen the public surface. | ||
| * | ||
| * @internal | ||
| */ | ||
| var RpcFunctionsHostImpl = class extends RpcFunctionsCollectorBase { | ||
| /** | ||
| * @internal | ||
| */ | ||
| _rpcGroup = void 0; | ||
| _asyncStorage = void 0; | ||
| constructor(context) { | ||
| super(context); | ||
| this.sharedState = createRpcSharedStateServerHost(this); | ||
| this.streaming = createRpcStreamingServerHost(this); | ||
| } | ||
| sharedState; | ||
| streaming; | ||
| /** | ||
| * Adapters call this from their WS `onDisconnected` hook so downstream | ||
| * hosts (streaming, …) can free per-session state. Public-ish because | ||
| * tests / custom adapters may want to mirror it. | ||
| * | ||
| * @internal | ||
| */ | ||
| _emitSessionDisconnected(meta) { | ||
| this.streaming._onSessionDisconnected(meta); | ||
| } | ||
| async invokeLocal(method, ...args) { | ||
| if (!this.definitions.has(method)) throw diagnostics$1.DF0006({ name: String(method) }); | ||
| const handler = await this.getHandler(method); | ||
| return await Promise.resolve(handler(...args)); | ||
| } | ||
| async broadcast(options) { | ||
| if (!this._rpcGroup) return; | ||
| debugBroadcast(JSON.stringify(options.method)); | ||
| await Promise.allSettled(this._rpcGroup.clients.map((client) => { | ||
| if (options.filter?.(client) === false) return void 0; | ||
| return client.$callRaw({ | ||
| optional: true, | ||
| event: true, | ||
| ...options | ||
| }); | ||
| })); | ||
| } | ||
| getCurrentRpcSession() { | ||
| if (!this._asyncStorage) throw diagnostics$1.DF0007(); | ||
| return this._asyncStorage.getStore(); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/services-install.ts | ||
| /** | ||
| * Turn a `resolveFrom` value (a file path, a file URL like `import.meta.url`, | ||
| * or a directory) into something `createRequire` accepts — a directory gets a | ||
| * synthetic filename appended so resolution starts inside it. | ||
| */ | ||
| function toRequireBase(resolveFrom) { | ||
| if (resolveFrom.startsWith("file://")) return resolveFrom; | ||
| if ((resolveFrom.split(/[/\\]/).pop() ?? "").includes(".")) return resolveFrom; | ||
| return join(resolveFrom, "_devframe_resolve.js"); | ||
| } | ||
| /** | ||
| * Normalize an `install()` `resolveFrom` into a resolution base. Paths and | ||
| * file URLs pass through (the common case: the declaring plugin's | ||
| * `importMetaUrl`, so a service it declares resolves against the plugin's own | ||
| * dependencies); a bare npm package name resolves to that package's location | ||
| * from `cwd`. An unresolvable package name reads as no base (the caller's | ||
| * workspace fallbacks apply). | ||
| */ | ||
| function expandResolveFrom(resolveFrom, cwd) { | ||
| if (resolveFrom.startsWith("file://") || resolveFrom.startsWith(".") || isAbsolute(resolveFrom)) return resolveFrom; | ||
| const require = createRequire(join(cwd, "_devframe_resolve.js")); | ||
| try { | ||
| return require.resolve(`${resolveFrom}/package.json`); | ||
| } catch {} | ||
| try { | ||
| return require.resolve(resolveFrom); | ||
| } catch {} | ||
| } | ||
| /** | ||
| * Import a service package's module, trying each `resolveFrom` candidate in | ||
| * order (so a plugin-declared service resolves against the plugin's own | ||
| * dependency tree first, then the workspace fallback). Throws the last | ||
| * resolution error when no candidate succeeds. | ||
| */ | ||
| async function importServicePackage(pkg, resolveFroms) { | ||
| const candidates = [...new Set(resolveFroms.filter((x) => typeof x === "string" && x.length > 0))]; | ||
| let lastError = /* @__PURE__ */ new Error(`no resolution base available for "${pkg}"`); | ||
| for (const from of candidates) { | ||
| let resolved; | ||
| try { | ||
| resolved = createRequire(toRequireBase(from)).resolve(pkg); | ||
| } catch (error) { | ||
| lastError = error; | ||
| continue; | ||
| } | ||
| return await import( | ||
| /* webpackIgnore: true */ | ||
| /* @vite-ignore */ | ||
| /* turbopackIgnore: true */ | ||
| pathToFileURL(resolved).href | ||
| ); | ||
| } | ||
| throw lastError; | ||
| } | ||
| function parseVersion(input) { | ||
| const [core, ...prerelease] = input.trim().replace(/^v/, "").split("-"); | ||
| if (!core) return void 0; | ||
| const parts = core.split(".").map((part) => Number.parseInt(part, 10)); | ||
| if (parts.length === 0 || parts.some((part) => Number.isNaN(part) || part < 0)) return void 0; | ||
| while (parts.length < 3) parts.push(0); | ||
| return { | ||
| parts, | ||
| ...prerelease.length ? { prerelease: prerelease.join("-") } : {} | ||
| }; | ||
| } | ||
| function compareVersions(a, b) { | ||
| for (let i = 0; i < 3; i++) { | ||
| const diff = (a.parts[i] ?? 0) - (b.parts[i] ?? 0); | ||
| if (diff !== 0) return diff; | ||
| } | ||
| if (a.prerelease && !b.prerelease) return -1; | ||
| if (!a.prerelease && b.prerelease) return 1; | ||
| if (a.prerelease && b.prerelease) return a.prerelease < b.prerelease ? -1 : a.prerelease > b.prerelease ? 1 : 0; | ||
| return 0; | ||
| } | ||
| function satisfiesComparator(version, comparator) { | ||
| const raw = comparator.trim(); | ||
| if (!raw || raw === "*" || raw === "x") return true; | ||
| const operatorMatch = raw.match(/^([\^~]|>=|<=|[><=])?(.+)$/); | ||
| if (!operatorMatch) return false; | ||
| const operator = operatorMatch[1]; | ||
| const rest = operatorMatch[2].trim(); | ||
| const segments = rest.replace(/\.[x*]/gi, "").split(".").filter(Boolean); | ||
| const base = parseVersion(rest.replace(/[x*]/gi, "0")); | ||
| if (!base) return false; | ||
| switch (operator) { | ||
| case ">": return compareVersions(version, base) > 0; | ||
| case ">=": return compareVersions(version, base) >= 0; | ||
| case "<": return compareVersions(version, base) < 0; | ||
| case "<=": return compareVersions(version, base) <= 0; | ||
| case "^": { | ||
| if (compareVersions(version, base) < 0) return false; | ||
| const fixedIndex = base.parts.findIndex((part) => part !== 0); | ||
| const lockUpTo = fixedIndex === -1 ? base.parts.length - 1 : fixedIndex; | ||
| for (let i = 0; i <= lockUpTo; i++) if ((version.parts[i] ?? 0) !== (base.parts[i] ?? 0)) return false; | ||
| return true; | ||
| } | ||
| case "~": { | ||
| if (compareVersions(version, base) < 0) return false; | ||
| const lockUpTo = segments.length >= 2 ? 1 : 0; | ||
| for (let i = 0; i <= lockUpTo; i++) if ((version.parts[i] ?? 0) !== (base.parts[i] ?? 0)) return false; | ||
| return true; | ||
| } | ||
| default: | ||
| for (let i = 0; i < Math.max(segments.length, 3); i++) if (i < segments.length && (version.parts[i] ?? 0) !== (base.parts[i] ?? 0)) return false; | ||
| return segments.length >= 3 ? compareVersions(version, base) === 0 : true; | ||
| } | ||
| } | ||
| /** | ||
| * Pragmatic semver range check for service version declarations — supports | ||
| * the common forms (`1.2.3`, `^1.2.3`, `~1.2`, `>=1 <3`, `1.x`, `*`, and | ||
| * `||`-joined alternatives) without pulling in a semver dependency. An | ||
| * unparseable version or range reads as **not satisfied**. | ||
| */ | ||
| function satisfiesVersionRange(version, range) { | ||
| const parsed = parseVersion(version); | ||
| if (!parsed) return false; | ||
| const alternatives = range.split("||").map((alt) => alt.trim()).filter(Boolean); | ||
| if (alternatives.length === 0) return true; | ||
| return alternatives.some((alternative) => alternative.split(/\s+/).every((comparator) => satisfiesComparator(parsed, comparator))); | ||
| } | ||
| function isPlainObject(value) { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value); | ||
| } | ||
| /** Deep-merge two values with the service option-set rules (see below). */ | ||
| function deepMergeTwo(a, b) { | ||
| if (Array.isArray(a) && Array.isArray(b)) return [.../* @__PURE__ */ new Set([...a, ...b])]; | ||
| if (isPlainObject(a) && isPlainObject(b)) { | ||
| const out = { ...a }; | ||
| for (const key of Object.keys(b)) out[key] = key in a ? deepMergeTwo(a[key], b[key]) : b[key]; | ||
| return out; | ||
| } | ||
| return b; | ||
| } | ||
| /** | ||
| * Default option-set merge when a service declares no `mergeOptions`: | ||
| * deep-merge in declaration order — objects recurse, arrays union-dedupe, | ||
| * scalars take the later value. Covers the built-in services (`roots` / | ||
| * `langs` union, `themes` per-key last-wins) without a custom hook. | ||
| */ | ||
| function deepMergeOptionSets(sets) { | ||
| return sets.reduce((merged, set) => deepMergeTwo(merged, set)); | ||
| } | ||
| //#endregion | ||
| //#region src/node/host-services.ts | ||
| const debug = createDebug("devframe:services"); | ||
| function isServiceDefinition(input) { | ||
| return typeof input.setup === "function"; | ||
| } | ||
| function validateServiceInput(input) { | ||
| if (!input || typeof input.package !== "string" || input.package.length === 0) throw diagnostics$1.DF0070({ | ||
| package: String(input?.package ?? input), | ||
| reason: "the input has no `package` name" | ||
| }); | ||
| if (isServiceDefinition(input)) validateServiceDefinition(input); | ||
| } | ||
| function validateServiceDefinition(def) { | ||
| if (typeof def.version !== "string" || def.version.length === 0) throw diagnostics$1.DF0070({ | ||
| package: def.package, | ||
| reason: "the definition has no `version`" | ||
| }); | ||
| if (typeof def.scope !== "string" || def.scope.length === 0) throw diagnostics$1.DF0070({ | ||
| package: def.package, | ||
| reason: "the definition has no RPC `scope` namespace" | ||
| }); | ||
| } | ||
| /** | ||
| * Cross-plugin service registry (see `types/services.ts` for the contract). | ||
| * Values are held per context instance; `whenAvailable` subscriptions make | ||
| * the mechanism robust against setup ordering between provider and consumer. | ||
| * | ||
| * On top of the in-process `provide`/`get` tier, this host implements the | ||
| * **wire-service** lifecycle: `install()` queues definitions/descriptors, | ||
| * `ready()` fires the collect-then-setup barrier — importing descriptor | ||
| * packages, merging option sets per service, constructing each service once, | ||
| * providing its node API under the package name, and advertising it to | ||
| * clients through the `devframe:services` shared state. | ||
| */ | ||
| var DevframeServicesHostImpl = class { | ||
| context; | ||
| services = /* @__PURE__ */ new Map(); | ||
| listeners = /* @__PURE__ */ new Map(); | ||
| pending = /* @__PURE__ */ new Map(); | ||
| installed = /* @__PURE__ */ new Map(); | ||
| readyPromise; | ||
| constructor(context) { | ||
| this.context = context; | ||
| } | ||
| provide(id, service) { | ||
| const key = id; | ||
| if (this.services.has(key)) throw diagnostics$1.DF0037({ id: key }); | ||
| this.services.set(key, service); | ||
| for (const listener of this.listeners.get(key) ?? []) listener(service); | ||
| return () => { | ||
| if (this.services.get(key) === service) this.services.delete(key); | ||
| }; | ||
| } | ||
| get(id) { | ||
| return this.services.get(id); | ||
| } | ||
| has(id) { | ||
| return this.services.has(id); | ||
| } | ||
| whenAvailable(id, callback) { | ||
| const key = id; | ||
| if (this.services.has(key)) callback(this.services.get(key)); | ||
| let set = this.listeners.get(key); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| this.listeners.set(key, set); | ||
| } | ||
| const listener = callback; | ||
| set.add(listener); | ||
| return () => { | ||
| set.delete(listener); | ||
| }; | ||
| } | ||
| keys() { | ||
| return Array.from(this.services.keys()); | ||
| } | ||
| install(input, options) { | ||
| validateServiceInput(input); | ||
| const promise = new Promise((resolve, reject) => { | ||
| const entry = { | ||
| input, | ||
| resolveFrom: options?.resolveFrom, | ||
| resolve, | ||
| reject | ||
| }; | ||
| if (this.readyPromise) this.flushPackage(input.package, [entry]).catch(() => {}); | ||
| else { | ||
| let entries = this.pending.get(input.package); | ||
| if (!entries) { | ||
| entries = []; | ||
| this.pending.set(input.package, entries); | ||
| } | ||
| entries.push(entry); | ||
| } | ||
| }); | ||
| promise.catch(() => {}); | ||
| return promise; | ||
| } | ||
| ready() { | ||
| if (this.readyPromise) return this.readyPromise; | ||
| this.readyPromise = this.flushAll(); | ||
| return this.readyPromise; | ||
| } | ||
| async flushAll() { | ||
| if (this.context) await this.advertisementState(); | ||
| const groups = Array.from(this.pending.entries()); | ||
| this.pending.clear(); | ||
| for (const [pkg, entries] of groups) await this.flushPackage(pkg, entries); | ||
| } | ||
| async flushPackage(pkg, entries) { | ||
| try { | ||
| const api = await this.installPackage(pkg, entries); | ||
| for (const entry of entries) entry.resolve(api); | ||
| return api; | ||
| } catch (error) { | ||
| for (const entry of entries) entry.reject(error); | ||
| throw error; | ||
| } | ||
| } | ||
| async installPackage(pkg, entries) { | ||
| if (this.installed.has(pkg)) { | ||
| diagnostics$1.DF0066({ package: pkg }); | ||
| return this.installed.get(pkg); | ||
| } | ||
| let def = entries.filter((entry) => isServiceDefinition(entry.input))[0]?.input; | ||
| if (!def) { | ||
| const required = entries.map((entry) => entry.input).some((descriptor) => descriptor.required === true); | ||
| const cwd = this.context?.cwd ?? process.cwd(); | ||
| const resolveFroms = [ | ||
| ...entries.map((entry) => entry.resolveFrom && expandResolveFrom(entry.resolveFrom, cwd)), | ||
| this.context?.workspaceRoot, | ||
| cwd | ||
| ]; | ||
| let mod; | ||
| try { | ||
| mod = await importServicePackage(pkg, resolveFroms); | ||
| } catch (error) { | ||
| const reason = error instanceof Error ? error.message : String(error); | ||
| if (required) throw diagnostics$1.DF0067({ | ||
| package: pkg, | ||
| reason, | ||
| cause: error | ||
| }); | ||
| debug("optional service %s not importable, skipping: %s", pkg, reason); | ||
| return; | ||
| } | ||
| const factory = mod.default; | ||
| if (typeof factory !== "function") throw diagnostics$1.DF0070({ | ||
| package: pkg, | ||
| reason: "its default export is not a factory function" | ||
| }); | ||
| def = await factory(); | ||
| if (!def || typeof def.setup !== "function") throw diagnostics$1.DF0070({ | ||
| package: pkg, | ||
| reason: "its factory did not return a definition with a `setup` function" | ||
| }); | ||
| if (typeof def.package !== "string" || def.package.length === 0) def = { | ||
| ...def, | ||
| package: pkg | ||
| }; | ||
| validateServiceDefinition(def); | ||
| } | ||
| for (const entry of entries) { | ||
| const descriptor = entry.input; | ||
| if (isServiceDefinition(entry.input) || typeof descriptor.version !== "string") continue; | ||
| if (satisfiesVersionRange(def.version, descriptor.version)) continue; | ||
| if (descriptor.required === true) throw diagnostics$1.DF0068({ | ||
| package: pkg, | ||
| required: descriptor.version, | ||
| installed: def.version | ||
| }); | ||
| diagnostics$1.DF0069({ | ||
| package: pkg, | ||
| required: descriptor.version, | ||
| installed: def.version | ||
| }); | ||
| } | ||
| const sets = entries.map((entry) => entry.input.options).filter((options) => options !== void 0); | ||
| const options = def.mergeOptions ? def.mergeOptions(sets) : sets.length > 0 ? deepMergeOptionSets(sets) : void 0; | ||
| if (!this.context) throw diagnostics$1.DF0070({ | ||
| package: pkg, | ||
| reason: "this services host has no node context to install into" | ||
| }); | ||
| debug("installing service %s@%s (scope %s)", def.package, def.version, def.scope); | ||
| const scoped = this.context.scope(def.scope); | ||
| const api = await def.setup(scoped, options === void 0 ? {} : { options }); | ||
| this.installed.set(def.package, api); | ||
| this.provide(def.package, api); | ||
| await this.advertise(def); | ||
| return api; | ||
| } | ||
| advertisementState() { | ||
| return this.context.rpc.sharedState.get(DEVFRAME_SERVICES_STATE_KEY, { initialValue: {} }); | ||
| } | ||
| async advertise(def) { | ||
| const state = await this.advertisementState(); | ||
| const { package: pkg, version, scope, meta } = def; | ||
| state.mutate((value) => { | ||
| value[pkg] = { | ||
| package: pkg, | ||
| version, | ||
| scope, | ||
| ...meta ? { meta } : {} | ||
| }; | ||
| }); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/host-views.ts | ||
| var DevframeViewHost = class { | ||
| context; | ||
| importMetaUrl; | ||
| /** | ||
| * @internal | ||
| */ | ||
| buildStaticDirs = []; | ||
| constructor(context, importMetaUrl) { | ||
| this.context = context; | ||
| this.importMetaUrl = importMetaUrl; | ||
| } | ||
| hostStatic(baseUrl, source, defaultResolveFrom = this.importMetaUrl) { | ||
| const resolved = resolveStaticAssetsSource(source, this.context.host.getStorageDir("project"), defaultResolveFrom); | ||
| if (typeof resolved === "string" && !existsSync(resolved)) throw diagnostics$1.DF0008({ distDir: resolved }); | ||
| this.buildStaticDirs.push({ | ||
| baseUrl, | ||
| source | ||
| }); | ||
| this.context.host.mountStatic(baseUrl, resolved); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/rpc/agent-invoke-tool.ts | ||
| const agentInvokeTool = defineRpcFunction({ | ||
| name: "devframe:agent:invoke-tool", | ||
| type: "action", | ||
| setup: (ctx) => { | ||
| return { async handler(id, args) { | ||
| return await ctx.agent.invoke(id, args); | ||
| } }; | ||
| } | ||
| }); | ||
| //#endregion | ||
| //#region src/node/rpc/agent-list-resources.ts | ||
| const agentListResources = defineRpcFunction({ | ||
| name: "devframe:agent:list-resources", | ||
| type: "query", | ||
| jsonSerializable: true, | ||
| setup: (ctx) => { | ||
| return { async handler() { | ||
| return ctx.agent.list().resources; | ||
| } }; | ||
| } | ||
| }); | ||
| //#endregion | ||
| //#region src/node/rpc/index.ts | ||
| /** | ||
| * Built-in agent introspection RPC functions. Registered automatically | ||
| * by `createHostContext`. Not themselves agent-exposed (no `agent` | ||
| * field) — they power the MCP adapter and any future agent CLI. | ||
| */ | ||
| const BUILTIN_AGENT_RPC = [ | ||
| defineRpcFunction({ | ||
| name: "devframe:agent:list-tools", | ||
| type: "query", | ||
| jsonSerializable: true, | ||
| setup: (ctx) => { | ||
| return { async handler() { | ||
| return ctx.agent.list().tools; | ||
| } }; | ||
| } | ||
| }), | ||
| agentInvokeTool, | ||
| agentListResources, | ||
| defineRpcFunction({ | ||
| name: "devframe:agent:read-resource", | ||
| type: "query", | ||
| jsonSerializable: true, | ||
| setup: (ctx) => { | ||
| return { async handler(id) { | ||
| return await ctx.agent.read(id); | ||
| } }; | ||
| } | ||
| }) | ||
| ]; | ||
| //#endregion | ||
| //#region src/utils/scope.ts | ||
| /** Whether a name is already namespaced (contains a `:` separator). */ | ||
| function isQualifiedName(name) { | ||
| return name.includes(":"); | ||
| } | ||
| /** | ||
| * Prefix a bare name with `<namespace>:`. Names that already contain a | ||
| * `:` are returned unchanged, so callers can reference another scope's | ||
| * ids explicitly (e.g. `ctx.rpc.call('other-plugin:fn')`). | ||
| */ | ||
| function qualifyName(namespace, name) { | ||
| return isQualifiedName(name) ? name : `${namespace}:${name}`; | ||
| } | ||
| //#endregion | ||
| //#region src/node/settings.ts | ||
| const STORAGE_SCOPE = { | ||
| global: "global", | ||
| project: "project" | ||
| }; | ||
| function createNodeSettingsStore(context, namespace, scope) { | ||
| const stateKey = `devframe:settings:${scope}:${namespace}`; | ||
| let statePromise; | ||
| function store() { | ||
| if (!statePromise) { | ||
| const dir = context.host.getStorageDir(STORAGE_SCOPE[scope]); | ||
| const filepath = join(dir, "settings", `${namespace}.json`); | ||
| statePromise = context.rpc.sharedState.get(stateKey, { sharedState: createStorage({ | ||
| filepath, | ||
| initialValue: {} | ||
| }) }); | ||
| } | ||
| return statePromise; | ||
| } | ||
| return { | ||
| async get(key) { | ||
| return (await store()).value()[key]; | ||
| }, | ||
| async set(key, value) { | ||
| (await store()).mutate((draft) => { | ||
| draft[key] = value; | ||
| }); | ||
| }, | ||
| async delete(key) { | ||
| (await store()).mutate((draft) => { | ||
| delete draft[key]; | ||
| }); | ||
| }, | ||
| async all() { | ||
| return (await store()).value(); | ||
| }, | ||
| async onChange(fn) { | ||
| return (await store()).on("updated", (full) => fn(full)); | ||
| } | ||
| }; | ||
| } | ||
| /** | ||
| * Build the node-side `settings` surface for a scope namespace. `project` | ||
| * persists under the host's `workspace` storage dir, `global` under its | ||
| * `global` dir. Each is a file-backed, client-synced key-value store. | ||
| */ | ||
| function createNodeSettings(context, namespace) { | ||
| return { | ||
| global: createNodeSettingsStore(context, namespace, "global"), | ||
| project: createNodeSettingsStore(context, namespace, "project") | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/node/scope.ts | ||
| function prefixDefinition(namespace, fn) { | ||
| if (isQualifiedName(fn.name)) throw diagnostics$1.DF0034({ | ||
| namespace, | ||
| name: fn.name | ||
| }); | ||
| return { | ||
| ...fn, | ||
| name: `${namespace}:${fn.name}` | ||
| }; | ||
| } | ||
| /** | ||
| * Build a namespace-scoped view of a {@link DevframeNodeContext}. Every | ||
| * RPC id, shared-state key, and streaming channel passed through the | ||
| * returned `rpc` surface is auto-namespaced with `<namespace>:`. | ||
| */ | ||
| function createScopedNodeContext(context, namespace) { | ||
| const base = context.rpc; | ||
| const rpc = { | ||
| namespace, | ||
| register(fn, force) { | ||
| base.register(prefixDefinition(namespace, fn), force); | ||
| }, | ||
| update(fn, force) { | ||
| base.update(prefixDefinition(namespace, fn), force); | ||
| }, | ||
| call: ((method, ...args) => base.invokeLocal(qualifyName(namespace, method), ...args)), | ||
| broadcast: ((options) => base.broadcast({ | ||
| ...options, | ||
| method: qualifyName(namespace, options.method) | ||
| })), | ||
| sharedState: ((key, options) => base.sharedState.get(qualifyName(namespace, key), options)), | ||
| streaming: { create: (name, opts) => base.streaming.create(qualifyName(namespace, name), opts) }, | ||
| getCurrentRpcSession: () => base.getCurrentRpcSession() | ||
| }; | ||
| return { | ||
| namespace, | ||
| base: context, | ||
| cwd: context.cwd, | ||
| workspaceRoot: context.workspaceRoot, | ||
| mode: context.mode, | ||
| host: context.host, | ||
| rpc, | ||
| settings: createNodeSettings(context, namespace), | ||
| views: context.views, | ||
| diagnostics: context.diagnostics, | ||
| agent: context.agent, | ||
| scope: context.scope | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/node/context.ts | ||
| /** | ||
| * Framework- and build-tool-agnostic core of the Devframe node context. | ||
| * Wires the RPC host, view (HTTP file-serving) host, diagnostics, and | ||
| * agent subsystems. Host adapters can wrap this to augment `ctx` with | ||
| * extra surfaces — for example, `@vitejs/devtools-kit`'s | ||
| * `createKitContext` attaches `docks`, `terminals`, `messages`, and | ||
| * `commands` when mounted into Vite DevTools. | ||
| */ | ||
| async function createHostContext(options) { | ||
| const { cwd, workspaceRoot = cwd, mode, host, importMetaUrl, builtinRpcDeclarations = [] } = options; | ||
| const context = { | ||
| cwd, | ||
| workspaceRoot, | ||
| mode, | ||
| host, | ||
| rpc: void 0, | ||
| views: void 0, | ||
| diagnostics: void 0, | ||
| agent: void 0, | ||
| services: void 0, | ||
| staticConfig: {}, | ||
| scope: void 0 | ||
| }; | ||
| const rpcHost = new RpcFunctionsHostImpl(context); | ||
| const viewsHost = new DevframeViewHost(context, importMetaUrl); | ||
| const diagnosticsHost = new DevframeDiagnosticsHost(context, [diagnostics$1, diagnostics]); | ||
| context.rpc = rpcHost; | ||
| context.views = viewsHost; | ||
| context.diagnostics = diagnosticsHost; | ||
| context.services = new DevframeServicesHostImpl(context); | ||
| context.agent = new DevframeAgentHost(context); | ||
| const scopedCache = /* @__PURE__ */ new Map(); | ||
| context.scope = ((namespace) => { | ||
| if (!namespace) return context; | ||
| let scoped = scopedCache.get(namespace); | ||
| if (!scoped) { | ||
| scoped = createScopedNodeContext(context, namespace); | ||
| scopedCache.set(namespace, scoped); | ||
| } | ||
| return scoped; | ||
| }); | ||
| for (const fn of BUILTIN_AGENT_RPC) rpcHost.register(fn); | ||
| for (const fn of builtinRpcDeclarations) rpcHost.register(fn); | ||
| return context; | ||
| } | ||
| //#endregion | ||
| export { createHostContext as t }; |
Sorry, the diff of this file is too big to display
| import { n as randomToken } from "./crypto-token-XCqTSMg9.mjs"; | ||
| import { t as createStorage } from "./storage-BXNVhKyR.mjs"; | ||
| import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-PihtvVil.mjs"; | ||
| import { join } from "pathe"; | ||
| //#region src/node/hub-internals/context.ts | ||
| const internalContextMap = /* @__PURE__ */ new WeakMap(); | ||
| function getInternalContext(context) { | ||
| if (!internalContextMap.has(context)) { | ||
| const storage = createStorage({ | ||
| filepath: join(context.host.getStorageDir("global"), "auth.json"), | ||
| initialValue: { trusted: {} } | ||
| }); | ||
| const remoteTokens = /* @__PURE__ */ new Map(); | ||
| const wsEndpointListeners = /* @__PURE__ */ new Set(); | ||
| function revokeRemoteToken(token) { | ||
| if (!remoteTokens.delete(token)) return; | ||
| revokeActiveConnectionsForToken(context, token); | ||
| } | ||
| const internalContext = { | ||
| storage: { auth: storage }, | ||
| revokeAuthToken: (token) => revokeAuthToken(context, storage, token), | ||
| setWsEndpoint(endpoint) { | ||
| internalContext.wsEndpoint = endpoint; | ||
| for (const listener of wsEndpointListeners) listener(); | ||
| }, | ||
| onWsEndpointChange(cb) { | ||
| wsEndpointListeners.add(cb); | ||
| return () => wsEndpointListeners.delete(cb); | ||
| }, | ||
| remoteTokens, | ||
| allocateRemoteToken(dockId, origin, originLock) { | ||
| const token = randomToken(); | ||
| remoteTokens.set(token, { | ||
| dockId, | ||
| origin, | ||
| originLock | ||
| }); | ||
| return token; | ||
| }, | ||
| revokeRemoteToken, | ||
| revokeRemoteTokensForDock(dockId) { | ||
| const tokensToRevoke = []; | ||
| for (const [token, record] of remoteTokens) if (record.dockId === dockId) tokensToRevoke.push(token); | ||
| for (const token of tokensToRevoke) revokeRemoteToken(token); | ||
| }, | ||
| isRemoteTokenTrusted(token, requestOrigin) { | ||
| const record = remoteTokens.get(token); | ||
| if (!record) return false; | ||
| if (!record.originLock) return true; | ||
| return !!requestOrigin && record.origin === requestOrigin; | ||
| } | ||
| }; | ||
| internalContextMap.set(context, internalContext); | ||
| } | ||
| return internalContextMap.get(context); | ||
| } | ||
| //#endregion | ||
| export { internalContextMap as n, getInternalContext as t }; |
| import { k as SharedState, o as DevframeNodeContext } from "./context-c0OlSc12.mjs"; | ||
| //#region src/node/hub-internals/context.d.ts | ||
| interface InternalAnonymousAuthStorage { | ||
| trusted: Record<string, { | ||
| authToken: string; | ||
| ua: string; | ||
| origin: string; | ||
| timestamp: number; | ||
| } | undefined>; | ||
| } | ||
| interface RemoteTokenRecord { | ||
| dockId: string; | ||
| /** Dock URL origin — matched against WS handshake `Origin` header when `originLock` is on. */ | ||
| origin: string; | ||
| originLock: boolean; | ||
| } | ||
| interface DevframeInternalContext { | ||
| storage: { | ||
| auth: SharedState<InternalAnonymousAuthStorage>; | ||
| }; | ||
| /** | ||
| * Revoke an auth token: remove from storage and notify all connected clients | ||
| * using this token that they are no longer trusted. | ||
| */ | ||
| revokeAuthToken: (token: string) => Promise<void>; | ||
| /** | ||
| * Session-only tokens issued to remote-UI iframe docks. Not persisted — | ||
| * regenerated on every dev-server restart. | ||
| */ | ||
| remoteTokens: Map<string, RemoteTokenRecord>; | ||
| allocateRemoteToken: (dockId: string, origin: string, originLock: boolean) => string; | ||
| revokeRemoteToken: (token: string) => void; | ||
| revokeRemoteTokensForDock: (dockId: string) => void; | ||
| /** | ||
| * Returns true if `token` is a valid remote token and, when `originLock` is | ||
| * on, `requestOrigin` matches the recorded dock origin. | ||
| */ | ||
| isRemoteTokenTrusted: (token: string, requestOrigin?: string) => boolean; | ||
| /** | ||
| * Populated by `createWsServer` once the WS port is bound. Consumed by the | ||
| * docks host when enriching remote iframe URLs with a connection descriptor. | ||
| */ | ||
| wsEndpoint?: { | ||
| /** Full `ws://` or `wss://` URL with host and port. */ | ||
| url: string; | ||
| }; | ||
| /** | ||
| * Set {@link DevframeInternalContext.wsEndpoint} and notify subscribers — | ||
| * the WS-binding tiers (side-car, shared-server, and the `unbound` tier's | ||
| * `attach()`) call this once the socket is bound (or `undefined` once torn | ||
| * down) instead of assigning the field directly, so anything that already | ||
| * projected the endpoint (a hub's remote-dock URLs, registered before an | ||
| * async bind resolves) gets a chance to re-project it. | ||
| */ | ||
| setWsEndpoint: (endpoint: { | ||
| url: string; | ||
| } | undefined) => void; | ||
| /** | ||
| * Subscribe to every {@link DevframeInternalContext.setWsEndpoint} call. | ||
| * Returns an unsubscribe function. The hub context uses this to refresh | ||
| * the `devframe:docks` shared state so a remote dock registered before the | ||
| * WS port resolves still ends up with a live connection URL. | ||
| */ | ||
| onWsEndpointChange: (cb: () => void) => () => void; | ||
| } | ||
| declare const internalContextMap: WeakMap<DevframeNodeContext, DevframeInternalContext>; | ||
| declare function getInternalContext(context: DevframeNodeContext): DevframeInternalContext; | ||
| //#endregion | ||
| export { internalContextMap as a, getInternalContext as i, InternalAnonymousAuthStorage as n, RemoteTokenRecord as r, DevframeInternalContext as t }; |
| import { resolveClientAssets } from "./index.mjs"; | ||
| import { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs"; | ||
| import { t as createHostContext } from "./context-Bc5S40Qt.mjs"; | ||
| import { t as resolveStaticAssetsSource } from "./remote-assets-DezQmPQU.mjs"; | ||
| import { i as resolveMcpConnectionMeta, n as resolveBasePath, r as resolveDevServerPort, t as normalizeBasePath } from "./_shared-BM3PdYli.mjs"; | ||
| import { t as createH3DevframeHost } from "./host-h3-fRbF9yor.mjs"; | ||
| import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, t as createInstanceShell } from "./instance-shell-fB5PyVtR.mjs"; | ||
| import { open } from "./utils/open.mjs"; | ||
| import { mountStaticHandler } from "./utils/serve-static.mjs"; | ||
| import { createServer } from "node:http"; | ||
| import process from "node:process"; | ||
| import { resolve } from "pathe"; | ||
| 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 ?? resolveClientAssets(def); | ||
| const app = options.app ?? new H3(); | ||
| const host = options.host ?? def.cli?.host ?? "localhost"; | ||
| const shell = createInstanceShell({ | ||
| base, | ||
| app, | ||
| host, | ||
| 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, | ||
| importMetaUrl: def.importMetaUrl | ||
| }); | ||
| const setupInfo = { flags: options.flags ?? {} }; | ||
| for (const input of def.services ?? []) context.services.install(input, { resolveFrom: def.importMetaUrl }); | ||
| await context.services.ready(); | ||
| 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-D7wWuy7-.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"), def.importMetaUrl); | ||
| 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 the definition's client assets are 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 }; |
| //#region src/events.ts | ||
| /** | ||
| * Centralized registry of the core devframe event names — the node-side host | ||
| * bus events, the client RPC connection events, and the server→client | ||
| * broadcast notifications — so these names live in one place instead of | ||
| * scattered string literals. | ||
| * | ||
| * **Keep this in sync with [`docs/content/1.guide/20.events.md`](../../../docs/content/1.guide/20.events.md)** | ||
| * (the "Core devframe events" section): every name here appears in that page's | ||
| * tables, and every name there resolves to an entry here. Add, rename, or | ||
| * remove a name in both places in the same change, and reference | ||
| * `DEVFRAME_EVENTS.*` from call sites instead of re-typing a literal. | ||
| * | ||
| * This map covers **notifications** (events, broadcasts). The request/response | ||
| * RPC endpoints of the shared-state, streaming, and auth-handshake protocols | ||
| * (`devframe:rpc:server-state:*`, `devframe:streaming:subscribe`, | ||
| * `anonymous:devframe:auth`, …) are defined at their handlers and typed in | ||
| * `types/rpc-augments.ts`; they aren't events and stay out of this map. | ||
| * | ||
| * The `EventEmitter` maps (`RpcClientEvents`, `DevframeAgentHostEvents`) and the | ||
| * `DevframeRpcClientFunctions` augmentation declare these names as type-level | ||
| * keys (a literal is unavoidable in a type position); those declarations mirror | ||
| * this map and move with it. | ||
| */ | ||
| const DEVFRAME_EVENTS = { | ||
| /** | ||
| * Node-side host `EventEmitter` events. The agent host (`ctx.agent.events`) | ||
| * emits these as its tool/resource surface changes; protocol adapters (e.g. | ||
| * MCP) subscribe to re-publish their manifest. | ||
| */ | ||
| bus: { | ||
| agentManifestChanged: "agent:manifest:changed", | ||
| agentToolRegistered: "agent:tool:registered", | ||
| agentToolUnregistered: "agent:tool:unregistered", | ||
| agentResourceRegistered: "agent:resource:registered", | ||
| agentResourceUnregistered: "agent:resource:unregistered" | ||
| }, | ||
| /** | ||
| * Client-side RPC connection `EventEmitter` events (`rpc.events`) a UI | ||
| * subscribes to for connection lifecycle and error surfacing. | ||
| */ | ||
| client: { | ||
| isTrustedUpdated: "rpc:is-trusted:updated", | ||
| error: "rpc:error", | ||
| connectionStatus: "connection:status", | ||
| connectionError: "connection:error" | ||
| }, | ||
| /** | ||
| * Broadcast notifications the server pushes to clients (server → client), | ||
| * `devframe:` prefix. The paired request methods (subscribe/get/set/…) are | ||
| * RPC endpoints, not events, and are omitted deliberately. | ||
| */ | ||
| broadcast: { | ||
| authRevoked: "devframe:auth:revoked", | ||
| clientStateUpdated: "devframe:rpc:client-state:updated", | ||
| clientStatePatch: "devframe:rpc:client-state:patch", | ||
| streamingChunk: "devframe:streaming:chunk", | ||
| streamingEnd: "devframe:streaming:end", | ||
| streamingUploadCancel: "devframe:streaming:upload-cancel" | ||
| }, | ||
| /** `postMessage` channels the runtime posts across window boundaries. */ | ||
| postMessage: { remoteAssetsError: "devframe:remote-assets-error" } | ||
| }; | ||
| //#endregion | ||
| export { DEVFRAME_EVENTS as t }; |
| import { t as DEVFRAME_EVENTS } from "./events-DzolaBEs.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs"; | ||
| //#region src/utils/events.ts | ||
| /** | ||
| * Create event emitter. | ||
| */ | ||
| function createEventEmitter() { | ||
| const _listeners = {}; | ||
| function emit(event, ...args) { | ||
| const callbacks = _listeners[event] || []; | ||
| for (let i = 0, length = callbacks.length; i < length; i++) { | ||
| const callback = callbacks[i]; | ||
| if (callback) callback(...args); | ||
| } | ||
| } | ||
| function emitOnce(event, ...args) { | ||
| emit(event, ...args); | ||
| delete _listeners[event]; | ||
| } | ||
| function on(event, cb) { | ||
| (_listeners[event] ||= []).push(cb); | ||
| return () => { | ||
| _listeners[event] = _listeners[event]?.filter((i) => cb !== i); | ||
| }; | ||
| } | ||
| function once(event, cb) { | ||
| const unsubscribe = on(event, ((...args) => { | ||
| unsubscribe(); | ||
| return cb(...args); | ||
| })); | ||
| return unsubscribe; | ||
| } | ||
| return { | ||
| _listeners, | ||
| emit, | ||
| emitOnce, | ||
| on, | ||
| once | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/node/agent-args.ts | ||
| /** | ||
| * Map the args payload an agent surface receives (MCP sends an object | ||
| * keyed `arg0`/`arg1`/…, matching the schema the adapter advertises) onto | ||
| * a handler's positional parameters. Shared by the agent host's RPC | ||
| * bridge and the hub's command-derived tools so the coercion cannot | ||
| * drift between them. | ||
| * | ||
| * - an array passes through as-is | ||
| * - `null`/`undefined` become a zero-argument call | ||
| * - with declared schemas, each schema reads its own `argN` key, in order | ||
| * - without schemas, `arg0`/`arg1`/… keys are collected when present | ||
| * - an empty object becomes a zero-argument call | ||
| * - anything else follows the {@link AgentArgsFallback} | ||
| */ | ||
| function coerceAgentPositionalArgs(args, schemas, fallback = "wrap") { | ||
| if (Array.isArray(args)) return args; | ||
| if (args === void 0 || args === null) return []; | ||
| if (typeof args === "object") { | ||
| const obj = args; | ||
| if (schemas && schemas.length) return schemas.map((_, i) => obj[`arg${i}`]); | ||
| if ("arg0" in obj) { | ||
| const out = []; | ||
| let i = 0; | ||
| while (`arg${i}` in obj) { | ||
| out.push(obj[`arg${i}`]); | ||
| i++; | ||
| } | ||
| return out; | ||
| } | ||
| if (Object.keys(obj).length === 0) return []; | ||
| } | ||
| return fallback === "drop" ? [] : [args]; | ||
| } | ||
| //#endregion | ||
| //#region src/node/host-agent.ts | ||
| /** | ||
| * Framework-neutral host aggregating the agent-exposed surface of a | ||
| * devframe. Auto-discovers RPC functions with an `agent` field from | ||
| * `ctx.rpc.definitions`, and accepts plugin-registered tools / | ||
| * resources via `registerTool` / `registerResource`. | ||
| */ | ||
| var DevframeAgentHost = class { | ||
| context; | ||
| events = createEventEmitter(); | ||
| tools = /* @__PURE__ */ new Map(); | ||
| resources = /* @__PURE__ */ new Map(); | ||
| providers = /* @__PURE__ */ new Set(); | ||
| _rpcUnsubscribe; | ||
| constructor(context) { | ||
| this.context = context; | ||
| this._rpcUnsubscribe = context.rpc.onChanged(() => { | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| }); | ||
| } | ||
| registerTool(input) { | ||
| this._validateToolId(input.id); | ||
| const tool = this._projectTool(input); | ||
| this.tools.set(tool.id, { | ||
| tool, | ||
| handler: input.handler | ||
| }); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentToolRegistered, tool); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| return { unregister: () => this.unregisterTool(tool.id) }; | ||
| } | ||
| unregisterTool(id) { | ||
| const existed = this.tools.delete(id); | ||
| if (existed) { | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentToolUnregistered, id); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| } | ||
| return existed; | ||
| } | ||
| registerToolProvider(provider) { | ||
| this.providers.add(provider); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| const notifyChanged = () => { | ||
| if (this.providers.has(provider)) this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| }; | ||
| return { | ||
| notifyChanged, | ||
| unregister: () => { | ||
| if (this.providers.delete(provider)) this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| } | ||
| }; | ||
| } | ||
| registerResource(input) { | ||
| if (this.resources.has(input.id)) throw diagnostics.DF0016({ id: input.id }); | ||
| const resource = { | ||
| id: input.id, | ||
| name: input.name, | ||
| description: input.description, | ||
| mimeType: input.mimeType ?? "application/json", | ||
| uri: input.uri ?? `devframe://resource/${encodeURIComponent(input.id)}` | ||
| }; | ||
| this.resources.set(resource.id, { | ||
| resource, | ||
| read: input.read | ||
| }); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentResourceRegistered, resource); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| return { unregister: () => this.unregisterResource(resource.id) }; | ||
| } | ||
| unregisterResource(id) { | ||
| const existed = this.resources.delete(id); | ||
| if (existed) { | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentResourceUnregistered, id); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| } | ||
| return existed; | ||
| } | ||
| list() { | ||
| const rpcTools = this._collectRpcTools(); | ||
| const plainTools = Array.from(this.tools.values()).map((t) => t.tool); | ||
| const resources = Array.from(this.resources.values()).map((r) => r.resource); | ||
| const seen = new Set([...rpcTools, ...plainTools].map((t) => t.id)); | ||
| const providerTools = []; | ||
| for (const { tool } of this._collectProviderTools()) { | ||
| if (seen.has(tool.id)) continue; | ||
| seen.add(tool.id); | ||
| providerTools.push(tool); | ||
| } | ||
| return { | ||
| tools: [ | ||
| ...rpcTools, | ||
| ...plainTools, | ||
| ...providerTools | ||
| ], | ||
| resources | ||
| }; | ||
| } | ||
| getTool(id) { | ||
| const plain = this.tools.get(id); | ||
| if (plain) return plain.tool; | ||
| const rpc = this._collectRpcTools().find((t) => t.id === id); | ||
| if (rpc) return rpc; | ||
| return this._collectProviderTools().find((t) => t.tool.id === id)?.tool; | ||
| } | ||
| getResource(id) { | ||
| return this.resources.get(id)?.resource; | ||
| } | ||
| async invoke(id, args) { | ||
| const plain = this.tools.get(id); | ||
| if (plain?.handler) return await plain.handler(args); | ||
| const rpcDef = this._findRpcDefinition(id); | ||
| if (rpcDef) { | ||
| const positional = coerceAgentPositionalArgs(args, rpcDef.args, "wrap"); | ||
| return await this.context.rpc.invokeLocal(id, ...positional); | ||
| } | ||
| const provided = this._collectProviderTools().find((t) => t.tool.id === id); | ||
| if (provided) return await provided.input.handler(args); | ||
| throw new Error(`[devframe/agent] tool "${id}" not found`); | ||
| } | ||
| async read(id) { | ||
| const entry = this.resources.get(id); | ||
| if (!entry) throw new Error(`[devframe/agent] resource "${id}" not found`); | ||
| return await entry.read(); | ||
| } | ||
| /** @internal */ | ||
| _dispose() { | ||
| this._rpcUnsubscribe?.(); | ||
| this._rpcUnsubscribe = void 0; | ||
| } | ||
| _validateToolId(id) { | ||
| if (this.tools.has(id)) throw diagnostics.DF0015({ id }); | ||
| if (this.context.rpc.definitions.get(id)?.agent) throw diagnostics.DF0015({ id }); | ||
| } | ||
| _projectTool(input) { | ||
| if (!input.description || typeof input.description !== "string") throw diagnostics.DF0014({ name: input.id }); | ||
| return { | ||
| id: input.id, | ||
| kind: "tool", | ||
| title: input.title ?? input.id, | ||
| description: input.description, | ||
| safety: input.safety ?? "action", | ||
| tags: input.tags, | ||
| args: input.args, | ||
| inputSchema: input.inputSchema, | ||
| outputSchema: input.outputSchema, | ||
| examples: input.examples | ||
| }; | ||
| } | ||
| /** Query every registered provider, projecting inputs to serializable tools. */ | ||
| _collectProviderTools() { | ||
| const out = []; | ||
| for (const provider of this.providers) for (const input of provider()) out.push({ | ||
| input, | ||
| tool: this._projectTool(input) | ||
| }); | ||
| return out; | ||
| } | ||
| _collectRpcTools() { | ||
| const out = []; | ||
| for (const [name, def] of this.context.rpc.definitions) { | ||
| const agent = def.agent; | ||
| if (!agent) continue; | ||
| if (!agent.description || typeof agent.description !== "string") throw diagnostics.DF0014({ name }); | ||
| const type = def.type ?? "query"; | ||
| const safety = agent.safety ?? inferSafety(type); | ||
| out.push({ | ||
| id: name, | ||
| kind: "rpc", | ||
| title: agent.title ?? name, | ||
| description: agent.description, | ||
| safety, | ||
| tags: agent.tags, | ||
| rpcName: name, | ||
| examples: agent.examples | ||
| }); | ||
| } | ||
| return out; | ||
| } | ||
| _findRpcDefinition(id) { | ||
| const def = this.context.rpc.definitions.get(id); | ||
| if (def?.agent) return def; | ||
| } | ||
| }; | ||
| function inferSafety(type) { | ||
| if (type === "static" || type === "query") return "read"; | ||
| return "action"; | ||
| } | ||
| //#endregion | ||
| export { coerceAgentPositionalArgs as n, createEventEmitter as r, DevframeAgentHost as t }; |
| import { n as __exportAll } from "./rolldown-runtime-B4iAMlE-.mjs"; | ||
| import { t as Diagnostic } from "./nostics-CzECRXpE.mjs"; | ||
| import { t as DEVFRAME_EVENTS } from "./events-DzolaBEs.mjs"; | ||
| import { i as isAllowedOrigin } from "./ws-server-BdSLrhxE.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs"; | ||
| import { t as createHostContext } from "./context-Bc5S40Qt.mjs"; | ||
| import { t as toAgentToolName } from "./agent-tool-name-EgfoFO8C.mjs"; | ||
| import { randomUUID } from "node:crypto"; | ||
| import process from "node:process"; | ||
| import { join } from "pathe"; | ||
| 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(DEVFRAME_EVENTS.bus.agentManifestChanged, () => { | ||
| 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`); | ||
| } | ||
| }, | ||
| importMetaUrl: definition.importMetaUrl | ||
| }); | ||
| for (const input of definition.services ?? []) ctx.services.install(input, { resolveFrom: definition.importMetaUrl }); | ||
| await ctx.services.ready(); | ||
| 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 { k as SharedState, o as DevframeNodeContext, s as DevframeNodeRpcSession } from "./context-c0OlSc12.mjs"; | ||
| import { n as InternalAnonymousAuthStorage } from "./context-DO0Qu1O8.mjs"; | ||
| //#region src/node/auth/revoke.d.ts | ||
| /** | ||
| * Flip `isTrusted` to false on any live WS clients connected with `token` | ||
| * and broadcast the `auth:revoked` event so they can react. | ||
| * | ||
| * Shared between persisted-auth revocation and remote-dock token revocation. | ||
| */ | ||
| declare function revokeActiveConnectionsForToken(context: DevframeNodeContext, token: string): Promise<void>; | ||
| /** | ||
| * Revoke an auth token: remove from storage and notify all connected clients | ||
| * using this token that they are no longer trusted. | ||
| */ | ||
| declare function revokeAuthToken(context: DevframeNodeContext, storage: SharedState<InternalAnonymousAuthStorage>, token: string): Promise<void>; | ||
| //#endregion | ||
| //#region src/node/auth/state.d.ts | ||
| /** | ||
| * The current one-time authentication code. Display this to the user (e.g. in | ||
| * the dev-server terminal) so they can type it into the browser to authenticate. | ||
| */ | ||
| declare function getTempAuthCode(): string; | ||
| /** | ||
| * Rotate the authentication code, resetting its expiry window and failed-attempt | ||
| * counter. Call this when a new authentication flow begins (e.g. when an | ||
| * untrusted client starts authenticating) so the displayed code is freshly | ||
| * valid for its full TTL. | ||
| */ | ||
| declare function refreshTempAuthCode(): string; | ||
| /** | ||
| * Build a "magic link" authentication URL that embeds a one-time code (OTP) in | ||
| * the URL **fragment**. Opening it authenticates the client without typing — | ||
| * print it on startup (devframe stays headless, so the host prints its own | ||
| * banner). Defaults to the current code; the link is subject to the same TTL. | ||
| * | ||
| * The code rides the fragment (`#devframe_otp=…`), not the query string, so it | ||
| * is never sent to the server, written to an access log, or leaked in a | ||
| * `Referer` header — the browser client reads it locally (see | ||
| * `consumeOtpFromUrl`). Any existing fragment parameters are preserved. | ||
| */ | ||
| declare function buildOtpAuthUrl(baseUrl: string, code?: string): string; | ||
| /** | ||
| * Re-authenticate a connection that presents a previously-issued bearer token. | ||
| * Returns `true` and marks the session trusted when the token is known. | ||
| * | ||
| * Used by the `anonymous:devframe:auth` handler so a client that already | ||
| * authenticated (token persisted in the browser) is trusted on reconnect | ||
| * without entering the code again. | ||
| */ | ||
| declare function verifyAuthToken(token: string, session: DevframeNodeRpcSession, storage: SharedState<InternalAnonymousAuthStorage>): boolean; | ||
| /** | ||
| * Exchange a one-time authentication code for a fresh, node-issued bearer token. | ||
| * | ||
| * On success this mints a high-entropy token, records it in the trusted store, | ||
| * marks the calling session trusted, rotates the code, and returns the token | ||
| * for the client to persist. Returns `null` on any failure. | ||
| * | ||
| * Because the code is short and human-typed, verification is hardened against | ||
| * brute force: it enforces a time-to-live, compares in constant time, and | ||
| * rotates the code after {@link TEMP_AUTH_MAX_ATTEMPTS} failed attempts so an | ||
| * attacker cannot keep guessing against the same code. | ||
| */ | ||
| declare function exchangeTempAuthCode(code: string, session: DevframeNodeRpcSession, info: { | ||
| ua: string; | ||
| origin: string; | ||
| }, storage: SharedState<InternalAnonymousAuthStorage>): string | null; | ||
| //#endregion | ||
| export { verifyAuthToken as a, refreshTempAuthCode as i, exchangeTempAuthCode as n, revokeActiveConnectionsForToken as o, getTempAuthCode as r, revokeAuthToken as s, buildOtpAuthUrl as t }; |
| import { d as DevframeRpcConnection, n as WsOriginRegistry, u as DevframeNodeRpcSessionMeta } from "./ws-server-DLtEoLdR.mjs"; | ||
| import { Ct as DevframeAuthHandler, U as DevframeSseOptions, W as DevframeWsOptions, _t as DevframeRpcServerFunctions, gt as DevframeRpcClientFunctions, o as DevframeNodeContext, s as DevframeNodeRpcSession, t as ConnectionMeta } from "./context-c0OlSc12.mjs"; | ||
| import "./index-B_DPNIT4.mjs"; | ||
| import { BirpcGroup } from "birpc"; | ||
| import { NodeAdapter } from "crossws/adapters/node"; | ||
| import { Buffer } from "node:buffer"; | ||
| import { IncomingMessage, Server, ServerResponse } from "node:http"; | ||
| import { Duplex } from "node:stream"; | ||
| import { H3 } from "h3"; | ||
| //#region src/node/instance-registry.d.ts | ||
| /** | ||
| * One running devframe instance, as recorded in the instance registry. | ||
| * Records are self-describing JSON — additive fields are safe. | ||
| */ | ||
| interface DevframeInstanceRecord { | ||
| /** Process id of the dev server. */ | ||
| pid: number; | ||
| /** Listening port. */ | ||
| port: number; | ||
| /** Dialable HTTP origin, e.g. `http://127.0.0.1:9876`. */ | ||
| origin: string; | ||
| /** Base path the devframe is mounted at (trailing slash). */ | ||
| basePath: string; | ||
| /** Definition id. */ | ||
| id: string; | ||
| /** Definition display name. */ | ||
| name?: string; | ||
| /** Working directory the instance was started from. */ | ||
| rootDir: string; | ||
| /** | ||
| * Absolute URL path of the MCP Streamable-HTTP endpoint on `origin`, or | ||
| * `null` when the instance runs without an MCP route. | ||
| */ | ||
| mcp: { | ||
| path: string; | ||
| } | null; | ||
| /** Epoch-ms timestamp of registration. */ | ||
| startedAt: number; | ||
| } | ||
| /** | ||
| * Handle returned by {@link registerDevframeInstance}. | ||
| */ | ||
| interface DevframeInstanceRegistration { | ||
| /** The registry file backing this registration. */ | ||
| readonly file: string; | ||
| /** Remove the record (idempotent). Call on server close. */ | ||
| unregister: () => void; | ||
| } | ||
| /** | ||
| * Record a running devframe instance in the global instance registry so | ||
| * discovery tooling (`devframe connect`, editor integrations) can find it | ||
| * without port guessing. | ||
| * | ||
| * `createDevServer` registers automatically; custom hosts that serve a | ||
| * devframe in-process (e.g. `@devframes/next`'s host inside a Next dev | ||
| * server) call this explicitly with the origin they are reachable at. | ||
| * | ||
| * The record is written atomically to `<dir>/<pid>-<port>.json` and removed | ||
| * by {@link DevframeInstanceRegistration.unregister}. Records surviving a | ||
| * crash are pruned by readers whose liveness probe fails. Registration never | ||
| * throws — a write failure degrades to a coded warning (`DF0045`), since a | ||
| * dev server must not die over discovery metadata. | ||
| */ | ||
| declare function registerDevframeInstance(record: DevframeInstanceRecord, options?: { | ||
| instancesDir?: string; | ||
| }): DevframeInstanceRegistration; | ||
| /** | ||
| * Read the registry and split records into live and dead by probing each | ||
| * one's `__connection.json`, deleting dead records (prune-on-read). Live | ||
| * records carry the dialable origin the probe confirmed (a `localhost` | ||
| * record may come back as `127.0.0.1` / `[::1]`). | ||
| * | ||
| * A liveness probe only proves *something* answers on the record's port, so | ||
| * records left behind by killed processes shadow the server currently bound | ||
| * there: per `(port, basePath)` only the newest record survives, older | ||
| * ghosts are pruned with the dead. | ||
| */ | ||
| declare function listLiveDevframeInstances(options?: { | ||
| instancesDir?: string; | ||
| timeoutMs?: number; | ||
| }): Promise<{ | ||
| live: DevframeInstanceRecord[]; | ||
| pruned: DevframeInstanceRecord[]; | ||
| }>; | ||
| //#endregion | ||
| //#region src/node/instance-shell.d.ts | ||
| /** | ||
| * The live handle for a bound HTTP + WebSocket RPC server — what the | ||
| * side-car / shared-server tiers produce and what {@link createDevServer} | ||
| * re-exposes through its own return contract. | ||
| */ | ||
| interface StartedServer { | ||
| /** Listening origin, e.g. `http://localhost:9999`. */ | ||
| origin: string; | ||
| port: number; | ||
| app: H3; | ||
| /** | ||
| * The crossws node adapter driving the RPC socket (connected peers, | ||
| * pub/sub). Absent when the WebSocket transport is disabled (`ws: false`). | ||
| */ | ||
| ws?: NodeAdapter; | ||
| rpcGroup: BirpcGroup<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>; | ||
| /** | ||
| * The {@link ConnectionMeta} descriptor for this server — the same shape a | ||
| * `__connection.json` route should serve so a devframe client's | ||
| * `resolveWsUrl` can dial back in. | ||
| */ | ||
| connectionMeta: () => ConnectionMeta; | ||
| close: () => Promise<void>; | ||
| } | ||
| /** | ||
| * How the instance's RPC socket is bound: | ||
| * | ||
| * - `sidecar` — its own HTTP+WS server on a dedicated port (`ws.port` / | ||
| * `ws.sidecar`), advertised with that port. | ||
| * - `server` — a shared upgrade route on the host's `node:http` server. | ||
| * - `external` — no local transport: `ws.url` alone names a server that owns | ||
| * both the socket and its auth. | ||
| * - `unbound` — the transport exists but nothing is bound to it yet; the host | ||
| * drives it through {@link InstanceShell.handleUpgrade} / | ||
| * {@link InstanceShell.attach}. | ||
| * - `disabled` — `ws: false`: no WebSocket at all; clients connect over the | ||
| * SSE endpoint instead (`backend: 'sse'`). | ||
| */ | ||
| type InstanceWsTier = 'sidecar' | 'server' | 'external' | 'unbound' | 'disabled'; | ||
| /** The live shell surface an `init` / `mount` callback can reach. */ | ||
| interface InstanceShellApi { | ||
| /** The normalized mount base, with leading and trailing slash. */ | ||
| base: string; | ||
| /** The h3 app every route is mounted on. */ | ||
| app: H3; | ||
| /** The public origin, once known (pinned, or derived from the first request). */ | ||
| origin: () => string | undefined; | ||
| /** The connection meta, once the transport has resolved. */ | ||
| connectionMeta: () => ConnectionMeta | undefined; | ||
| } | ||
| /** What an instance's own initialization contributes to the shell. */ | ||
| interface InstanceShellInit<TContext extends DevframeNodeContext> { | ||
| /** The context every mounted surface shares. */ | ||
| context: TContext; | ||
| /** The `mcp` entry to advertise, when an MCP route was mounted. */ | ||
| mcp?: ConnectionMeta['mcp']; | ||
| /** Torn down before the transport on `close()` (e.g. MCP sessions). */ | ||
| dispose?: () => Promise<void>; | ||
| } | ||
| interface CreateInstanceShellOptions<TContext extends DevframeNodeContext> { | ||
| /** Normalized mount base (leading and trailing slash). */ | ||
| base: string; | ||
| /** h3 app to mount on. A fresh one is created when omitted. */ | ||
| app?: H3; | ||
| /** Public origin, or a getter. Derived from the first request when omitted. */ | ||
| origin?: string | (() => string); | ||
| /** Resolved auth intent: `undefined`/`true` gates, `false` opts out, a handler installs a scheme. */ | ||
| auth?: boolean | DevframeAuthHandler; | ||
| /** Host `node:http` server to share the WS upgrade with. */ | ||
| server?: Server; | ||
| /** Explicit WebSocket control — see {@link DevframeWsOptions}. `false` disables the socket (SSE-only). */ | ||
| ws?: DevframeWsOptions | false; | ||
| /** SSE endpoint control — enabled by default; `false` disables, an object renames the route. */ | ||
| sse?: boolean | DevframeSseOptions; | ||
| /** Bind host for a side-car WebSocket server. Default: `localhost`. */ | ||
| host?: string; | ||
| /** Extra WS-upgrade origins beyond the loopback default; `false` disables the gate. */ | ||
| allowedOrigins?: readonly string[] | WsOriginRegistry | false; | ||
| /** Destroy off-route upgrades on a shared `server`. */ | ||
| destroyUnmatchedUpgrades?: boolean; | ||
| onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void; | ||
| onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void; | ||
| /** | ||
| * Advertise the WS and SSE routes as base-absolute paths (`<base>__ws` / | ||
| * `<base>__sse`) instead of the base-relative default. A hub serves one | ||
| * meta document from several bases, so its clients need the absolute form | ||
| * to resolve the same endpoints. | ||
| */ | ||
| absoluteWsPath?: boolean; | ||
| /** Pick the first port a `ws.sidecar` server tries. Default: a random free port. */ | ||
| resolveSidecarPort?: (host: string) => Promise<number>; | ||
| /** | ||
| * Publish this instance in the global registry (`~/.devframe/instances/`) | ||
| * once its public origin is known — a dynamic import so the registry code | ||
| * stays out of instances that opt out. Omit to skip registration. | ||
| */ | ||
| register?: InstanceRegisterConfig; | ||
| /** Create the context and mount everything that must precede the transport. */ | ||
| init: (api: InstanceShellApi) => Promise<InstanceShellInit<TContext>>; | ||
| /** Mount the routes that describe the resolved transport (discovery, SPA). */ | ||
| mount?: (context: TContext, meta: ConnectionMeta, api: InstanceShellApi) => void | Promise<void>; | ||
| /** Throw the instance's own diagnostic for `connectionMeta()` before readiness. */ | ||
| onMetaUnavailable: () => never; | ||
| } | ||
| /** | ||
| * The identity a shell needs to publish itself in the global instance | ||
| * registry — the parts it can't derive on its own. The shell fills in | ||
| * `pid` / `origin` / `port` / `basePath` / `mcp` / `startedAt` once the | ||
| * origin resolves, then merges {@link InstanceRegisterConfig.overrides} last. | ||
| */ | ||
| interface InstanceRegisterConfig { | ||
| /** Definition id (or a synthetic one for a hub). */ | ||
| id: string; | ||
| /** Display name. */ | ||
| name?: string; | ||
| /** Working directory the instance runs from. Default: `process.cwd()`. */ | ||
| rootDir?: string; | ||
| /** Fields overriding the shell-derived record (from the public option's object form). */ | ||
| overrides?: Partial<DevframeInstanceRecord>; | ||
| } | ||
| /** | ||
| * 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. | ||
| */ | ||
| declare function resolveInstanceRegister(option: boolean | Partial<DevframeInstanceRecord> | undefined, defaults: { | ||
| id: string; | ||
| name?: string; | ||
| rootDir?: string; | ||
| }): InstanceRegisterConfig | undefined; | ||
| /** Live internals the first-party adapters read off an instance. */ | ||
| interface InstanceShellInternals { | ||
| readonly started?: StartedServer; | ||
| readonly authHandler?: DevframeAuthHandler; | ||
| } | ||
| interface InstanceShell<TContext extends DevframeNodeContext> { | ||
| base: string; | ||
| handler: (request: Request) => Promise<Response>; | ||
| nodeMiddleware: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void; | ||
| ready: Promise<void>; | ||
| context: Promise<TContext>; | ||
| connectionMeta: () => ConnectionMeta; | ||
| /** Complete a host server's `upgrade` event on the instance's socket. */ | ||
| handleUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer) => void; | ||
| /** Route a host server's `upgrade` events to the instance's socket. */ | ||
| attach: (server: Server) => () => void; | ||
| close: () => Promise<void>; | ||
| internals: InstanceShellInternals; | ||
| } | ||
| /** Compare two URL paths ignoring a trailing slash. */ | ||
| declare function samePath(a: string, b: string): boolean; | ||
| /** | ||
| * 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 | ||
| */ | ||
| declare function createInstanceShell<TContext extends DevframeNodeContext>(options: CreateInstanceShellOptions<TContext>): InstanceShell<TContext>; | ||
| //#endregion | ||
| export { InstanceShellInit as a, StartedServer as c, samePath as d, DevframeInstanceRecord as f, registerDevframeInstance as h, InstanceShellApi as i, createInstanceShell as l, listLiveDevframeInstances as m, InstanceRegisterConfig as n, InstanceShellInternals as o, DevframeInstanceRegistration as p, InstanceShell as r, InstanceWsTier as s, CreateInstanceShellOptions as t, resolveInstanceRegister as u }; |
| import "./constants.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs"; | ||
| import { t as getInternalContext } from "./context-Cx9V9Grq.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-BjlMQbFB.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-DlHd276T.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-CZXfGEkd.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 src/types/remote-assets.d.ts | ||
| /** | ||
| * A version-locked pointer at browser assets published as their own npm | ||
| * package (e.g. `@devframes/plugin-git-client`), served through devframe's | ||
| * caching back-proxy instead of a directory shipped inside the node package. | ||
| * | ||
| * Resolution order at serve time: | ||
| * | ||
| * 1. The package installed locally (resolved from {@link resolveFrom}) | ||
| * — the zero-network / air-gap path. Version skew warns; a major | ||
| * version mismatch throws. | ||
| * 2. The per-file cache under | ||
| * `<storageDir project>/.remote-assets/<package>@<version>/`. | ||
| * 3. The CDN {@link provider} — each requested file streams through to | ||
| * the browser while being written into the cache. | ||
| * | ||
| * Anywhere a static mount accepts a dist directory (`clientAssets`, | ||
| * `hostStatic`, `mountStatic`) it also accepts this object — see | ||
| * {@link StaticAssetsSource}. | ||
| */ | ||
| interface RemoteAssets { | ||
| /** npm package name that ships the assets, e.g. `@devframes/plugin-git-client`. */ | ||
| package: string; | ||
| /** Exact version to serve, e.g. `1.2.3`. Typically the host package's own version. */ | ||
| version: string; | ||
| /** | ||
| * Subpath inside the package the served assets live under. | ||
| * | ||
| * @default 'dist' | ||
| */ | ||
| path?: string; | ||
| /** | ||
| * CDN that mirrors npm and serves individual package files. | ||
| * | ||
| * @default 'jsdelivr' | ||
| */ | ||
| provider?: RemoteAssetsProvider; | ||
| /** | ||
| * `import.meta.url` of the declaring module. When set, a locally | ||
| * installed copy of {@link package} is resolved from this module's own | ||
| * dependency graph first (works under pnpm's strict layout) and served | ||
| * with zero network. Omitting it skips the installed-package step — | ||
| * cache + CDN still work. | ||
| */ | ||
| resolveFrom?: string | null; | ||
| /** Custom fetch implementation (proxies, tests). Defaults to the global `fetch`. */ | ||
| fetch?: typeof globalThis.fetch; | ||
| /** | ||
| * Never touch the network: serve only from the locally installed package | ||
| * or files already in the cache. | ||
| * | ||
| * @default false | ||
| */ | ||
| offline?: boolean; | ||
| } | ||
| /** | ||
| * Built-in CDN providers (`'jsdelivr'` — default, `'unpkg'`) or a custom | ||
| * provider for corp mirrors. | ||
| */ | ||
| type RemoteAssetsProvider = 'jsdelivr' | 'unpkg' | RemoteAssetsProviderCustom; | ||
| /** A custom {@link RemoteAssets} CDN provider (e.g. an internal npm mirror). */ | ||
| interface RemoteAssetsProviderCustom { | ||
| /** | ||
| * Absolute URL serving `filePath` (package-relative, POSIX, no leading | ||
| * slash) of `pkg@version`. | ||
| */ | ||
| fileUrl: (pkg: string, version: string, filePath: string) => string; | ||
| /** | ||
| * List every file path in `pkg@version` (package-relative, no leading | ||
| * slash). Powers request-path resolution (correct 404s / SPA fallback) | ||
| * and build-time materialization. When omitted, requests are resolved by | ||
| * probing {@link fileUrl} directly and builds cannot materialize from | ||
| * this provider. | ||
| */ | ||
| listFiles?: (pkg: string, version: string, fetchImpl: typeof globalThis.fetch) => Promise<string[]>; | ||
| } | ||
| /** | ||
| * What every static-assets seam accepts: a local dist directory, or a | ||
| * {@link RemoteAssets} pointer served through the caching back-proxy. | ||
| */ | ||
| type StaticAssetsSource = string | RemoteAssets; | ||
| /** | ||
| * What the remote-assets fallback page posts to `window.parent` when a | ||
| * devframe's client assets could be served from neither a local install nor | ||
| * their provider. A viewer embedding the devframe in an iframe listens for | ||
| * `DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE` (`devframe/constants`) and can | ||
| * render the failure in its own design, with the two ways out the page also | ||
| * spells out: install `package@version` locally, or restore network access. | ||
| */ | ||
| interface RemoteAssetsErrorMessage { | ||
| type: 'devframe:remote-assets-error'; | ||
| /** npm package the assets are published as. */ | ||
| package: string; | ||
| /** Exact version the devframe asked for. */ | ||
| version: string; | ||
| /** Why the fetch failed, as reported by the provider or the network stack. */ | ||
| reason: string; | ||
| } | ||
| /** | ||
| * A resolved, servable handle over a {@link RemoteAssets} declaration — | ||
| * produced by `resolveStaticAssetsSource()` (`devframe/utils/remote-assets`) | ||
| * and consumed by the static-serving engine (`devframe/utils/serve-static`). | ||
| */ | ||
| interface RemoteAssetsStore { | ||
| /** The declaration this store serves (with defaults applied). */ | ||
| readonly assets: RemoteAssets & { | ||
| path: string; | ||
| }; | ||
| /** | ||
| * Resolve a request path (relative to the mount base, SPA fallback to | ||
| * `index.html`) and return a `Response`: streamed from the cache when | ||
| * present, otherwise through the provider while being written into the | ||
| * cache. `null` on a miss (404); throws on provider/network failure. | ||
| */ | ||
| serve: (urlPath: string) => Promise<Response | null>; | ||
| /** | ||
| * Download every listed file under `assets.path` into `targetDir` | ||
| * (paths relative to `assets.path`). Requires a provider file listing. | ||
| */ | ||
| materialize: (targetDir: string) => Promise<void>; | ||
| } | ||
| //#endregion | ||
| export { RemoteAssetsStore as a, RemoteAssetsProviderCustom as i, RemoteAssetsErrorMessage as n, StaticAssetsSource as o, RemoteAssetsProvider as r, RemoteAssets as t }; |
| import { t as DEVFRAME_EVENTS } from "./events-DzolaBEs.mjs"; | ||
| //#region src/node/auth/revoke.ts | ||
| /** | ||
| * Flip `isTrusted` to false on any live WS clients connected with `token` | ||
| * and broadcast the `auth:revoked` event so they can react. | ||
| * | ||
| * Shared between persisted-auth revocation and remote-dock token revocation. | ||
| */ | ||
| async function revokeActiveConnectionsForToken(context, token) { | ||
| const rpcHost = context.rpc; | ||
| if (!rpcHost?._rpcGroup) return; | ||
| const affectedSessionIds = /* @__PURE__ */ new Set(); | ||
| for (const client of rpcHost._rpcGroup.clients) if (client.$meta.clientAuthToken === token) { | ||
| affectedSessionIds.add(client.$meta.id); | ||
| client.$meta.isTrusted = false; | ||
| client.$meta.clientAuthToken = void 0; | ||
| } | ||
| if (affectedSessionIds.size === 0) return; | ||
| await rpcHost.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.authRevoked, | ||
| args: [], | ||
| filter: (client) => affectedSessionIds.has(client.$meta.id) | ||
| }); | ||
| } | ||
| /** | ||
| * Revoke an auth token: remove from storage and notify all connected clients | ||
| * using this token that they are no longer trusted. | ||
| */ | ||
| async function revokeAuthToken(context, storage, token) { | ||
| storage.mutate((state) => { | ||
| delete state.trusted[token]; | ||
| }); | ||
| await revokeActiveConnectionsForToken(context, token); | ||
| } | ||
| //#endregion | ||
| export { revokeAuthToken as n, revokeActiveConnectionsForToken as t }; |
| import { d as DevframeRpcConnection, u as DevframeNodeRpcSessionMeta } from "./ws-server-DLtEoLdR.mjs"; | ||
| import { Ct as DevframeAuthHandler, _t as DevframeRpcServerFunctions, gt as DevframeRpcClientFunctions, o as DevframeNodeContext, s as DevframeNodeRpcSession } from "./context-c0OlSc12.mjs"; | ||
| import "./index-B_DPNIT4.mjs"; | ||
| import { BirpcGroup, EventOptions } from "birpc"; | ||
| //#region src/node/rpc-core.d.ts | ||
| interface CreateContextRpcServerOptions { | ||
| context: DevframeNodeContext; | ||
| /** | ||
| * Auth intent: `true`/omitted gates by default, `false` opts out (auto-trust | ||
| * handshake shim), a {@link DevframeAuthHandler} installs a custom scheme. | ||
| */ | ||
| auth?: boolean | DevframeAuthHandler; | ||
| /** Lower-level per-call gate by method name and session, without a full handler. */ | ||
| authorize?: (methodName: string, session: DevframeNodeRpcSession) => boolean; | ||
| /** Called once per new RPC connection, right after its session is created. */ | ||
| onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void; | ||
| /** Called once per closed RPC connection, after the transport's disconnect bookkeeping. */ | ||
| onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void; | ||
| /** Forwarded verbatim to birpc's `rpcOptions` so a host keeps seeing RPC failures. */ | ||
| rpcOptions?: Pick<EventOptions<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>, 'onFunctionError' | 'onGeneralError'>; | ||
| } | ||
| interface ContextRpcServer { | ||
| rpcGroup: BirpcGroup<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>; | ||
| /** The resolved auth handler when `auth` was passed as one. */ | ||
| authHandler?: DevframeAuthHandler; | ||
| /** | ||
| * Connection lifecycle handlers to wire into a transport binding | ||
| * (`attachWsRpcTransport`'s `onConnected` / `onDisconnected`, or any other | ||
| * crossws adapter's peer hooks via `createWsRpcPeerHooks`). | ||
| */ | ||
| onConnected?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void; | ||
| onDisconnected: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void; | ||
| } | ||
| /** | ||
| * Bind a devframe context's registered RPC functions to a birpc group, | ||
| * transport-agnostically — the shared core under the instance shell's own | ||
| * HTTP+WS binding (Node http + WS) and the Bun fetch-upgrade tier of | ||
| * `createHandler`. | ||
| * | ||
| * Owns everything about serving RPC that is independent of *how* peers | ||
| * connect: the auth handler's function registration, the | ||
| * `AsyncLocalStorage`-based session resolver (so | ||
| * `ctx.rpc.getCurrentRpcSession()` works inside handlers), the | ||
| * `authorize` gate, and the `auth: false` auto-trust handshake shim. | ||
| */ | ||
| declare function createContextRpcServer(options: CreateContextRpcServerOptions): ContextRpcServer; | ||
| //#endregion | ||
| export { CreateContextRpcServerOptions as n, createContextRpcServer as r, ContextRpcServer as t }; |
@@ -1,3 +0,3 @@ | ||
| import { F as DevframeDefinition, V as DevframeSnapshotRpcEntry, o as DevframeNodeContext } from "../context-_i51nYOs.mjs"; | ||
| import { o as StaticAssetsSource } from "../remote-assets-Bg4gCUZ_.mjs"; | ||
| import { F as DevframeDefinition, V as DevframeSnapshotRpcEntry, o as DevframeNodeContext } from "../context-c0OlSc12.mjs"; | ||
| import { o as StaticAssetsSource } from "../remote-assets-B13_vRkJ.mjs"; | ||
| //#region src/adapters/build.d.ts | ||
@@ -10,4 +10,5 @@ interface CreateBuildOptions { | ||
| * remote-assets declaration (materialized in full at build time). When | ||
| * omitted the adapter reads `devframe.cli?.distDir` — authors typically | ||
| * set this once on the definition itself. | ||
| * omitted the adapter reads `devframe.clientAssets` (or the deprecated | ||
| * `devframe.cli?.distDir`) — authors typically set this once on the | ||
| * definition itself. | ||
| */ | ||
@@ -14,0 +15,0 @@ distDir?: StaticAssetsSource; |
| import { s as colors } from "../nostics-CzECRXpE.mjs"; | ||
| import { n as strictJsonStringify } from "../serialization-BGzEwAdr.mjs"; | ||
| import { resolveClientAssets } from "../index.mjs"; | ||
| import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME } from "../constants.mjs"; | ||
@@ -7,3 +8,3 @@ import { t as collectStaticRpcDump } from "../static-DV_qUSRS.mjs"; | ||
| import { t as diagnostics } from "../diagnostics-BXWW3VK-.mjs"; | ||
| import { t as createHostContext } from "../context-CQW0lSw8.mjs"; | ||
| import { t as createHostContext } from "../context-Bc5S40Qt.mjs"; | ||
| import { t as resolveStaticAssetsSource } from "../remote-assets-DezQmPQU.mjs"; | ||
@@ -30,4 +31,4 @@ import { t as createH3DevframeHost } from "../host-h3-fRbF9yor.mjs"; | ||
| const outDir = resolve(options.outDir ?? "dist-static"); | ||
| const distSource = options.distDir ?? d.cli?.distDir; | ||
| if (!distSource) throw new Error(`[devframe] createBuild: no distDir for "${d.id}". Set \`cli.distDir\` on the definition or pass it as an option.`); | ||
| const distSource = options.distDir ?? resolveClientAssets(d); | ||
| if (!distSource) throw new Error(`[devframe] createBuild: no client assets for "${d.id}". Set \`clientAssets\` on the definition or pass it as an option.`); | ||
| if (existsSync(outDir)) await fs$1.rm(outDir, { recursive: true }); | ||
@@ -34,0 +35,0 @@ await fs$1.mkdir(outDir, { recursive: true }); |
@@ -1,2 +0,2 @@ | ||
| import { Dt as parseCliFlags, Et as defineCliFlags, F as DevframeDefinition, Tt as InferCliFlags, wt as CliFlagsSchema } from "../context-_i51nYOs.mjs"; | ||
| import { Dt as parseCliFlags, Et as defineCliFlags, F as DevframeDefinition, Tt as InferCliFlags, wt as CliFlagsSchema } from "../context-c0OlSc12.mjs"; | ||
| import { CAC } from "cac"; | ||
@@ -3,0 +3,0 @@ import { H3 } from "h3"; |
| import { s as colors } from "../nostics-CzECRXpE.mjs"; | ||
| import { r as resolveDevServerPort } from "../_shared-BM3PdYli.mjs"; | ||
| import { createBuild } from "./build.mjs"; | ||
| import { t as createDevServer } from "../dev-lpIvwagf.mjs"; | ||
| import { t as createDevServer } from "../dev-BRQA42Lh.mjs"; | ||
| import process from "node:process"; | ||
@@ -6,0 +6,0 @@ import cac$1 from "cac"; |
+11
-10
| import { d as DevframeRpcConnection, n as WsOriginRegistry, u as DevframeNodeRpcSessionMeta } from "../ws-server-DLtEoLdR.mjs"; | ||
| import { Ct as DevframeAuthHandler, F as DevframeDefinition, G as McpRouteOptions, U as DevframeSseOptions, W as DevframeWsOptions, s as DevframeNodeRpcSession } from "../context-_i51nYOs.mjs"; | ||
| import { o as StaticAssetsSource } from "../remote-assets-Bg4gCUZ_.mjs"; | ||
| import { c as StartedServer } from "../instance-shell-BHvBIGP4.mjs"; | ||
| import { a as resolveMcpConnectionMeta, i as resolveDevServerPort, t as ResolveDevServerPortOptions } from "../_shared-BPovvJW8.mjs"; | ||
| import { Ct as DevframeAuthHandler, F as DevframeDefinition, G as McpRouteOptions, U as DevframeSseOptions, W as DevframeWsOptions, s as DevframeNodeRpcSession } from "../context-c0OlSc12.mjs"; | ||
| import { o as StaticAssetsSource } from "../remote-assets-B13_vRkJ.mjs"; | ||
| import { c as StartedServer } from "../instance-shell-CPuYY8pw.mjs"; | ||
| import { a as resolveMcpConnectionMeta, i as resolveDevServerPort, t as ResolveDevServerPortOptions } from "../_shared-CsBsnhuO.mjs"; | ||
| import { H3 } from "h3"; | ||
@@ -24,7 +24,8 @@ //#region src/adapters/dev.d.ts | ||
| /** | ||
| * Override `def.cli?.distDir`. When neither this option nor | ||
| * `def.cli?.distDir` is set, the dev server runs in **bridge mode** — | ||
| * only `__connection.json` and the WS endpoint are mounted; the SPA | ||
| * is expected to be hosted elsewhere (e.g. by a parent Vite/Nuxt | ||
| * dev server via `devframeViteBridge` from `@devframes/vite`). | ||
| * Override the definition's `clientAssets` (or deprecated `cli.distDir`). | ||
| * When neither this option nor the definition's client assets are set, the | ||
| * dev server runs in **bridge mode** — only `__connection.json` and the WS | ||
| * endpoint are mounted; the SPA is expected to be hosted elsewhere (e.g. by | ||
| * a parent Vite/Nuxt dev server via `devframeViteBridge` from | ||
| * `@devframes/vite`). | ||
| */ | ||
@@ -121,3 +122,3 @@ distDir?: StaticAssetsSource; | ||
| * | ||
| * When `distDir` is omitted (and `def.cli?.distDir` is unset) the | ||
| * When `distDir` is omitted (and the definition's client assets are unset) the | ||
| * server runs in **bridge mode**: only `__connection.json` and the WS | ||
@@ -124,0 +125,0 @@ * endpoint are mounted, with no SPA mount. The SPA is expected to be |
| import { i as resolveMcpConnectionMeta, r as resolveDevServerPort } from "../_shared-BM3PdYli.mjs"; | ||
| import { t as createDevServer } from "../dev-lpIvwagf.mjs"; | ||
| import { t as createDevServer } from "../dev-BRQA42Lh.mjs"; | ||
| export { createDevServer, resolveDevServerPort, resolveMcpConnectionMeta }; |
@@ -1,2 +0,2 @@ | ||
| import { F as DevframeDefinition, o as DevframeNodeContext } from "../context-_i51nYOs.mjs"; | ||
| import { F as DevframeDefinition, o as DevframeNodeContext } from "../context-c0OlSc12.mjs"; | ||
| //#region src/adapters/embedded.d.ts | ||
@@ -3,0 +3,0 @@ interface CreateEmbeddedOptions { |
| import { d as DevframeRpcConnection, n as WsOriginRegistry, u as DevframeNodeRpcSessionMeta } from "../ws-server-DLtEoLdR.mjs"; | ||
| import { Ct as DevframeAuthHandler, F as DevframeDefinition, G as McpRouteOptions, U as DevframeSseOptions, W as DevframeWsOptions, bt as DevframeStorageScope, o as DevframeNodeContext, s as DevframeNodeRpcSession, t as ConnectionMeta } from "../context-_i51nYOs.mjs"; | ||
| import { o as StaticAssetsSource } from "../remote-assets-Bg4gCUZ_.mjs"; | ||
| import { c as StartedServer, f as DevframeInstanceRecord } from "../instance-shell-BHvBIGP4.mjs"; | ||
| import { Ct as DevframeAuthHandler, F as DevframeDefinition, G as McpRouteOptions, U as DevframeSseOptions, W as DevframeWsOptions, bt as DevframeStorageScope, o as DevframeNodeContext, s as DevframeNodeRpcSession, t as ConnectionMeta } from "../context-c0OlSc12.mjs"; | ||
| import { o as StaticAssetsSource } from "../remote-assets-B13_vRkJ.mjs"; | ||
| import { c as StartedServer, f as DevframeInstanceRecord } from "../instance-shell-CPuYY8pw.mjs"; | ||
| import { Buffer } from "node:buffer"; | ||
@@ -21,6 +21,7 @@ import { IncomingMessage, Server, ServerResponse } from "node:http"; | ||
| /** | ||
| * Override `def.cli?.distDir`. When neither is set — or `false` is passed | ||
| * to suppress the definition's own `distDir` — the handler runs in | ||
| * **bridge mode**: only `__connection.json`, the WS endpoint, and the MCP | ||
| * route (when enabled) are served; the SPA is hosted elsewhere. | ||
| * Override the definition's `clientAssets` (or deprecated `cli.distDir`). | ||
| * When neither is set — or `false` is passed to suppress the definition's | ||
| * own client assets — the handler runs in **bridge mode**: only | ||
| * `__connection.json`, the WS endpoint, and the MCP route (when enabled) are | ||
| * served; the SPA is hosted elsewhere. | ||
| */ | ||
@@ -27,0 +28,0 @@ distDir?: StaticAssetsSource | false; |
@@ -1,2 +0,2 @@ | ||
| import { n as getInstanceInternals, r as initDevframe } from "../dev-lpIvwagf.mjs"; | ||
| import { n as getInstanceInternals, r as initDevframe } from "../dev-BRQA42Lh.mjs"; | ||
| export { getInstanceInternals, initDevframe }; |
@@ -1,2 +0,2 @@ | ||
| import { F as DevframeDefinition, o as DevframeNodeContext } from "../context-_i51nYOs.mjs"; | ||
| import { F as DevframeDefinition, o as DevframeNodeContext } from "../context-c0OlSc12.mjs"; | ||
| import { H3 } from "h3"; | ||
@@ -3,0 +3,0 @@ import "@modelcontextprotocol/server"; |
@@ -1,2 +0,2 @@ | ||
| import { i as createMcpServer, n as mountMcpHttp, r as createMcpFetchHandler } from "../http-BKiqHcXX.mjs"; | ||
| import { i as createMcpServer, n as mountMcpHttp, r as createMcpFetchHandler } from "../http-D7wWuy7-.mjs"; | ||
| export { createMcpFetchHandler, createMcpServer, mountMcpHttp }; |
| import { f as RpcCacheManager, p as RpcCacheOptions } from "../index-DOg4S5wk.mjs"; | ||
| import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-BBLeGfpt.mjs"; | ||
| import { Q as DevframeServiceScopeOf, X as DevframeServiceMeta, _t as DevframeRpcServerFunctions, b as StreamSink, d as RpcSharedStateHost, dt as ScopedRpcFn, ft as ScopedServerFunctions, gt as DevframeRpcClientFunctions, k as SharedState, mt as SettingsForNamespace, nt as DevframeServicesState, ot as DevframeSettings, pt as ScopedSharedStates, t as ConnectionMeta, u as RpcSharedStateGetOptions, y as StreamReader, zt as EventEmitter } from "../context-_i51nYOs.mjs"; | ||
| import { Q as DevframeServiceScopeOf, X as DevframeServiceMeta, _t as DevframeRpcServerFunctions, b as StreamSink, d as RpcSharedStateHost, dt as ScopedRpcFn, ft as ScopedServerFunctions, gt as DevframeRpcClientFunctions, k as SharedState, mt as SettingsForNamespace, nt as DevframeServicesState, ot as DevframeSettings, pt as ScopedSharedStates, t as ConnectionMeta, u as RpcSharedStateGetOptions, y as StreamReader, zt as EventEmitter } from "../context-c0OlSc12.mjs"; | ||
| import { t as SseRpcChannelOptions } from "../sse-client-CdGD_B8_.mjs"; | ||
@@ -5,0 +5,0 @@ import { t as WsRpcChannelOptions } from "../ws-client-Bo2t6hES.mjs"; |
@@ -8,3 +8,3 @@ //#region src/events.d.ts | ||
| * | ||
| * **Keep this in sync with [`docs/guide/events.md`](../../../docs/guide/events.md)** | ||
| * **Keep this in sync with [`docs/content/1.guide/20.events.md`](../../../docs/content/1.guide/20.events.md)** | ||
| * (the "Core devframe events" section): every name here appears in that page's | ||
@@ -11,0 +11,0 @@ * tables, and every name there resolves to an entry here. Add, rename, or |
@@ -1,2 +0,2 @@ | ||
| import { t as DEVFRAME_EVENTS } from "./events-DKfSmoTj.mjs"; | ||
| import { t as DEVFRAME_EVENTS } from "./events-DzolaBEs.mjs"; | ||
| //#region src/constants.ts | ||
@@ -3,0 +3,0 @@ const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json"; |
+11
-3
| import "./index-DOg4S5wk.mjs"; | ||
| import { C as RpcFunctionType, T as RpcReturnSchema, _ as RpcFunctionDefinition, g as RpcFunctionAgentOptions, i as RpcArgsSchema } from "./types-BBLeGfpt.mjs"; | ||
| import { d as DevframeRpcConnection, f as DevframeRpcConnectionRequest, p as DevframeRpcTransportKind, u as DevframeNodeRpcSessionMeta } from "./ws-server-DLtEoLdR.mjs"; | ||
| import { $ as DevframeServicesHost, At as AgentResource, B as DevframeSetupInfo, Bt as EventUnsubscribe, F as DevframeDefinition, Ft as AgentToolProvider, G as McpRouteOptions, H as DevframeSnapshotRpcInputs, I as DevframeDeploymentKind, It as AgentToolProviderHandle, J as DevframeServiceId, K as DevframeServiceDefinition, L as DevframeDockDefaults, Lt as DevframeAgentHost, Mt as AgentResourceInput, Nt as AgentTool, Ot as AgentHandle, P as DevframeCliOptions, Pt as AgentToolInput, Q as DevframeServiceScopeOf, R as DevframeDuplicationStrategy, Rt as DevframeAgentHostEvents, St as DevframeDiagnosticsLogger, U as DevframeSseOptions, V as DevframeSnapshotRpcEntry, Vt as EventsMap, W as DevframeWsOptions, X as DevframeServiceMeta, Y as DevframeServiceInput, Z as DevframeServiceOf, _t as DevframeRpcServerFunctions, a as DevframeConnectionConfigsRegistry, at as DevframeScopedStreamingHost, bt as DevframeStorageScope, c as RpcBroadcastOptions, ct as DevframeSettingsStore, d as RpcSharedStateHost, dt as ScopedRpcFn, et as DevframeServicesRegistry, f as RpcStreamingChannel, ft as ScopedServerFunctions, gt as DevframeRpcClientFunctions, ht as DevframeViewHost, i as DevframeCapabilities, it as DevframeScopedNodeRpc, jt as AgentResourceContent, kt as AgentManifest, l as RpcFunctionsHost, lt as ScopedBroadcastOptions, m as RpcStreamingHost, mt as SettingsForNamespace, n as ConnectionMetaSse, nt as DevframeServicesState, o as DevframeNodeContext, ot as DevframeSettings, p as RpcStreamingChannelOptions, pt as ScopedSharedStates, q as DevframeServiceDescriptor, r as ConnectionMetaWebsocket, rt as DevframeScopedNodeContext, s as DevframeNodeRpcSession, st as DevframeSettingsRegistry, t as ConnectionMeta, tt as DevframeServicesScopeRegistry, u as RpcSharedStateGetOptions, ut as ScopedClientFunctions, vt as DevframeRpcSharedStates, xt as DevframeDiagnosticsHost, yt as DevframeHost, z as DevframeRpcOptions, zt as EventEmitter } from "./context-_i51nYOs.mjs"; | ||
| import { a as RemoteAssetsStore, i as RemoteAssetsProviderCustom, n as RemoteAssetsErrorMessage, o as StaticAssetsSource, r as RemoteAssetsProvider, t as RemoteAssets } from "./remote-assets-Bg4gCUZ_.mjs"; | ||
| import { $ as DevframeServicesHost, At as AgentResource, B as DevframeSetupInfo, Bt as EventUnsubscribe, F as DevframeDefinition, Ft as AgentToolProvider, G as McpRouteOptions, H as DevframeSnapshotRpcInputs, I as DevframeDeploymentKind, It as AgentToolProviderHandle, J as DevframeServiceId, K as DevframeServiceDefinition, L as DevframeDockDefaults, Lt as DevframeAgentHost, Mt as AgentResourceInput, Nt as AgentTool, Ot as AgentHandle, P as DevframeCliOptions, Pt as AgentToolInput, Q as DevframeServiceScopeOf, R as DevframeDuplicationStrategy, Rt as DevframeAgentHostEvents, St as DevframeDiagnosticsLogger, U as DevframeSseOptions, V as DevframeSnapshotRpcEntry, Vt as EventsMap, W as DevframeWsOptions, X as DevframeServiceMeta, Y as DevframeServiceInput, Z as DevframeServiceOf, _t as DevframeRpcServerFunctions, a as DevframeConnectionConfigsRegistry, at as DevframeScopedStreamingHost, bt as DevframeStorageScope, c as RpcBroadcastOptions, ct as DevframeSettingsStore, d as RpcSharedStateHost, dt as ScopedRpcFn, et as DevframeServicesRegistry, f as RpcStreamingChannel, ft as ScopedServerFunctions, gt as DevframeRpcClientFunctions, ht as DevframeViewHost, i as DevframeCapabilities, it as DevframeScopedNodeRpc, jt as AgentResourceContent, kt as AgentManifest, l as RpcFunctionsHost, lt as ScopedBroadcastOptions, m as RpcStreamingHost, mt as SettingsForNamespace, n as ConnectionMetaSse, nt as DevframeServicesState, o as DevframeNodeContext, ot as DevframeSettings, p as RpcStreamingChannelOptions, pt as ScopedSharedStates, q as DevframeServiceDescriptor, r as ConnectionMetaWebsocket, rt as DevframeScopedNodeContext, s as DevframeNodeRpcSession, st as DevframeSettingsRegistry, t as ConnectionMeta, tt as DevframeServicesScopeRegistry, u as RpcSharedStateGetOptions, ut as ScopedClientFunctions, vt as DevframeRpcSharedStates, xt as DevframeDiagnosticsHost, yt as DevframeHost, z as DevframeRpcOptions, zt as EventEmitter } from "./context-c0OlSc12.mjs"; | ||
| import { a as RemoteAssetsStore, i as RemoteAssetsProviderCustom, n as RemoteAssetsErrorMessage, o as StaticAssetsSource, r as RemoteAssetsProvider, t as RemoteAssets } from "./remote-assets-B13_vRkJ.mjs"; | ||
| import { i as DevframeDefineDiagnosticsOptions } from "./nostics-SPGDgHEP.mjs"; | ||
@@ -14,3 +14,11 @@ //#region src/define.d.ts | ||
| declare function defineDevframe(d: DevframeDefinition): DevframeDefinition; | ||
| /** | ||
| * Resolve a definition's client assets source — the built SPA served as its | ||
| * UI. Prefers the top-level {@link DevframeDefinition.clientAssets} and falls | ||
| * back to the deprecated {@link DevframeCliOptions.distDir}, so both the new | ||
| * and legacy shapes resolve. Returns `undefined` when neither is set (bridge | ||
| * mode — the SPA is hosted elsewhere). | ||
| */ | ||
| declare function resolveClientAssets(d: DevframeDefinition): StaticAssetsSource | undefined; | ||
| //#endregion | ||
| export { type AgentHandle, type AgentManifest, type AgentResource, type AgentResourceContent, type AgentResourceInput, type AgentTool, type AgentToolInput, type AgentToolProvider, type AgentToolProviderHandle, type ConnectionMeta, type ConnectionMetaSse, type ConnectionMetaWebsocket, type DevframeAgentHost, type DevframeAgentHostEvents, type DevframeCapabilities, type DevframeCliOptions, type DevframeConnectionConfigsRegistry, type DevframeDefineDiagnosticsOptions, type DevframeDefinition, type DevframeDeploymentKind, type DevframeDiagnosticsHost, type DevframeDiagnosticsLogger, type DevframeDockDefaults, type DevframeDuplicationStrategy, type DevframeHost, type DevframeNodeContext, type DevframeNodeRpcSession, type DevframeNodeRpcSessionMeta, type DevframeRpcClientFunctions, type DevframeRpcConnection, type DevframeRpcConnectionRequest, type DevframeRpcOptions, type DevframeRpcServerFunctions, type DevframeRpcSharedStates, type DevframeRpcTransportKind, type DevframeScopedNodeContext, type DevframeScopedNodeRpc, type DevframeScopedStreamingHost, type DevframeServiceDefinition, type DevframeServiceDescriptor, type DevframeServiceId, type DevframeServiceInput, type DevframeServiceMeta, type DevframeServiceOf, type DevframeServiceScopeOf, type DevframeServicesHost, type DevframeServicesRegistry, type DevframeServicesScopeRegistry, type DevframeServicesState, type DevframeSettings, type DevframeSettingsRegistry, type DevframeSettingsStore, type DevframeSetupInfo, type DevframeSnapshotRpcEntry, type DevframeSnapshotRpcInputs, type DevframeSseOptions, type DevframeStorageScope, type DevframeViewHost, type DevframeWsOptions, type EventEmitter, type EventUnsubscribe, type EventsMap, type McpRouteOptions, type RemoteAssets, type RemoteAssetsErrorMessage, type RemoteAssetsProvider, type RemoteAssetsProviderCustom, type RemoteAssetsStore, type RpcBroadcastOptions, type RpcFunctionAgentOptions, type RpcFunctionsHost, type RpcSharedStateGetOptions, type RpcSharedStateHost, type RpcStreamingChannel, type RpcStreamingChannelOptions, type RpcStreamingHost, type ScopedBroadcastOptions, type ScopedClientFunctions, type ScopedRpcFn, type ScopedServerFunctions, type ScopedSharedStates, type SettingsForNamespace, type StaticAssetsSource, defineDevframe, defineRpcFunction }; | ||
| export { type AgentHandle, type AgentManifest, type AgentResource, type AgentResourceContent, type AgentResourceInput, type AgentTool, type AgentToolInput, type AgentToolProvider, type AgentToolProviderHandle, type ConnectionMeta, type ConnectionMetaSse, type ConnectionMetaWebsocket, type DevframeAgentHost, type DevframeAgentHostEvents, type DevframeCapabilities, type DevframeCliOptions, type DevframeConnectionConfigsRegistry, type DevframeDefineDiagnosticsOptions, type DevframeDefinition, type DevframeDeploymentKind, type DevframeDiagnosticsHost, type DevframeDiagnosticsLogger, type DevframeDockDefaults, type DevframeDuplicationStrategy, type DevframeHost, type DevframeNodeContext, type DevframeNodeRpcSession, type DevframeNodeRpcSessionMeta, type DevframeRpcClientFunctions, type DevframeRpcConnection, type DevframeRpcConnectionRequest, type DevframeRpcOptions, type DevframeRpcServerFunctions, type DevframeRpcSharedStates, type DevframeRpcTransportKind, type DevframeScopedNodeContext, type DevframeScopedNodeRpc, type DevframeScopedStreamingHost, type DevframeServiceDefinition, type DevframeServiceDescriptor, type DevframeServiceId, type DevframeServiceInput, type DevframeServiceMeta, type DevframeServiceOf, type DevframeServiceScopeOf, type DevframeServicesHost, type DevframeServicesRegistry, type DevframeServicesScopeRegistry, type DevframeServicesState, type DevframeSettings, type DevframeSettingsRegistry, type DevframeSettingsStore, type DevframeSetupInfo, type DevframeSnapshotRpcEntry, type DevframeSnapshotRpcInputs, type DevframeSseOptions, type DevframeStorageScope, type DevframeViewHost, type DevframeWsOptions, type EventEmitter, type EventUnsubscribe, type EventsMap, type McpRouteOptions, type RemoteAssets, type RemoteAssetsErrorMessage, type RemoteAssetsProvider, type RemoteAssetsProviderCustom, type RemoteAssetsStore, type RpcBroadcastOptions, type RpcFunctionAgentOptions, type RpcFunctionsHost, type RpcSharedStateGetOptions, type RpcSharedStateHost, type RpcStreamingChannel, type RpcStreamingChannelOptions, type RpcStreamingHost, type ScopedBroadcastOptions, type ScopedClientFunctions, type ScopedRpcFn, type ScopedServerFunctions, type ScopedSharedStates, type SettingsForNamespace, type StaticAssetsSource, defineDevframe, defineRpcFunction, resolveClientAssets }; |
+11
-1
@@ -11,3 +11,13 @@ import { t as createDefineWrapperWithContext } from "./define-BLWPsH6y.mjs"; | ||
| } | ||
| /** | ||
| * Resolve a definition's client assets source — the built SPA served as its | ||
| * UI. Prefers the top-level {@link DevframeDefinition.clientAssets} and falls | ||
| * back to the deprecated {@link DevframeCliOptions.distDir}, so both the new | ||
| * and legacy shapes resolve. Returns `undefined` when neither is set (bridge | ||
| * mode — the SPA is hosted elsewhere). | ||
| */ | ||
| function resolveClientAssets(d) { | ||
| return d.clientAssets ?? d.cli?.distDir; | ||
| } | ||
| //#endregion | ||
| export { defineDevframe, defineRpcFunction }; | ||
| export { defineDevframe, defineRpcFunction, resolveClientAssets }; |
| import { v as RpcFunctionDefinitionAny } from "../types-BBLeGfpt.mjs"; | ||
| import { At as AgentResource, Ft as AgentToolProvider, It as AgentToolProviderHandle, Lt as DevframeAgentHost$1, Mt as AgentResourceInput, Nt as AgentTool, Ot as AgentHandle, Pt as AgentToolInput, Rt as DevframeAgentHostEvents, jt as AgentResourceContent, kt as AgentManifest, o as DevframeNodeContext, yt as DevframeHost, zt as EventEmitter } from "../context-_i51nYOs.mjs"; | ||
| import { a as RemoteAssetsStore } from "../remote-assets-Bg4gCUZ_.mjs"; | ||
| import { a as InstanceShellInit, c as StartedServer, d as samePath, f as DevframeInstanceRecord, h as registerDevframeInstance, i as InstanceShellApi, l as createInstanceShell, m as listLiveDevframeInstances, n as InstanceRegisterConfig, o as InstanceShellInternals, p as DevframeInstanceRegistration, r as InstanceShell, s as InstanceWsTier, t as CreateInstanceShellOptions, u as resolveInstanceRegister } from "../instance-shell-BHvBIGP4.mjs"; | ||
| import { n as normalizeBasePath, r as resolveBasePath } from "../_shared-BPovvJW8.mjs"; | ||
| import { n as CreateContextRpcServerOptions, r as createContextRpcServer, t as ContextRpcServer } from "../rpc-core-CDZoGYiD.mjs"; | ||
| import { At as AgentResource, Ft as AgentToolProvider, It as AgentToolProviderHandle, Lt as DevframeAgentHost$1, Mt as AgentResourceInput, Nt as AgentTool, Ot as AgentHandle, Pt as AgentToolInput, Rt as DevframeAgentHostEvents, jt as AgentResourceContent, kt as AgentManifest, o as DevframeNodeContext, yt as DevframeHost, zt as EventEmitter } from "../context-c0OlSc12.mjs"; | ||
| import { a as RemoteAssetsStore } from "../remote-assets-B13_vRkJ.mjs"; | ||
| import { a as InstanceShellInit, c as StartedServer, d as samePath, f as DevframeInstanceRecord, h as registerDevframeInstance, i as InstanceShellApi, l as createInstanceShell, m as listLiveDevframeInstances, n as InstanceRegisterConfig, o as InstanceShellInternals, p as DevframeInstanceRegistration, r as InstanceShell, s as InstanceWsTier, t as CreateInstanceShellOptions, u as resolveInstanceRegister } from "../instance-shell-CPuYY8pw.mjs"; | ||
| import { n as normalizeBasePath, r as resolveBasePath } from "../_shared-CsBsnhuO.mjs"; | ||
| import { n as CreateContextRpcServerOptions, r as createContextRpcServer, t as ContextRpcServer } from "../rpc-core-Oyz-cJBC.mjs"; | ||
| //#region src/node/agent-args.d.ts | ||
@@ -8,0 +8,0 @@ /** |
| import { n as peekRpcWireFrame, t as createRpcWireCodec } from "../wire-codec-0K-o5MYW.mjs"; | ||
| import { t as diagnostics } from "../diagnostics-BXWW3VK-.mjs"; | ||
| import { n as coerceAgentPositionalArgs, t as DevframeAgentHost } from "../host-agent-B84av916.mjs"; | ||
| import { n as coerceAgentPositionalArgs, t as DevframeAgentHost } from "../host-agent-DbPWo0Bl.mjs"; | ||
| import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-BM3PdYli.mjs"; | ||
| import { t as createH3DevframeHost } from "../host-h3-fRbF9yor.mjs"; | ||
| import { i as registerDevframeInstance, n as listLiveDevframeInstances } from "../instance-registry-BjlMQbFB.mjs"; | ||
| import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, r as samePath, t as createInstanceShell } from "../instance-shell-B-uevnK-.mjs"; | ||
| import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, r as samePath, t as createInstanceShell } from "../instance-shell-fB5PyVtR.mjs"; | ||
| import { t as createContextRpcServer } from "../rpc-core-DlHd276T.mjs"; | ||
| export { DevframeAgentHost, coerceAgentPositionalArgs, createContextRpcServer, createH3DevframeHost, createInstanceShell, createRpcWireCodec, diagnostics, listLiveDevframeInstances, normalizeBasePath, normalizeHttpServerUrl, peekRpcWireFrame, registerDevframeInstance, resolveBasePath, resolveInstanceRegister, samePath }; |
@@ -1,3 +0,3 @@ | ||
| import { Ct as DevframeAuthHandler } from "../context-_i51nYOs.mjs"; | ||
| import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, o as revokeActiveConnectionsForToken, r as getTempAuthCode, s as revokeAuthToken, t as buildOtpAuthUrl } from "../index-CEvd-YXD.mjs"; | ||
| import { Ct as DevframeAuthHandler } from "../context-c0OlSc12.mjs"; | ||
| import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, o as revokeActiveConnectionsForToken, r as getTempAuthCode, s as revokeAuthToken, t as buildOtpAuthUrl } from "../index-B_DPNIT4.mjs"; | ||
| export { DevframeAuthHandler, buildOtpAuthUrl, exchangeTempAuthCode, getTempAuthCode, refreshTempAuthCode, revokeActiveConnectionsForToken, revokeAuthToken, verifyAuthToken }; |
@@ -1,3 +0,3 @@ | ||
| import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "../revoke-3Q6aDKWk.mjs"; | ||
| import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "../revoke-PihtvVil.mjs"; | ||
| import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, r as getTempAuthCode, t as buildOtpAuthUrl } from "../state-CK9LjrnT.mjs"; | ||
| export { buildOtpAuthUrl, exchangeTempAuthCode, getTempAuthCode, refreshTempAuthCode, revokeActiveConnectionsForToken, revokeAuthToken, verifyAuthToken }; |
@@ -1,3 +0,3 @@ | ||
| import { a as internalContextMap, i as getInternalContext, n as InternalAnonymousAuthStorage, r as RemoteTokenRecord, t as DevframeInternalContext } from "../context-CO7eZHJy.mjs"; | ||
| import { n as normalizeBasePath, r as resolveBasePath } from "../_shared-BPovvJW8.mjs"; | ||
| import { a as internalContextMap, i as getInternalContext, n as InternalAnonymousAuthStorage, r as RemoteTokenRecord, t as DevframeInternalContext } from "../context-DO0Qu1O8.mjs"; | ||
| import { n as normalizeBasePath, r as resolveBasePath } from "../_shared-CsBsnhuO.mjs"; | ||
| export { type DevframeInternalContext, type InternalAnonymousAuthStorage, type RemoteTokenRecord, getInternalContext, internalContextMap, normalizeBasePath, resolveBasePath }; |
| import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-BM3PdYli.mjs"; | ||
| import { n as internalContextMap, t as getInternalContext } from "../context-BUJGvlje.mjs"; | ||
| import { n as internalContextMap, t as getInternalContext } from "../context-Cx9V9Grq.mjs"; | ||
| export { getInternalContext, internalContextMap, normalizeBasePath, resolveBasePath }; |
| import "../index-DOg4S5wk.mjs"; | ||
| import { v as RpcFunctionDefinitionAny } from "../types-BBLeGfpt.mjs"; | ||
| import { k as SharedState, l as RpcFunctionsHost, o as DevframeNodeContext, yt as DevframeHost } from "../context-_i51nYOs.mjs"; | ||
| import { k as SharedState, l as RpcFunctionsHost, o as DevframeNodeContext, yt as DevframeHost } from "../context-c0OlSc12.mjs"; | ||
| import { BirpcGroup } from "birpc"; | ||
@@ -5,0 +5,0 @@ //#region src/node/context.d.ts |
@@ -1,3 +0,3 @@ | ||
| import { t as createHostContext } from "../context-CQW0lSw8.mjs"; | ||
| import { t as createHostContext } from "../context-Bc5S40Qt.mjs"; | ||
| import { t as createStorage } from "../storage-BXNVhKyR.mjs"; | ||
| export { createHostContext, createStorage }; |
| import "../index-DOg4S5wk.mjs"; | ||
| import { E as Thenable, S as RpcFunctionSetupResult, c as RpcDump, g as RpcFunctionAgentOptions } from "../types-BBLeGfpt.mjs"; | ||
| import "../context-_i51nYOs.mjs"; | ||
| import "../context-c0OlSc12.mjs"; | ||
| import { SimpleSchema } from "../utils/simple-schema.mjs"; | ||
@@ -5,0 +5,0 @@ //#region src/recipes/common-rpc-functions.d.ts |
@@ -1,3 +0,3 @@ | ||
| import { Ct as DevframeAuthHandler, o as DevframeNodeContext } from "../context-_i51nYOs.mjs"; | ||
| import "../index-CEvd-YXD.mjs"; | ||
| import { Ct as DevframeAuthHandler, o as DevframeNodeContext } from "../context-c0OlSc12.mjs"; | ||
| import "../index-B_DPNIT4.mjs"; | ||
| //#region src/recipes/interactive-auth.d.ts | ||
@@ -4,0 +4,0 @@ interface CreateInteractiveAuthOptions { |
@@ -5,3 +5,3 @@ import { s as colors } from "../nostics-CzECRXpE.mjs"; | ||
| import { a as verifyAuthToken, n as exchangeTempAuthCode, r as getTempAuthCode, t as buildOtpAuthUrl } from "../state-CK9LjrnT.mjs"; | ||
| import { t as getInternalContext } from "../context-BUJGvlje.mjs"; | ||
| import { t as getInternalContext } from "../context-Cx9V9Grq.mjs"; | ||
| import { t as s } from "../simple-schema-DQPZrAaZ.mjs"; | ||
@@ -8,0 +8,0 @@ //#region src/recipes/interactive-auth.ts |
| import { n as WsOriginRegistry } from "../../ws-server-DLtEoLdR.mjs"; | ||
| import { t as ContextRpcServer } from "../../rpc-core-CDZoGYiD.mjs"; | ||
| import { t as ContextRpcServer } from "../../rpc-core-Oyz-cJBC.mjs"; | ||
| //#region src/rpc/transports/ws-bun.d.ts | ||
@@ -4,0 +4,0 @@ interface AttachBunWsTransportOptions { |
| import { n as WsOriginRegistry } from "../../ws-server-DLtEoLdR.mjs"; | ||
| import { t as ContextRpcServer } from "../../rpc-core-CDZoGYiD.mjs"; | ||
| import { t as ContextRpcServer } from "../../rpc-core-Oyz-cJBC.mjs"; | ||
| //#region src/rpc/transports/ws-deno.d.ts | ||
@@ -4,0 +4,0 @@ interface AttachDenoWsTransportOptions { |
| import { g as RpcFunctionAgentOptions } from "../types-BBLeGfpt.mjs"; | ||
| import { d as DevframeRpcConnection, f as DevframeRpcConnectionRequest, p as DevframeRpcTransportKind, u as DevframeNodeRpcSessionMeta } from "../ws-server-DLtEoLdR.mjs"; | ||
| import { $ as DevframeServicesHost, At as AgentResource, B as DevframeSetupInfo, Bt as EventUnsubscribe, F as DevframeDefinition, Ft as AgentToolProvider, G as McpRouteOptions, H as DevframeSnapshotRpcInputs, I as DevframeDeploymentKind, It as AgentToolProviderHandle, J as DevframeServiceId, K as DevframeServiceDefinition, L as DevframeDockDefaults, Lt as DevframeAgentHost, Mt as AgentResourceInput, Nt as AgentTool, Ot as AgentHandle, P as DevframeCliOptions, Pt as AgentToolInput, Q as DevframeServiceScopeOf, R as DevframeDuplicationStrategy, Rt as DevframeAgentHostEvents, St as DevframeDiagnosticsLogger, U as DevframeSseOptions, V as DevframeSnapshotRpcEntry, Vt as EventsMap, W as DevframeWsOptions, X as DevframeServiceMeta, Y as DevframeServiceInput, Z as DevframeServiceOf, _t as DevframeRpcServerFunctions, a as DevframeConnectionConfigsRegistry, at as DevframeScopedStreamingHost, bt as DevframeStorageScope, c as RpcBroadcastOptions, ct as DevframeSettingsStore, d as RpcSharedStateHost, dt as ScopedRpcFn, et as DevframeServicesRegistry, f as RpcStreamingChannel, ft as ScopedServerFunctions, gt as DevframeRpcClientFunctions, ht as DevframeViewHost, i as DevframeCapabilities, it as DevframeScopedNodeRpc, jt as AgentResourceContent, kt as AgentManifest, l as RpcFunctionsHost, lt as ScopedBroadcastOptions, m as RpcStreamingHost, mt as SettingsForNamespace, n as ConnectionMetaSse, nt as DevframeServicesState, o as DevframeNodeContext, ot as DevframeSettings, p as RpcStreamingChannelOptions, pt as ScopedSharedStates, q as DevframeServiceDescriptor, r as ConnectionMetaWebsocket, rt as DevframeScopedNodeContext, s as DevframeNodeRpcSession, st as DevframeSettingsRegistry, t as ConnectionMeta, tt as DevframeServicesScopeRegistry, u as RpcSharedStateGetOptions, ut as ScopedClientFunctions, vt as DevframeRpcSharedStates, xt as DevframeDiagnosticsHost, yt as DevframeHost, z as DevframeRpcOptions, zt as EventEmitter } from "../context-_i51nYOs.mjs"; | ||
| import { a as RemoteAssetsStore, i as RemoteAssetsProviderCustom, n as RemoteAssetsErrorMessage, o as StaticAssetsSource, r as RemoteAssetsProvider, t as RemoteAssets } from "../remote-assets-Bg4gCUZ_.mjs"; | ||
| import { $ as DevframeServicesHost, At as AgentResource, B as DevframeSetupInfo, Bt as EventUnsubscribe, F as DevframeDefinition, Ft as AgentToolProvider, G as McpRouteOptions, H as DevframeSnapshotRpcInputs, I as DevframeDeploymentKind, It as AgentToolProviderHandle, J as DevframeServiceId, K as DevframeServiceDefinition, L as DevframeDockDefaults, Lt as DevframeAgentHost, Mt as AgentResourceInput, Nt as AgentTool, Ot as AgentHandle, P as DevframeCliOptions, Pt as AgentToolInput, Q as DevframeServiceScopeOf, R as DevframeDuplicationStrategy, Rt as DevframeAgentHostEvents, St as DevframeDiagnosticsLogger, U as DevframeSseOptions, V as DevframeSnapshotRpcEntry, Vt as EventsMap, W as DevframeWsOptions, X as DevframeServiceMeta, Y as DevframeServiceInput, Z as DevframeServiceOf, _t as DevframeRpcServerFunctions, a as DevframeConnectionConfigsRegistry, at as DevframeScopedStreamingHost, bt as DevframeStorageScope, c as RpcBroadcastOptions, ct as DevframeSettingsStore, d as RpcSharedStateHost, dt as ScopedRpcFn, et as DevframeServicesRegistry, f as RpcStreamingChannel, ft as ScopedServerFunctions, gt as DevframeRpcClientFunctions, ht as DevframeViewHost, i as DevframeCapabilities, it as DevframeScopedNodeRpc, jt as AgentResourceContent, kt as AgentManifest, l as RpcFunctionsHost, lt as ScopedBroadcastOptions, m as RpcStreamingHost, mt as SettingsForNamespace, n as ConnectionMetaSse, nt as DevframeServicesState, o as DevframeNodeContext, ot as DevframeSettings, p as RpcStreamingChannelOptions, pt as ScopedSharedStates, q as DevframeServiceDescriptor, r as ConnectionMetaWebsocket, rt as DevframeScopedNodeContext, s as DevframeNodeRpcSession, st as DevframeSettingsRegistry, t as ConnectionMeta, tt as DevframeServicesScopeRegistry, u as RpcSharedStateGetOptions, ut as ScopedClientFunctions, vt as DevframeRpcSharedStates, xt as DevframeDiagnosticsHost, yt as DevframeHost, z as DevframeRpcOptions, zt as EventEmitter } from "../context-c0OlSc12.mjs"; | ||
| import { a as RemoteAssetsStore, i as RemoteAssetsProviderCustom, n as RemoteAssetsErrorMessage, o as StaticAssetsSource, r as RemoteAssetsProvider, t as RemoteAssets } from "../remote-assets-B13_vRkJ.mjs"; | ||
| import { i as DevframeDefineDiagnosticsOptions } from "../nostics-SPGDgHEP.mjs"; | ||
| export { AgentHandle, AgentManifest, AgentResource, AgentResourceContent, AgentResourceInput, AgentTool, AgentToolInput, AgentToolProvider, AgentToolProviderHandle, ConnectionMeta, ConnectionMetaSse, ConnectionMetaWebsocket, DevframeAgentHost, DevframeAgentHostEvents, DevframeCapabilities, DevframeCliOptions, DevframeConnectionConfigsRegistry, type DevframeDefineDiagnosticsOptions, DevframeDefinition, DevframeDeploymentKind, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeDockDefaults, DevframeDuplicationStrategy, DevframeHost, DevframeNodeContext, DevframeNodeRpcSession, type DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, type DevframeRpcConnection, type DevframeRpcConnectionRequest, DevframeRpcOptions, DevframeRpcServerFunctions, DevframeRpcSharedStates, type DevframeRpcTransportKind, DevframeScopedNodeContext, DevframeScopedNodeRpc, DevframeScopedStreamingHost, DevframeServiceDefinition, DevframeServiceDescriptor, DevframeServiceId, DevframeServiceInput, DevframeServiceMeta, DevframeServiceOf, DevframeServiceScopeOf, DevframeServicesHost, DevframeServicesRegistry, DevframeServicesScopeRegistry, DevframeServicesState, DevframeSettings, DevframeSettingsRegistry, DevframeSettingsStore, DevframeSetupInfo, DevframeSnapshotRpcEntry, DevframeSnapshotRpcInputs, DevframeSseOptions, DevframeStorageScope, DevframeViewHost, DevframeWsOptions, EventEmitter, EventUnsubscribe, EventsMap, McpRouteOptions, RemoteAssets, RemoteAssetsErrorMessage, RemoteAssetsProvider, RemoteAssetsProviderCustom, RemoteAssetsStore, RpcBroadcastOptions, type RpcFunctionAgentOptions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost, ScopedBroadcastOptions, ScopedClientFunctions, ScopedRpcFn, ScopedServerFunctions, ScopedSharedStates, SettingsForNamespace, StaticAssetsSource }; |
@@ -1,2 +0,2 @@ | ||
| import { Vt as EventsMap, zt as EventEmitter } from "../context-_i51nYOs.mjs"; | ||
| import { Vt as EventsMap, zt as EventEmitter } from "../context-c0OlSc12.mjs"; | ||
| //#region src/utils/events.d.ts | ||
@@ -3,0 +3,0 @@ /** |
@@ -1,2 +0,2 @@ | ||
| import { a as RemoteAssetsStore, o as StaticAssetsSource } from "../remote-assets-Bg4gCUZ_.mjs"; | ||
| import { a as RemoteAssetsStore, o as StaticAssetsSource } from "../remote-assets-B13_vRkJ.mjs"; | ||
| //#region src/utils/remote-assets.d.ts | ||
@@ -3,0 +3,0 @@ /** |
@@ -1,2 +0,2 @@ | ||
| import { a as RemoteAssetsStore } from "../remote-assets-Bg4gCUZ_.mjs"; | ||
| import { a as RemoteAssetsStore } from "../remote-assets-B13_vRkJ.mjs"; | ||
| import { IncomingMessage, ServerResponse } from "node:http"; | ||
@@ -3,0 +3,0 @@ import { EventHandler, H3 } from "h3"; |
@@ -1,2 +0,2 @@ | ||
| import { t as DEVFRAME_EVENTS } from "../events-DKfSmoTj.mjs"; | ||
| import { t as DEVFRAME_EVENTS } from "../events-DzolaBEs.mjs"; | ||
| import { extname, join, normalize, resolve, sep } from "pathe"; | ||
@@ -3,0 +3,0 @@ import { createReadStream } from "node:fs"; |
@@ -1,2 +0,2 @@ | ||
| import { A as SharedStateEvents, D as ImmutableObject, E as ImmutableMap, M as SharedStatePatch, N as createSharedState, O as ImmutableSet, T as ImmutableArray, j as SharedStateOptions, k as SharedState, w as Immutable } from "../context-_i51nYOs.mjs"; | ||
| import { A as SharedStateEvents, D as ImmutableObject, E as ImmutableMap, M as SharedStatePatch, N as createSharedState, O as ImmutableSet, T as ImmutableArray, j as SharedStateOptions, k as SharedState, w as Immutable } from "../context-c0OlSc12.mjs"; | ||
| export { Immutable, ImmutableArray, ImmutableMap, ImmutableObject, ImmutableSet, SharedState, SharedStateEvents, SharedStateOptions, SharedStatePatch, createSharedState }; |
@@ -1,2 +0,2 @@ | ||
| import { C as createStreamSink, S as createStreamReader, _ as CreateStreamSinkOptions, b as StreamSink, g as CreateStreamReaderOptions, h as BufferedChunk, v as StreamErrorPayload, x as StreamSinkEvents, y as StreamReader } from "../context-_i51nYOs.mjs"; | ||
| import { C as createStreamSink, S as createStreamReader, _ as CreateStreamSinkOptions, b as StreamSink, g as CreateStreamReaderOptions, h as BufferedChunk, v as StreamErrorPayload, x as StreamSinkEvents, y as StreamReader } from "../context-c0OlSc12.mjs"; | ||
| export { BufferedChunk, CreateStreamReaderOptions, CreateStreamSinkOptions, StreamErrorPayload, StreamReader, StreamSink, StreamSinkEvents, createStreamReader, createStreamSink }; |
+1
-1
| { | ||
| "name": "devframe", | ||
| "type": "module", | ||
| "version": "0.9.4", | ||
| "version": "0.9.5", | ||
| "description": "Framework for building one portable devtool integration that runs in any viewer.", | ||
@@ -6,0 +6,0 @@ "author": "Anthony Fu <anthonyfu117@hotmail.com>", |
@@ -80,3 +80,3 @@ --- | ||
| icon: 'ph:magnifying-glass-duotone', | ||
| cli: { distDir: './client/dist' }, | ||
| clientAssets: './client/dist', // built SPA served as the UI | ||
| setup(ctx) { | ||
@@ -544,3 +544,3 @@ const my = ctx.scope('my-inspector') // preferred — auto-namespaces ids | ||
| |------------|--------| | ||
| | *(default)* | Dev server (port 9999 or `--port`) — WebSocket RPC, `cli.distDir` served at the base | | ||
| | *(default)* | Dev server (port 9999 or `--port`) — WebSocket RPC, `clientAssets` served at the base | | ||
| | `build` | Static snapshot → `./dist-static/` (`--out-dir`) | | ||
@@ -547,0 +547,0 @@ | `mcp` | stdio MCP server | |
@@ -9,3 +9,3 @@ // Compose many devframes into one devtools host behind a single standard | ||
| // like an `initDevframe` instance. `hub.nodeMiddleware` is the Connect-style | ||
| // form for Vite/Rsbuild. See docs/adapters/initiate.md for the mount snippets | ||
| // form for Vite/Rsbuild. See docs/content/2.adapters/1.initiate.md for the mount snippets | ||
| // and the WebSocket-binding precedence (`ws.port` / `server` / `ws.sidecar` / | ||
@@ -12,0 +12,0 @@ // host-attached `hub.attach(server)`). |
| import { F as DevframeDefinition, G as McpRouteOptions, I as DevframeDeploymentKind, t as ConnectionMeta } from "./context-_i51nYOs.mjs"; | ||
| //#region src/adapters/_shared.d.ts | ||
| /** | ||
| * 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. | ||
| */ | ||
| declare function resolveBasePath(def: DevframeDefinition, kind: DevframeDeploymentKind): string; | ||
| declare function normalizeBasePath(base: string): string; | ||
| interface ResolveDevServerPortOptions { | ||
| /** Bind host (passed to `get-port-please` for in-use detection). */ | ||
| host?: string; | ||
| /** Override the preferred port. Default: `def.cli?.port ?? 9999`. */ | ||
| defaultPort?: number; | ||
| } | ||
| /** | ||
| * 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. | ||
| */ | ||
| declare function resolveDevServerPort(def: DevframeDefinition, options?: ResolveDevServerPortOptions): Promise<number>; | ||
| /** | ||
| * 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). | ||
| */ | ||
| declare function resolveMcpConnectionMeta(def: DevframeDefinition, mcp: boolean | McpRouteOptions | undefined, port?: number): ConnectionMeta['mcp']; | ||
| //#endregion | ||
| export { resolveMcpConnectionMeta as a, resolveDevServerPort as i, normalizeBasePath as n, resolveBasePath as r, ResolveDevServerPortOptions as t }; |
Sorry, the diff of this file is too big to display
| import { n as randomToken } from "./crypto-token-XCqTSMg9.mjs"; | ||
| import { t as createStorage } from "./storage-BXNVhKyR.mjs"; | ||
| import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-3Q6aDKWk.mjs"; | ||
| import { join } from "pathe"; | ||
| //#region src/node/hub-internals/context.ts | ||
| const internalContextMap = /* @__PURE__ */ new WeakMap(); | ||
| function getInternalContext(context) { | ||
| if (!internalContextMap.has(context)) { | ||
| const storage = createStorage({ | ||
| filepath: join(context.host.getStorageDir("global"), "auth.json"), | ||
| initialValue: { trusted: {} } | ||
| }); | ||
| const remoteTokens = /* @__PURE__ */ new Map(); | ||
| const wsEndpointListeners = /* @__PURE__ */ new Set(); | ||
| function revokeRemoteToken(token) { | ||
| if (!remoteTokens.delete(token)) return; | ||
| revokeActiveConnectionsForToken(context, token); | ||
| } | ||
| const internalContext = { | ||
| storage: { auth: storage }, | ||
| revokeAuthToken: (token) => revokeAuthToken(context, storage, token), | ||
| setWsEndpoint(endpoint) { | ||
| internalContext.wsEndpoint = endpoint; | ||
| for (const listener of wsEndpointListeners) listener(); | ||
| }, | ||
| onWsEndpointChange(cb) { | ||
| wsEndpointListeners.add(cb); | ||
| return () => wsEndpointListeners.delete(cb); | ||
| }, | ||
| remoteTokens, | ||
| allocateRemoteToken(dockId, origin, originLock) { | ||
| const token = randomToken(); | ||
| remoteTokens.set(token, { | ||
| dockId, | ||
| origin, | ||
| originLock | ||
| }); | ||
| return token; | ||
| }, | ||
| revokeRemoteToken, | ||
| revokeRemoteTokensForDock(dockId) { | ||
| const tokensToRevoke = []; | ||
| for (const [token, record] of remoteTokens) if (record.dockId === dockId) tokensToRevoke.push(token); | ||
| for (const token of tokensToRevoke) revokeRemoteToken(token); | ||
| }, | ||
| isRemoteTokenTrusted(token, requestOrigin) { | ||
| const record = remoteTokens.get(token); | ||
| if (!record) return false; | ||
| if (!record.originLock) return true; | ||
| return !!requestOrigin && record.origin === requestOrigin; | ||
| } | ||
| }; | ||
| internalContextMap.set(context, internalContext); | ||
| } | ||
| return internalContextMap.get(context); | ||
| } | ||
| //#endregion | ||
| export { internalContextMap as n, getInternalContext as t }; |
| import { k as SharedState, o as DevframeNodeContext } from "./context-_i51nYOs.mjs"; | ||
| //#region src/node/hub-internals/context.d.ts | ||
| interface InternalAnonymousAuthStorage { | ||
| trusted: Record<string, { | ||
| authToken: string; | ||
| ua: string; | ||
| origin: string; | ||
| timestamp: number; | ||
| } | undefined>; | ||
| } | ||
| interface RemoteTokenRecord { | ||
| dockId: string; | ||
| /** Dock URL origin — matched against WS handshake `Origin` header when `originLock` is on. */ | ||
| origin: string; | ||
| originLock: boolean; | ||
| } | ||
| interface DevframeInternalContext { | ||
| storage: { | ||
| auth: SharedState<InternalAnonymousAuthStorage>; | ||
| }; | ||
| /** | ||
| * Revoke an auth token: remove from storage and notify all connected clients | ||
| * using this token that they are no longer trusted. | ||
| */ | ||
| revokeAuthToken: (token: string) => Promise<void>; | ||
| /** | ||
| * Session-only tokens issued to remote-UI iframe docks. Not persisted — | ||
| * regenerated on every dev-server restart. | ||
| */ | ||
| remoteTokens: Map<string, RemoteTokenRecord>; | ||
| allocateRemoteToken: (dockId: string, origin: string, originLock: boolean) => string; | ||
| revokeRemoteToken: (token: string) => void; | ||
| revokeRemoteTokensForDock: (dockId: string) => void; | ||
| /** | ||
| * Returns true if `token` is a valid remote token and, when `originLock` is | ||
| * on, `requestOrigin` matches the recorded dock origin. | ||
| */ | ||
| isRemoteTokenTrusted: (token: string, requestOrigin?: string) => boolean; | ||
| /** | ||
| * Populated by `createWsServer` once the WS port is bound. Consumed by the | ||
| * docks host when enriching remote iframe URLs with a connection descriptor. | ||
| */ | ||
| wsEndpoint?: { | ||
| /** Full `ws://` or `wss://` URL with host and port. */ | ||
| url: string; | ||
| }; | ||
| /** | ||
| * Set {@link DevframeInternalContext.wsEndpoint} and notify subscribers — | ||
| * the WS-binding tiers (side-car, shared-server, and the `unbound` tier's | ||
| * `attach()`) call this once the socket is bound (or `undefined` once torn | ||
| * down) instead of assigning the field directly, so anything that already | ||
| * projected the endpoint (a hub's remote-dock URLs, registered before an | ||
| * async bind resolves) gets a chance to re-project it. | ||
| */ | ||
| setWsEndpoint: (endpoint: { | ||
| url: string; | ||
| } | undefined) => void; | ||
| /** | ||
| * Subscribe to every {@link DevframeInternalContext.setWsEndpoint} call. | ||
| * Returns an unsubscribe function. The hub context uses this to refresh | ||
| * the `devframe:docks` shared state so a remote dock registered before the | ||
| * WS port resolves still ends up with a live connection URL. | ||
| */ | ||
| onWsEndpointChange: (cb: () => void) => () => void; | ||
| } | ||
| declare const internalContextMap: WeakMap<DevframeNodeContext, DevframeInternalContext>; | ||
| declare function getInternalContext(context: DevframeNodeContext): DevframeInternalContext; | ||
| //#endregion | ||
| export { internalContextMap as a, getInternalContext as i, InternalAnonymousAuthStorage as n, RemoteTokenRecord as r, DevframeInternalContext as t }; |
| import { i as defineDiagnostics } from "./nostics-CzECRXpE.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-CD8nlgll.mjs"; | ||
| import { RpcFunctionsCollectorBase } from "./rpc/index.mjs"; | ||
| import { defineRpcFunction } from "./index.mjs"; | ||
| import { t as DEVFRAME_EVENTS } from "./events-DKfSmoTj.mjs"; | ||
| import { DEVFRAME_SERVICES_STATE_KEY } from "./constants.mjs"; | ||
| import { t as diagnostics$1 } from "./diagnostics-BXWW3VK-.mjs"; | ||
| import { r as createEventEmitter, t as DevframeAgentHost } from "./host-agent-B84av916.mjs"; | ||
| import { n as createDebug, t as resolveStaticAssetsSource } from "./remote-assets-DezQmPQU.mjs"; | ||
| import { t as createStorage } from "./storage-BXNVhKyR.mjs"; | ||
| import { createRequire } from "node:module"; | ||
| import { createSharedState } from "devframe/utils/shared-state"; | ||
| import process from "node:process"; | ||
| import { pathToFileURL } from "node:url"; | ||
| import { isAbsolute, join } from "pathe"; | ||
| import { existsSync } from "node:fs"; | ||
| //#region src/node/host-diagnostics.ts | ||
| var DevframeDiagnosticsHost = class { | ||
| context; | ||
| _registry = {}; | ||
| logger = new Proxy({}, { get: (_, code) => this._registry[code] }); | ||
| defineDiagnostics = defineDiagnostics; | ||
| constructor(context, initialDefinitions = []) { | ||
| this.context = context; | ||
| for (const d of initialDefinitions) this.register(d); | ||
| } | ||
| register(diagnostics) { | ||
| Object.assign(this._registry, diagnostics); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/rpc-shared-state.ts | ||
| const debug$2 = createDebug("devframe:rpc:state:changed"); | ||
| const debugSubscribe = createDebug("devframe:rpc:state:subscribe"); | ||
| function createRpcSharedStateServerHost(rpc) { | ||
| const sharedState = /* @__PURE__ */ new Map(); | ||
| const stateDisposers = /* @__PURE__ */ new Map(); | ||
| const keyAddedListeners = /* @__PURE__ */ new Set(); | ||
| function registerSharedState(key, state) { | ||
| const offs = []; | ||
| offs.push(state.on("updated", (fullState, patches, syncId) => { | ||
| if (patches) { | ||
| debug$2("patch", { | ||
| key, | ||
| syncId | ||
| }); | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.clientStatePatch, | ||
| args: [ | ||
| key, | ||
| patches, | ||
| syncId | ||
| ], | ||
| filter: (client) => client.$meta.subscribedStates.has(key) | ||
| }); | ||
| } else { | ||
| debug$2("updated", { | ||
| key, | ||
| syncId | ||
| }); | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.clientStateUpdated, | ||
| args: [ | ||
| key, | ||
| fullState, | ||
| syncId | ||
| ], | ||
| filter: (client) => client.$meta.subscribedStates.has(key) | ||
| }); | ||
| } | ||
| })); | ||
| return () => { | ||
| for (const off of offs) off(); | ||
| }; | ||
| } | ||
| const host = { | ||
| get: async (key, options) => { | ||
| if (sharedState.has(key)) return sharedState.get(key); | ||
| if (options?.initialValue === void 0 && options?.sharedState === void 0) throw diagnostics$1.DF0013({ key }); | ||
| debug$2("new-state", key); | ||
| const state = options.sharedState ?? createSharedState({ | ||
| initialValue: options.initialValue, | ||
| enablePatches: false | ||
| }); | ||
| stateDisposers.set(key, registerSharedState(key, state)); | ||
| sharedState.set(key, state); | ||
| for (const fn of keyAddedListeners) fn(key); | ||
| return state; | ||
| }, | ||
| keys() { | ||
| return Array.from(sharedState.keys()); | ||
| }, | ||
| onKeyAdded(fn) { | ||
| keyAddedListeners.add(fn); | ||
| return () => { | ||
| keyAddedListeners.delete(fn); | ||
| }; | ||
| }, | ||
| delete(key) { | ||
| const dispose = stateDisposers.get(key); | ||
| if (!dispose) return false; | ||
| dispose(); | ||
| stateDisposers.delete(key); | ||
| sharedState.delete(key); | ||
| return true; | ||
| } | ||
| }; | ||
| rpc.register({ | ||
| name: "devframe:rpc:server-state:subscribe", | ||
| type: "event", | ||
| handler(key) { | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (!session) return; | ||
| debugSubscribe("subscribe", { | ||
| key, | ||
| session: session.meta.id | ||
| }); | ||
| session.meta.subscribedStates.add(key); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:rpc:server-state:get", | ||
| type: "query", | ||
| handler: async (key) => { | ||
| if (!sharedState.has(key)) return void 0; | ||
| return (await host.get(key)).value(); | ||
| }, | ||
| dump: () => ({ inputs: host.keys().map((key) => [key]) }) | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:rpc:server-state:set", | ||
| type: "query", | ||
| handler: async (key, value, syncId) => { | ||
| (await host.get(key, { initialValue: value })).mutate(() => value, syncId); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:rpc:server-state:patch", | ||
| type: "query", | ||
| handler: async (key, patches, syncId) => { | ||
| if (!sharedState.has(key)) return; | ||
| (await host.get(key)).patch(patches, syncId); | ||
| } | ||
| }); | ||
| return host; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/nanoid.ts | ||
| const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; | ||
| function nanoid(size = 21) { | ||
| let id = ""; | ||
| let i = size; | ||
| while (i--) id += urlAlphabet[Math.random() * 64 | 0]; | ||
| return id; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/streaming-channel.ts | ||
| const DEFAULT_HIGH_WATER_MARK = 256; | ||
| var StreamClosedError = class extends Error { | ||
| name = "StreamClosedError"; | ||
| }; | ||
| /** | ||
| * Build a server-side stream sink. RPC-agnostic — the RPC host wires | ||
| * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay | ||
| * for late or reconnecting subscribers. | ||
| */ | ||
| function createStreamSink(options = {}) { | ||
| const id = options.id ?? nanoid(); | ||
| const replayWindow = Math.max(0, options.replayWindow ?? 0); | ||
| const events = createEventEmitter(); | ||
| const controller = new AbortController(); | ||
| const buffer = []; | ||
| let closed = false; | ||
| let lastSeq = 0; | ||
| function write(chunk) { | ||
| if (closed) throw new StreamClosedError(`Cannot write to a closed stream "${id}"`); | ||
| lastSeq += 1; | ||
| if (replayWindow > 0) { | ||
| buffer.push({ | ||
| seq: lastSeq, | ||
| chunk | ||
| }); | ||
| if (buffer.length > replayWindow) buffer.splice(0, buffer.length - replayWindow); | ||
| } | ||
| events.emit("chunk", lastSeq, chunk); | ||
| } | ||
| function error(reason) { | ||
| if (closed) return; | ||
| closed = true; | ||
| const payload = toErrorPayload(reason); | ||
| controller.abort(reason); | ||
| events.emit("end", payload); | ||
| } | ||
| function close() { | ||
| if (closed) return; | ||
| closed = true; | ||
| if (!controller.signal.aborted) controller.abort("stream closed"); | ||
| events.emit("end", void 0); | ||
| } | ||
| function abort(reason) { | ||
| if (closed) return; | ||
| if (!controller.signal.aborted) controller.abort(reason ?? "aborted"); | ||
| } | ||
| const writable = new WritableStream({ | ||
| write(chunk) { | ||
| write(chunk); | ||
| }, | ||
| close() { | ||
| close(); | ||
| }, | ||
| abort(reason) { | ||
| error(reason); | ||
| } | ||
| }); | ||
| return { | ||
| id, | ||
| signal: controller.signal, | ||
| get closed() { | ||
| return closed; | ||
| }, | ||
| get lastSeq() { | ||
| return lastSeq; | ||
| }, | ||
| write, | ||
| error, | ||
| close, | ||
| abort, | ||
| writable, | ||
| events, | ||
| buffer | ||
| }; | ||
| } | ||
| /** | ||
| * Build a client-side stream reader. RPC-agnostic — the RPC host calls | ||
| * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the | ||
| * terminal frame. Consumers iterate with `for await` or pipe `readable`. | ||
| */ | ||
| function createStreamReader(options = {}) { | ||
| const id = options.id ?? nanoid(); | ||
| const highWaterMark = Math.max(1, options.highWaterMark ?? DEFAULT_HIGH_WATER_MARK); | ||
| const queue = []; | ||
| let lastSeenSeq = 0; | ||
| let done = false; | ||
| let cancelled = false; | ||
| let endError; | ||
| let pending; | ||
| let pullController; | ||
| let readableInstance; | ||
| function drainNext() { | ||
| if (!pending) return; | ||
| if (queue.length > 0) { | ||
| const value = queue.shift(); | ||
| const r = pending; | ||
| pending = void 0; | ||
| r.resolve({ | ||
| value, | ||
| done: false | ||
| }); | ||
| return; | ||
| } | ||
| if (done) { | ||
| const r = pending; | ||
| pending = void 0; | ||
| if (endError) { | ||
| const err = new Error(endError.message); | ||
| err.name = endError.name; | ||
| r.reject(err); | ||
| } else r.resolve({ | ||
| value: void 0, | ||
| done: true | ||
| }); | ||
| } | ||
| } | ||
| function feedReadable() { | ||
| if (!pullController) return; | ||
| while (queue.length > 0) { | ||
| const v = queue.shift(); | ||
| try { | ||
| pullController.enqueue(v); | ||
| } catch { | ||
| break; | ||
| } | ||
| } | ||
| if (done && pullController) { | ||
| try { | ||
| if (endError) { | ||
| const err = new Error(endError.message); | ||
| err.name = endError.name; | ||
| pullController.error(err); | ||
| } else pullController.close(); | ||
| } catch {} | ||
| pullController = void 0; | ||
| } | ||
| } | ||
| function push(seq, chunk) { | ||
| if (done || cancelled) return; | ||
| if (seq <= lastSeenSeq) return; | ||
| lastSeenSeq = seq; | ||
| queue.push(chunk); | ||
| if (queue.length > highWaterMark) { | ||
| const overflow = queue.length - highWaterMark; | ||
| queue.splice(0, overflow); | ||
| options.onOverflow?.(overflow); | ||
| } | ||
| drainNext(); | ||
| if (readableInstance) feedReadable(); | ||
| } | ||
| function end(error) { | ||
| if (done) return; | ||
| done = true; | ||
| endError = error; | ||
| drainNext(); | ||
| if (readableInstance) feedReadable(); | ||
| } | ||
| function cancel() { | ||
| if (cancelled || done) return; | ||
| cancelled = true; | ||
| options.onCancel?.(); | ||
| end(void 0); | ||
| } | ||
| function getReadable() { | ||
| if (readableInstance) return readableInstance; | ||
| readableInstance = new ReadableStream({ | ||
| start(controller) { | ||
| pullController = controller; | ||
| feedReadable(); | ||
| }, | ||
| cancel() { | ||
| cancel(); | ||
| } | ||
| }); | ||
| return readableInstance; | ||
| } | ||
| return { | ||
| id, | ||
| get cancelled() { | ||
| return cancelled; | ||
| }, | ||
| get done() { | ||
| return done; | ||
| }, | ||
| get lastSeenSeq() { | ||
| return lastSeenSeq; | ||
| }, | ||
| get readable() { | ||
| return getReadable(); | ||
| }, | ||
| cancel, | ||
| _push: push, | ||
| _end: end, | ||
| [Symbol.asyncIterator]() { | ||
| return { | ||
| next() { | ||
| if (queue.length > 0) return Promise.resolve({ | ||
| value: queue.shift(), | ||
| done: false | ||
| }); | ||
| if (done) { | ||
| if (endError) { | ||
| const err = new Error(endError.message); | ||
| err.name = endError.name; | ||
| return Promise.reject(err); | ||
| } | ||
| return Promise.resolve({ | ||
| value: void 0, | ||
| done: true | ||
| }); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| pending = { | ||
| resolve, | ||
| reject | ||
| }; | ||
| }); | ||
| }, | ||
| return() { | ||
| cancel(); | ||
| return Promise.resolve({ | ||
| value: void 0, | ||
| done: true | ||
| }); | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| function toErrorPayload(reason) { | ||
| if (reason instanceof Error) return { | ||
| name: reason.name || "Error", | ||
| message: reason.message | ||
| }; | ||
| if (typeof reason === "string") return { | ||
| name: "Error", | ||
| message: reason | ||
| }; | ||
| try { | ||
| return { | ||
| name: "Error", | ||
| message: JSON.stringify(reason) | ||
| }; | ||
| } catch { | ||
| return { | ||
| name: "Error", | ||
| message: String(reason) | ||
| }; | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/node/rpc-streaming.ts | ||
| const debug$1 = createDebug("devframe:rpc:streaming"); | ||
| const STREAM_KEY_SEPARATOR = ""; | ||
| function streamKey(channel, id) { | ||
| return `${channel}${STREAM_KEY_SEPARATOR}${id}`; | ||
| } | ||
| /** | ||
| * Build the server-side streaming host. Mirrors the layout of | ||
| * `createRpcSharedStateServerHost` — registers a fixed set of internal | ||
| * RPC methods (`subscribe` / `unsubscribe` / `cancel`) once, then per-channel | ||
| * state lives in a `Map<channelName, ChannelState>`. | ||
| */ | ||
| function createRpcStreamingServerHost(rpc) { | ||
| const channels = /* @__PURE__ */ new Map(); | ||
| function findStream(channelName, id) { | ||
| return channels.get(channelName)?.streams.get(id); | ||
| } | ||
| function freeStreamNow(state, id) { | ||
| const record = state.streams.get(id); | ||
| if (!record) return; | ||
| if (record.retentionTimer) { | ||
| clearTimeout(record.retentionTimer); | ||
| record.retentionTimer = void 0; | ||
| } | ||
| for (const off of record.unbinders) off(); | ||
| state.streams.delete(id); | ||
| debug$1("freed", state.name, id); | ||
| } | ||
| function maybeFreeStream(state, id) { | ||
| const record = state.streams.get(id); | ||
| if (!record) return; | ||
| if (!record.sink.closed || record.subscribers.size > 0) return; | ||
| const retention = state.options.closedStreamRetention; | ||
| if (retention <= 0) { | ||
| freeStreamNow(state, id); | ||
| return; | ||
| } | ||
| if (record.retentionTimer) return; | ||
| record.retentionTimer = setTimeout(freeStreamNow, retention, state, id); | ||
| } | ||
| function cancelRetention(record) { | ||
| if (record.retentionTimer) { | ||
| clearTimeout(record.retentionTimer); | ||
| record.retentionTimer = void 0; | ||
| } | ||
| } | ||
| rpc.register({ | ||
| name: "devframe:streaming:subscribe", | ||
| type: "event", | ||
| handler(channelName, id, opts) { | ||
| const state = channels.get(channelName); | ||
| if (!state) { | ||
| diagnostics$1.DF0030({ | ||
| channel: channelName, | ||
| id | ||
| }, { method: "error" }); | ||
| return; | ||
| } | ||
| const record = state.streams.get(id); | ||
| if (!record) { | ||
| diagnostics$1.DF0030({ | ||
| channel: channelName, | ||
| id | ||
| }, { method: "error" }); | ||
| return; | ||
| } | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (!session) return; | ||
| const key = streamKey(channelName, id); | ||
| session.meta.subscribedStreams ??= /* @__PURE__ */ new Set(); | ||
| session.meta.subscribedStreams.add(key); | ||
| record.subscribers.add(session.meta); | ||
| cancelRetention(record); | ||
| const afterSeq = opts?.afterSeq ?? 0; | ||
| for (const buffered of record.sink.buffer) if (buffered.seq > afterSeq) rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingChunk, | ||
| args: [ | ||
| channelName, | ||
| id, | ||
| buffered.seq, | ||
| buffered.chunk | ||
| ], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => client.$meta === session.meta | ||
| }); | ||
| if (record.sink.closed) rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingEnd, | ||
| args: [ | ||
| channelName, | ||
| id, | ||
| void 0 | ||
| ], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => client.$meta === session.meta | ||
| }); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:streaming:unsubscribe", | ||
| type: "event", | ||
| handler(channelName, id) { | ||
| const state = channels.get(channelName); | ||
| const record = state?.streams.get(id); | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (!session) return; | ||
| session.meta.subscribedStreams?.delete(streamKey(channelName, id)); | ||
| if (state && record) { | ||
| record.subscribers.delete(session.meta); | ||
| maybeFreeStream(state, id); | ||
| } | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:streaming:cancel", | ||
| type: "event", | ||
| handler(channelName, id) { | ||
| const record = findStream(channelName, id); | ||
| if (!record) return; | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (!session) return; | ||
| record.subscribers.delete(session.meta); | ||
| session.meta.subscribedStreams?.delete(streamKey(channelName, id)); | ||
| if (record.subscribers.size === 0) record.sink.abort("cancelled by client"); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:streaming:upload-chunk", | ||
| type: "event", | ||
| handler(channelName, id, seq, chunk) { | ||
| const record = channels.get(channelName)?.inbound.get(id); | ||
| if (!record) { | ||
| diagnostics$1.DF0030({ | ||
| channel: channelName, | ||
| id | ||
| }, { method: "error" }); | ||
| return; | ||
| } | ||
| if (!record.uploaderMeta) { | ||
| const session = rpc.getCurrentRpcSession(); | ||
| if (session) { | ||
| record.uploaderMeta = session.meta; | ||
| session.meta.uploadingStreams ??= /* @__PURE__ */ new Set(); | ||
| session.meta.uploadingStreams.add(streamKey(channelName, id)); | ||
| } | ||
| } | ||
| record.reader._push(seq, chunk); | ||
| } | ||
| }); | ||
| rpc.register({ | ||
| name: "devframe:streaming:upload-end", | ||
| type: "event", | ||
| handler(channelName, id, error) { | ||
| const state = channels.get(channelName); | ||
| const record = state?.inbound.get(id); | ||
| if (!record) return; | ||
| record.reader._end(error); | ||
| if (record.uploaderMeta) record.uploaderMeta.uploadingStreams?.delete(streamKey(channelName, id)); | ||
| state?.inbound.delete(id); | ||
| } | ||
| }); | ||
| function createChannel(name, opts = {}) { | ||
| if (channels.has(name)) throw diagnostics$1.DF0032({ channel: name }); | ||
| const replayWindow = opts.replayWindow ?? 0; | ||
| const state = { | ||
| name, | ||
| options: { | ||
| replayWindow, | ||
| closedStreamRetention: opts.closedStreamRetention ?? (replayWindow > 0 ? 3e4 : 0) | ||
| }, | ||
| streams: /* @__PURE__ */ new Map(), | ||
| inbound: /* @__PURE__ */ new Map() | ||
| }; | ||
| channels.set(name, state); | ||
| function start(startOpts = {}) { | ||
| const sink = createStreamSink({ | ||
| id: startOpts.id, | ||
| replayWindow: state.options.replayWindow | ||
| }); | ||
| const record = { | ||
| sink, | ||
| subscribers: /* @__PURE__ */ new Set(), | ||
| unbinders: [] | ||
| }; | ||
| state.streams.set(sink.id, record); | ||
| record.unbinders.push(sink.events.on("chunk", (seq, chunk) => { | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingChunk, | ||
| args: [ | ||
| name, | ||
| sink.id, | ||
| seq, | ||
| chunk | ||
| ], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => record.subscribers.has(client.$meta) | ||
| }); | ||
| })); | ||
| record.unbinders.push(sink.events.on("end", (error) => { | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingEnd, | ||
| args: [ | ||
| name, | ||
| sink.id, | ||
| error | ||
| ], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => record.subscribers.has(client.$meta) | ||
| }); | ||
| maybeFreeStream(state, sink.id); | ||
| })); | ||
| return sink; | ||
| } | ||
| async function pipeFrom(readable, startOpts = {}) { | ||
| const sink = start(startOpts); | ||
| readable.pipeTo(sink.writable, { signal: sink.signal }).catch(() => {}); | ||
| return sink; | ||
| } | ||
| function get(id) { | ||
| return state.streams.get(id)?.sink; | ||
| } | ||
| function ids() { | ||
| return Array.from(state.streams.keys()); | ||
| } | ||
| function openInbound(inboundOpts = {}) { | ||
| let inboundRecord; | ||
| const reader = createStreamReader({ | ||
| id: inboundOpts.id, | ||
| onCancel() { | ||
| const targetMeta = inboundRecord?.uploaderMeta; | ||
| if (!targetMeta) return; | ||
| rpc.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.streamingUploadCancel, | ||
| args: [name, reader.id], | ||
| event: true, | ||
| optional: true, | ||
| filter: (client) => client.$meta === targetMeta | ||
| }); | ||
| } | ||
| }); | ||
| inboundRecord = { reader }; | ||
| state.inbound.set(reader.id, inboundRecord); | ||
| debug$1("opened-inbound", name, reader.id); | ||
| return reader; | ||
| } | ||
| return { | ||
| name, | ||
| start, | ||
| pipeFrom, | ||
| get, | ||
| ids, | ||
| openInbound | ||
| }; | ||
| } | ||
| function parseKey(key) { | ||
| const sepIdx = key.indexOf(STREAM_KEY_SEPARATOR); | ||
| if (sepIdx < 0) return void 0; | ||
| return { | ||
| channelName: key.slice(0, sepIdx), | ||
| id: key.slice(sepIdx + 1) | ||
| }; | ||
| } | ||
| return { | ||
| create: createChannel, | ||
| _onSessionDisconnected(meta) { | ||
| if (meta.subscribedStreams) { | ||
| for (const key of meta.subscribedStreams) { | ||
| const parsed = parseKey(key); | ||
| if (!parsed) continue; | ||
| const state = channels.get(parsed.channelName); | ||
| const record = state?.streams.get(parsed.id); | ||
| if (!state || !record) continue; | ||
| record.subscribers.delete(meta); | ||
| if (record.subscribers.size === 0 && !record.sink.closed) record.sink.abort("all subscribers disconnected"); | ||
| maybeFreeStream(state, parsed.id); | ||
| } | ||
| meta.subscribedStreams.clear(); | ||
| } | ||
| if (meta.uploadingStreams) { | ||
| for (const key of meta.uploadingStreams) { | ||
| const parsed = parseKey(key); | ||
| if (!parsed) continue; | ||
| const state = channels.get(parsed.channelName); | ||
| const record = state?.inbound.get(parsed.id); | ||
| if (!state || !record) continue; | ||
| record.reader._end({ | ||
| name: "UploadDisconnected", | ||
| message: "Uploader disconnected before completing the stream" | ||
| }); | ||
| state.inbound.delete(parsed.id); | ||
| } | ||
| meta.uploadingStreams.clear(); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/node/host-functions.ts | ||
| const debugBroadcast = createDebug("devframe:rpc:broadcast"); | ||
| /** | ||
| * Concrete implementation backing `ctx.rpc`. Internal: consumers should | ||
| * depend on the structural {@link RpcFunctionsHost} type, never this class. | ||
| * Its `@internal` members (`_rpcGroup`, `_asyncStorage`, | ||
| * `_emitSessionDisconnected`) are wired by `createContextRpcServer` and must not | ||
| * widen the public surface. | ||
| * | ||
| * @internal | ||
| */ | ||
| var RpcFunctionsHostImpl = class extends RpcFunctionsCollectorBase { | ||
| /** | ||
| * @internal | ||
| */ | ||
| _rpcGroup = void 0; | ||
| _asyncStorage = void 0; | ||
| constructor(context) { | ||
| super(context); | ||
| this.sharedState = createRpcSharedStateServerHost(this); | ||
| this.streaming = createRpcStreamingServerHost(this); | ||
| } | ||
| sharedState; | ||
| streaming; | ||
| /** | ||
| * Adapters call this from their WS `onDisconnected` hook so downstream | ||
| * hosts (streaming, …) can free per-session state. Public-ish because | ||
| * tests / custom adapters may want to mirror it. | ||
| * | ||
| * @internal | ||
| */ | ||
| _emitSessionDisconnected(meta) { | ||
| this.streaming._onSessionDisconnected(meta); | ||
| } | ||
| async invokeLocal(method, ...args) { | ||
| if (!this.definitions.has(method)) throw diagnostics$1.DF0006({ name: String(method) }); | ||
| const handler = await this.getHandler(method); | ||
| return await Promise.resolve(handler(...args)); | ||
| } | ||
| async broadcast(options) { | ||
| if (!this._rpcGroup) return; | ||
| debugBroadcast(JSON.stringify(options.method)); | ||
| await Promise.allSettled(this._rpcGroup.clients.map((client) => { | ||
| if (options.filter?.(client) === false) return void 0; | ||
| return client.$callRaw({ | ||
| optional: true, | ||
| event: true, | ||
| ...options | ||
| }); | ||
| })); | ||
| } | ||
| getCurrentRpcSession() { | ||
| if (!this._asyncStorage) throw diagnostics$1.DF0007(); | ||
| return this._asyncStorage.getStore(); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/services-install.ts | ||
| /** | ||
| * Turn a `resolveFrom` value (a file path, a file URL like `import.meta.url`, | ||
| * or a directory) into something `createRequire` accepts — a directory gets a | ||
| * synthetic filename appended so resolution starts inside it. | ||
| */ | ||
| function toRequireBase(resolveFrom) { | ||
| if (resolveFrom.startsWith("file://")) return resolveFrom; | ||
| if ((resolveFrom.split(/[/\\]/).pop() ?? "").includes(".")) return resolveFrom; | ||
| return join(resolveFrom, "_devframe_resolve.js"); | ||
| } | ||
| /** | ||
| * Normalize an `install()` `resolveFrom` into a resolution base. Paths and | ||
| * file URLs pass through (the common case: the declaring plugin's | ||
| * `importMetaUrl`, so a service it declares resolves against the plugin's own | ||
| * dependencies); a bare npm package name resolves to that package's location | ||
| * from `cwd`. An unresolvable package name reads as no base (the caller's | ||
| * workspace fallbacks apply). | ||
| */ | ||
| function expandResolveFrom(resolveFrom, cwd) { | ||
| if (resolveFrom.startsWith("file://") || resolveFrom.startsWith(".") || isAbsolute(resolveFrom)) return resolveFrom; | ||
| const require = createRequire(join(cwd, "_devframe_resolve.js")); | ||
| try { | ||
| return require.resolve(`${resolveFrom}/package.json`); | ||
| } catch {} | ||
| try { | ||
| return require.resolve(resolveFrom); | ||
| } catch {} | ||
| } | ||
| /** | ||
| * Import a service package's module, trying each `resolveFrom` candidate in | ||
| * order (so a plugin-declared service resolves against the plugin's own | ||
| * dependency tree first, then the workspace fallback). Throws the last | ||
| * resolution error when no candidate succeeds. | ||
| */ | ||
| async function importServicePackage(pkg, resolveFroms) { | ||
| const candidates = [...new Set(resolveFroms.filter((x) => typeof x === "string" && x.length > 0))]; | ||
| let lastError = /* @__PURE__ */ new Error(`no resolution base available for "${pkg}"`); | ||
| for (const from of candidates) { | ||
| let resolved; | ||
| try { | ||
| resolved = createRequire(toRequireBase(from)).resolve(pkg); | ||
| } catch (error) { | ||
| lastError = error; | ||
| continue; | ||
| } | ||
| return await import( | ||
| /* webpackIgnore: true */ | ||
| /* @vite-ignore */ | ||
| /* turbopackIgnore: true */ | ||
| pathToFileURL(resolved).href | ||
| ); | ||
| } | ||
| throw lastError; | ||
| } | ||
| function parseVersion(input) { | ||
| const [core, ...prerelease] = input.trim().replace(/^v/, "").split("-"); | ||
| if (!core) return void 0; | ||
| const parts = core.split(".").map((part) => Number.parseInt(part, 10)); | ||
| if (parts.length === 0 || parts.some((part) => Number.isNaN(part) || part < 0)) return void 0; | ||
| while (parts.length < 3) parts.push(0); | ||
| return { | ||
| parts, | ||
| ...prerelease.length ? { prerelease: prerelease.join("-") } : {} | ||
| }; | ||
| } | ||
| function compareVersions(a, b) { | ||
| for (let i = 0; i < 3; i++) { | ||
| const diff = (a.parts[i] ?? 0) - (b.parts[i] ?? 0); | ||
| if (diff !== 0) return diff; | ||
| } | ||
| if (a.prerelease && !b.prerelease) return -1; | ||
| if (!a.prerelease && b.prerelease) return 1; | ||
| if (a.prerelease && b.prerelease) return a.prerelease < b.prerelease ? -1 : a.prerelease > b.prerelease ? 1 : 0; | ||
| return 0; | ||
| } | ||
| function satisfiesComparator(version, comparator) { | ||
| const raw = comparator.trim(); | ||
| if (!raw || raw === "*" || raw === "x") return true; | ||
| const operatorMatch = raw.match(/^([\^~]|>=|<=|[><=])?(.+)$/); | ||
| if (!operatorMatch) return false; | ||
| const operator = operatorMatch[1]; | ||
| const rest = operatorMatch[2].trim(); | ||
| const segments = rest.replace(/\.[x*]/gi, "").split(".").filter(Boolean); | ||
| const base = parseVersion(rest.replace(/[x*]/gi, "0")); | ||
| if (!base) return false; | ||
| switch (operator) { | ||
| case ">": return compareVersions(version, base) > 0; | ||
| case ">=": return compareVersions(version, base) >= 0; | ||
| case "<": return compareVersions(version, base) < 0; | ||
| case "<=": return compareVersions(version, base) <= 0; | ||
| case "^": { | ||
| if (compareVersions(version, base) < 0) return false; | ||
| const fixedIndex = base.parts.findIndex((part) => part !== 0); | ||
| const lockUpTo = fixedIndex === -1 ? base.parts.length - 1 : fixedIndex; | ||
| for (let i = 0; i <= lockUpTo; i++) if ((version.parts[i] ?? 0) !== (base.parts[i] ?? 0)) return false; | ||
| return true; | ||
| } | ||
| case "~": { | ||
| if (compareVersions(version, base) < 0) return false; | ||
| const lockUpTo = segments.length >= 2 ? 1 : 0; | ||
| for (let i = 0; i <= lockUpTo; i++) if ((version.parts[i] ?? 0) !== (base.parts[i] ?? 0)) return false; | ||
| return true; | ||
| } | ||
| default: | ||
| for (let i = 0; i < Math.max(segments.length, 3); i++) if (i < segments.length && (version.parts[i] ?? 0) !== (base.parts[i] ?? 0)) return false; | ||
| return segments.length >= 3 ? compareVersions(version, base) === 0 : true; | ||
| } | ||
| } | ||
| /** | ||
| * Pragmatic semver range check for service version declarations — supports | ||
| * the common forms (`1.2.3`, `^1.2.3`, `~1.2`, `>=1 <3`, `1.x`, `*`, and | ||
| * `||`-joined alternatives) without pulling in a semver dependency. An | ||
| * unparseable version or range reads as **not satisfied**. | ||
| */ | ||
| function satisfiesVersionRange(version, range) { | ||
| const parsed = parseVersion(version); | ||
| if (!parsed) return false; | ||
| const alternatives = range.split("||").map((alt) => alt.trim()).filter(Boolean); | ||
| if (alternatives.length === 0) return true; | ||
| return alternatives.some((alternative) => alternative.split(/\s+/).every((comparator) => satisfiesComparator(parsed, comparator))); | ||
| } | ||
| function isPlainObject(value) { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value); | ||
| } | ||
| /** Deep-merge two values with the service option-set rules (see below). */ | ||
| function deepMergeTwo(a, b) { | ||
| if (Array.isArray(a) && Array.isArray(b)) return [.../* @__PURE__ */ new Set([...a, ...b])]; | ||
| if (isPlainObject(a) && isPlainObject(b)) { | ||
| const out = { ...a }; | ||
| for (const key of Object.keys(b)) out[key] = key in a ? deepMergeTwo(a[key], b[key]) : b[key]; | ||
| return out; | ||
| } | ||
| return b; | ||
| } | ||
| /** | ||
| * Default option-set merge when a service declares no `mergeOptions`: | ||
| * deep-merge in declaration order — objects recurse, arrays union-dedupe, | ||
| * scalars take the later value. Covers the built-in services (`roots` / | ||
| * `langs` union, `themes` per-key last-wins) without a custom hook. | ||
| */ | ||
| function deepMergeOptionSets(sets) { | ||
| return sets.reduce((merged, set) => deepMergeTwo(merged, set)); | ||
| } | ||
| //#endregion | ||
| //#region src/node/host-services.ts | ||
| const debug = createDebug("devframe:services"); | ||
| function isServiceDefinition(input) { | ||
| return typeof input.setup === "function"; | ||
| } | ||
| function validateServiceInput(input) { | ||
| if (!input || typeof input.package !== "string" || input.package.length === 0) throw diagnostics$1.DF0070({ | ||
| package: String(input?.package ?? input), | ||
| reason: "the input has no `package` name" | ||
| }); | ||
| if (isServiceDefinition(input)) validateServiceDefinition(input); | ||
| } | ||
| function validateServiceDefinition(def) { | ||
| if (typeof def.version !== "string" || def.version.length === 0) throw diagnostics$1.DF0070({ | ||
| package: def.package, | ||
| reason: "the definition has no `version`" | ||
| }); | ||
| if (typeof def.scope !== "string" || def.scope.length === 0) throw diagnostics$1.DF0070({ | ||
| package: def.package, | ||
| reason: "the definition has no RPC `scope` namespace" | ||
| }); | ||
| } | ||
| /** | ||
| * Cross-plugin service registry (see `types/services.ts` for the contract). | ||
| * Values are held per context instance; `whenAvailable` subscriptions make | ||
| * the mechanism robust against setup ordering between provider and consumer. | ||
| * | ||
| * On top of the in-process `provide`/`get` tier, this host implements the | ||
| * **wire-service** lifecycle: `install()` queues definitions/descriptors, | ||
| * `ready()` fires the collect-then-setup barrier — importing descriptor | ||
| * packages, merging option sets per service, constructing each service once, | ||
| * providing its node API under the package name, and advertising it to | ||
| * clients through the `devframe:services` shared state. | ||
| */ | ||
| var DevframeServicesHostImpl = class { | ||
| context; | ||
| services = /* @__PURE__ */ new Map(); | ||
| listeners = /* @__PURE__ */ new Map(); | ||
| pending = /* @__PURE__ */ new Map(); | ||
| installed = /* @__PURE__ */ new Map(); | ||
| readyPromise; | ||
| constructor(context) { | ||
| this.context = context; | ||
| } | ||
| provide(id, service) { | ||
| const key = id; | ||
| if (this.services.has(key)) throw diagnostics$1.DF0037({ id: key }); | ||
| this.services.set(key, service); | ||
| for (const listener of this.listeners.get(key) ?? []) listener(service); | ||
| return () => { | ||
| if (this.services.get(key) === service) this.services.delete(key); | ||
| }; | ||
| } | ||
| get(id) { | ||
| return this.services.get(id); | ||
| } | ||
| has(id) { | ||
| return this.services.has(id); | ||
| } | ||
| whenAvailable(id, callback) { | ||
| const key = id; | ||
| if (this.services.has(key)) callback(this.services.get(key)); | ||
| let set = this.listeners.get(key); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| this.listeners.set(key, set); | ||
| } | ||
| const listener = callback; | ||
| set.add(listener); | ||
| return () => { | ||
| set.delete(listener); | ||
| }; | ||
| } | ||
| keys() { | ||
| return Array.from(this.services.keys()); | ||
| } | ||
| install(input, options) { | ||
| validateServiceInput(input); | ||
| const promise = new Promise((resolve, reject) => { | ||
| const entry = { | ||
| input, | ||
| resolveFrom: options?.resolveFrom, | ||
| resolve, | ||
| reject | ||
| }; | ||
| if (this.readyPromise) this.flushPackage(input.package, [entry]).catch(() => {}); | ||
| else { | ||
| let entries = this.pending.get(input.package); | ||
| if (!entries) { | ||
| entries = []; | ||
| this.pending.set(input.package, entries); | ||
| } | ||
| entries.push(entry); | ||
| } | ||
| }); | ||
| promise.catch(() => {}); | ||
| return promise; | ||
| } | ||
| ready() { | ||
| if (this.readyPromise) return this.readyPromise; | ||
| this.readyPromise = this.flushAll(); | ||
| return this.readyPromise; | ||
| } | ||
| async flushAll() { | ||
| if (this.context) await this.advertisementState(); | ||
| const groups = Array.from(this.pending.entries()); | ||
| this.pending.clear(); | ||
| for (const [pkg, entries] of groups) await this.flushPackage(pkg, entries); | ||
| } | ||
| async flushPackage(pkg, entries) { | ||
| try { | ||
| const api = await this.installPackage(pkg, entries); | ||
| for (const entry of entries) entry.resolve(api); | ||
| return api; | ||
| } catch (error) { | ||
| for (const entry of entries) entry.reject(error); | ||
| throw error; | ||
| } | ||
| } | ||
| async installPackage(pkg, entries) { | ||
| if (this.installed.has(pkg)) { | ||
| diagnostics$1.DF0066({ package: pkg }); | ||
| return this.installed.get(pkg); | ||
| } | ||
| let def = entries.filter((entry) => isServiceDefinition(entry.input))[0]?.input; | ||
| if (!def) { | ||
| const required = entries.map((entry) => entry.input).some((descriptor) => descriptor.required === true); | ||
| const cwd = this.context?.cwd ?? process.cwd(); | ||
| const resolveFroms = [ | ||
| ...entries.map((entry) => entry.resolveFrom && expandResolveFrom(entry.resolveFrom, cwd)), | ||
| this.context?.workspaceRoot, | ||
| cwd | ||
| ]; | ||
| let mod; | ||
| try { | ||
| mod = await importServicePackage(pkg, resolveFroms); | ||
| } catch (error) { | ||
| const reason = error instanceof Error ? error.message : String(error); | ||
| if (required) throw diagnostics$1.DF0067({ | ||
| package: pkg, | ||
| reason, | ||
| cause: error | ||
| }); | ||
| debug("optional service %s not importable, skipping: %s", pkg, reason); | ||
| return; | ||
| } | ||
| const factory = mod.default; | ||
| if (typeof factory !== "function") throw diagnostics$1.DF0070({ | ||
| package: pkg, | ||
| reason: "its default export is not a factory function" | ||
| }); | ||
| def = await factory(); | ||
| if (!def || typeof def.setup !== "function") throw diagnostics$1.DF0070({ | ||
| package: pkg, | ||
| reason: "its factory did not return a definition with a `setup` function" | ||
| }); | ||
| if (typeof def.package !== "string" || def.package.length === 0) def = { | ||
| ...def, | ||
| package: pkg | ||
| }; | ||
| validateServiceDefinition(def); | ||
| } | ||
| for (const entry of entries) { | ||
| const descriptor = entry.input; | ||
| if (isServiceDefinition(entry.input) || typeof descriptor.version !== "string") continue; | ||
| if (satisfiesVersionRange(def.version, descriptor.version)) continue; | ||
| if (descriptor.required === true) throw diagnostics$1.DF0068({ | ||
| package: pkg, | ||
| required: descriptor.version, | ||
| installed: def.version | ||
| }); | ||
| diagnostics$1.DF0069({ | ||
| package: pkg, | ||
| required: descriptor.version, | ||
| installed: def.version | ||
| }); | ||
| } | ||
| const sets = entries.map((entry) => entry.input.options).filter((options) => options !== void 0); | ||
| const options = def.mergeOptions ? def.mergeOptions(sets) : sets.length > 0 ? deepMergeOptionSets(sets) : void 0; | ||
| if (!this.context) throw diagnostics$1.DF0070({ | ||
| package: pkg, | ||
| reason: "this services host has no node context to install into" | ||
| }); | ||
| debug("installing service %s@%s (scope %s)", def.package, def.version, def.scope); | ||
| const scoped = this.context.scope(def.scope); | ||
| const api = await def.setup(scoped, options === void 0 ? {} : { options }); | ||
| this.installed.set(def.package, api); | ||
| this.provide(def.package, api); | ||
| await this.advertise(def); | ||
| return api; | ||
| } | ||
| advertisementState() { | ||
| return this.context.rpc.sharedState.get(DEVFRAME_SERVICES_STATE_KEY, { initialValue: {} }); | ||
| } | ||
| async advertise(def) { | ||
| const state = await this.advertisementState(); | ||
| const { package: pkg, version, scope, meta } = def; | ||
| state.mutate((value) => { | ||
| value[pkg] = { | ||
| package: pkg, | ||
| version, | ||
| scope, | ||
| ...meta ? { meta } : {} | ||
| }; | ||
| }); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/host-views.ts | ||
| var DevframeViewHost = class { | ||
| context; | ||
| importMetaUrl; | ||
| /** | ||
| * @internal | ||
| */ | ||
| buildStaticDirs = []; | ||
| constructor(context, importMetaUrl) { | ||
| this.context = context; | ||
| this.importMetaUrl = importMetaUrl; | ||
| } | ||
| hostStatic(baseUrl, source, defaultResolveFrom = this.importMetaUrl) { | ||
| const resolved = resolveStaticAssetsSource(source, this.context.host.getStorageDir("project"), defaultResolveFrom); | ||
| if (typeof resolved === "string" && !existsSync(resolved)) throw diagnostics$1.DF0008({ distDir: resolved }); | ||
| this.buildStaticDirs.push({ | ||
| baseUrl, | ||
| source | ||
| }); | ||
| this.context.host.mountStatic(baseUrl, resolved); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/rpc/agent-invoke-tool.ts | ||
| const agentInvokeTool = defineRpcFunction({ | ||
| name: "devframe:agent:invoke-tool", | ||
| type: "action", | ||
| setup: (ctx) => { | ||
| return { async handler(id, args) { | ||
| return await ctx.agent.invoke(id, args); | ||
| } }; | ||
| } | ||
| }); | ||
| //#endregion | ||
| //#region src/node/rpc/agent-list-resources.ts | ||
| const agentListResources = defineRpcFunction({ | ||
| name: "devframe:agent:list-resources", | ||
| type: "query", | ||
| jsonSerializable: true, | ||
| setup: (ctx) => { | ||
| return { async handler() { | ||
| return ctx.agent.list().resources; | ||
| } }; | ||
| } | ||
| }); | ||
| //#endregion | ||
| //#region src/node/rpc/index.ts | ||
| /** | ||
| * Built-in agent introspection RPC functions. Registered automatically | ||
| * by `createHostContext`. Not themselves agent-exposed (no `agent` | ||
| * field) — they power the MCP adapter and any future agent CLI. | ||
| */ | ||
| const BUILTIN_AGENT_RPC = [ | ||
| defineRpcFunction({ | ||
| name: "devframe:agent:list-tools", | ||
| type: "query", | ||
| jsonSerializable: true, | ||
| setup: (ctx) => { | ||
| return { async handler() { | ||
| return ctx.agent.list().tools; | ||
| } }; | ||
| } | ||
| }), | ||
| agentInvokeTool, | ||
| agentListResources, | ||
| defineRpcFunction({ | ||
| name: "devframe:agent:read-resource", | ||
| type: "query", | ||
| jsonSerializable: true, | ||
| setup: (ctx) => { | ||
| return { async handler(id) { | ||
| return await ctx.agent.read(id); | ||
| } }; | ||
| } | ||
| }) | ||
| ]; | ||
| //#endregion | ||
| //#region src/utils/scope.ts | ||
| /** Whether a name is already namespaced (contains a `:` separator). */ | ||
| function isQualifiedName(name) { | ||
| return name.includes(":"); | ||
| } | ||
| /** | ||
| * Prefix a bare name with `<namespace>:`. Names that already contain a | ||
| * `:` are returned unchanged, so callers can reference another scope's | ||
| * ids explicitly (e.g. `ctx.rpc.call('other-plugin:fn')`). | ||
| */ | ||
| function qualifyName(namespace, name) { | ||
| return isQualifiedName(name) ? name : `${namespace}:${name}`; | ||
| } | ||
| //#endregion | ||
| //#region src/node/settings.ts | ||
| const STORAGE_SCOPE = { | ||
| global: "global", | ||
| project: "project" | ||
| }; | ||
| function createNodeSettingsStore(context, namespace, scope) { | ||
| const stateKey = `devframe:settings:${scope}:${namespace}`; | ||
| let statePromise; | ||
| function store() { | ||
| if (!statePromise) { | ||
| const dir = context.host.getStorageDir(STORAGE_SCOPE[scope]); | ||
| const filepath = join(dir, "settings", `${namespace}.json`); | ||
| statePromise = context.rpc.sharedState.get(stateKey, { sharedState: createStorage({ | ||
| filepath, | ||
| initialValue: {} | ||
| }) }); | ||
| } | ||
| return statePromise; | ||
| } | ||
| return { | ||
| async get(key) { | ||
| return (await store()).value()[key]; | ||
| }, | ||
| async set(key, value) { | ||
| (await store()).mutate((draft) => { | ||
| draft[key] = value; | ||
| }); | ||
| }, | ||
| async delete(key) { | ||
| (await store()).mutate((draft) => { | ||
| delete draft[key]; | ||
| }); | ||
| }, | ||
| async all() { | ||
| return (await store()).value(); | ||
| }, | ||
| async onChange(fn) { | ||
| return (await store()).on("updated", (full) => fn(full)); | ||
| } | ||
| }; | ||
| } | ||
| /** | ||
| * Build the node-side `settings` surface for a scope namespace. `project` | ||
| * persists under the host's `workspace` storage dir, `global` under its | ||
| * `global` dir. Each is a file-backed, client-synced key-value store. | ||
| */ | ||
| function createNodeSettings(context, namespace) { | ||
| return { | ||
| global: createNodeSettingsStore(context, namespace, "global"), | ||
| project: createNodeSettingsStore(context, namespace, "project") | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/node/scope.ts | ||
| function prefixDefinition(namespace, fn) { | ||
| if (isQualifiedName(fn.name)) throw diagnostics$1.DF0034({ | ||
| namespace, | ||
| name: fn.name | ||
| }); | ||
| return { | ||
| ...fn, | ||
| name: `${namespace}:${fn.name}` | ||
| }; | ||
| } | ||
| /** | ||
| * Build a namespace-scoped view of a {@link DevframeNodeContext}. Every | ||
| * RPC id, shared-state key, and streaming channel passed through the | ||
| * returned `rpc` surface is auto-namespaced with `<namespace>:`. | ||
| */ | ||
| function createScopedNodeContext(context, namespace) { | ||
| const base = context.rpc; | ||
| const rpc = { | ||
| namespace, | ||
| register(fn, force) { | ||
| base.register(prefixDefinition(namespace, fn), force); | ||
| }, | ||
| update(fn, force) { | ||
| base.update(prefixDefinition(namespace, fn), force); | ||
| }, | ||
| call: ((method, ...args) => base.invokeLocal(qualifyName(namespace, method), ...args)), | ||
| broadcast: ((options) => base.broadcast({ | ||
| ...options, | ||
| method: qualifyName(namespace, options.method) | ||
| })), | ||
| sharedState: ((key, options) => base.sharedState.get(qualifyName(namespace, key), options)), | ||
| streaming: { create: (name, opts) => base.streaming.create(qualifyName(namespace, name), opts) }, | ||
| getCurrentRpcSession: () => base.getCurrentRpcSession() | ||
| }; | ||
| return { | ||
| namespace, | ||
| base: context, | ||
| cwd: context.cwd, | ||
| workspaceRoot: context.workspaceRoot, | ||
| mode: context.mode, | ||
| host: context.host, | ||
| rpc, | ||
| settings: createNodeSettings(context, namespace), | ||
| views: context.views, | ||
| diagnostics: context.diagnostics, | ||
| agent: context.agent, | ||
| scope: context.scope | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/node/context.ts | ||
| /** | ||
| * Framework- and build-tool-agnostic core of the Devframe node context. | ||
| * Wires the RPC host, view (HTTP file-serving) host, diagnostics, and | ||
| * agent subsystems. Host adapters can wrap this to augment `ctx` with | ||
| * extra surfaces — for example, `@vitejs/devtools-kit`'s | ||
| * `createKitContext` attaches `docks`, `terminals`, `messages`, and | ||
| * `commands` when mounted into Vite DevTools. | ||
| */ | ||
| async function createHostContext(options) { | ||
| const { cwd, workspaceRoot = cwd, mode, host, importMetaUrl, builtinRpcDeclarations = [] } = options; | ||
| const context = { | ||
| cwd, | ||
| workspaceRoot, | ||
| mode, | ||
| host, | ||
| rpc: void 0, | ||
| views: void 0, | ||
| diagnostics: void 0, | ||
| agent: void 0, | ||
| services: void 0, | ||
| staticConfig: {}, | ||
| scope: void 0 | ||
| }; | ||
| const rpcHost = new RpcFunctionsHostImpl(context); | ||
| const viewsHost = new DevframeViewHost(context, importMetaUrl); | ||
| const diagnosticsHost = new DevframeDiagnosticsHost(context, [diagnostics$1, diagnostics]); | ||
| context.rpc = rpcHost; | ||
| context.views = viewsHost; | ||
| context.diagnostics = diagnosticsHost; | ||
| context.services = new DevframeServicesHostImpl(context); | ||
| context.agent = new DevframeAgentHost(context); | ||
| const scopedCache = /* @__PURE__ */ new Map(); | ||
| context.scope = ((namespace) => { | ||
| if (!namespace) return context; | ||
| let scoped = scopedCache.get(namespace); | ||
| if (!scoped) { | ||
| scoped = createScopedNodeContext(context, namespace); | ||
| scopedCache.set(namespace, scoped); | ||
| } | ||
| return scoped; | ||
| }); | ||
| for (const fn of BUILTIN_AGENT_RPC) rpcHost.register(fn); | ||
| for (const fn of builtinRpcDeclarations) rpcHost.register(fn); | ||
| return context; | ||
| } | ||
| //#endregion | ||
| export { createHostContext as t }; |
| import { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs"; | ||
| import { t as createHostContext } from "./context-CQW0lSw8.mjs"; | ||
| import { t as resolveStaticAssetsSource } from "./remote-assets-DezQmPQU.mjs"; | ||
| import { i as resolveMcpConnectionMeta, n as resolveBasePath, r as resolveDevServerPort, t as normalizeBasePath } from "./_shared-BM3PdYli.mjs"; | ||
| import { t as createH3DevframeHost } from "./host-h3-fRbF9yor.mjs"; | ||
| import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, t as createInstanceShell } from "./instance-shell-B-uevnK-.mjs"; | ||
| import { open } from "./utils/open.mjs"; | ||
| import { mountStaticHandler } from "./utils/serve-static.mjs"; | ||
| import { createServer } from "node:http"; | ||
| import process from "node:process"; | ||
| import { resolve } from "pathe"; | ||
| 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 host = options.host ?? def.cli?.host ?? "localhost"; | ||
| const shell = createInstanceShell({ | ||
| base, | ||
| app, | ||
| host, | ||
| 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, | ||
| importMetaUrl: def.importMetaUrl | ||
| }); | ||
| const setupInfo = { flags: options.flags ?? {} }; | ||
| for (const input of def.services ?? []) context.services.install(input, { resolveFrom: def.importMetaUrl }); | ||
| await context.services.ready(); | ||
| 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-BKiqHcXX.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"), def.importMetaUrl); | ||
| 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 }; |
| //#region src/events.ts | ||
| /** | ||
| * Centralized registry of the core devframe event names — the node-side host | ||
| * bus events, the client RPC connection events, and the server→client | ||
| * broadcast notifications — so these names live in one place instead of | ||
| * scattered string literals. | ||
| * | ||
| * **Keep this in sync with [`docs/guide/events.md`](../../../docs/guide/events.md)** | ||
| * (the "Core devframe events" section): every name here appears in that page's | ||
| * tables, and every name there resolves to an entry here. Add, rename, or | ||
| * remove a name in both places in the same change, and reference | ||
| * `DEVFRAME_EVENTS.*` from call sites instead of re-typing a literal. | ||
| * | ||
| * This map covers **notifications** (events, broadcasts). The request/response | ||
| * RPC endpoints of the shared-state, streaming, and auth-handshake protocols | ||
| * (`devframe:rpc:server-state:*`, `devframe:streaming:subscribe`, | ||
| * `anonymous:devframe:auth`, …) are defined at their handlers and typed in | ||
| * `types/rpc-augments.ts`; they aren't events and stay out of this map. | ||
| * | ||
| * The `EventEmitter` maps (`RpcClientEvents`, `DevframeAgentHostEvents`) and the | ||
| * `DevframeRpcClientFunctions` augmentation declare these names as type-level | ||
| * keys (a literal is unavoidable in a type position); those declarations mirror | ||
| * this map and move with it. | ||
| */ | ||
| const DEVFRAME_EVENTS = { | ||
| /** | ||
| * Node-side host `EventEmitter` events. The agent host (`ctx.agent.events`) | ||
| * emits these as its tool/resource surface changes; protocol adapters (e.g. | ||
| * MCP) subscribe to re-publish their manifest. | ||
| */ | ||
| bus: { | ||
| agentManifestChanged: "agent:manifest:changed", | ||
| agentToolRegistered: "agent:tool:registered", | ||
| agentToolUnregistered: "agent:tool:unregistered", | ||
| agentResourceRegistered: "agent:resource:registered", | ||
| agentResourceUnregistered: "agent:resource:unregistered" | ||
| }, | ||
| /** | ||
| * Client-side RPC connection `EventEmitter` events (`rpc.events`) a UI | ||
| * subscribes to for connection lifecycle and error surfacing. | ||
| */ | ||
| client: { | ||
| isTrustedUpdated: "rpc:is-trusted:updated", | ||
| error: "rpc:error", | ||
| connectionStatus: "connection:status", | ||
| connectionError: "connection:error" | ||
| }, | ||
| /** | ||
| * Broadcast notifications the server pushes to clients (server → client), | ||
| * `devframe:` prefix. The paired request methods (subscribe/get/set/…) are | ||
| * RPC endpoints, not events, and are omitted deliberately. | ||
| */ | ||
| broadcast: { | ||
| authRevoked: "devframe:auth:revoked", | ||
| clientStateUpdated: "devframe:rpc:client-state:updated", | ||
| clientStatePatch: "devframe:rpc:client-state:patch", | ||
| streamingChunk: "devframe:streaming:chunk", | ||
| streamingEnd: "devframe:streaming:end", | ||
| streamingUploadCancel: "devframe:streaming:upload-cancel" | ||
| }, | ||
| /** `postMessage` channels the runtime posts across window boundaries. */ | ||
| postMessage: { remoteAssetsError: "devframe:remote-assets-error" } | ||
| }; | ||
| //#endregion | ||
| export { DEVFRAME_EVENTS as t }; |
| import { t as DEVFRAME_EVENTS } from "./events-DKfSmoTj.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs"; | ||
| //#region src/utils/events.ts | ||
| /** | ||
| * Create event emitter. | ||
| */ | ||
| function createEventEmitter() { | ||
| const _listeners = {}; | ||
| function emit(event, ...args) { | ||
| const callbacks = _listeners[event] || []; | ||
| for (let i = 0, length = callbacks.length; i < length; i++) { | ||
| const callback = callbacks[i]; | ||
| if (callback) callback(...args); | ||
| } | ||
| } | ||
| function emitOnce(event, ...args) { | ||
| emit(event, ...args); | ||
| delete _listeners[event]; | ||
| } | ||
| function on(event, cb) { | ||
| (_listeners[event] ||= []).push(cb); | ||
| return () => { | ||
| _listeners[event] = _listeners[event]?.filter((i) => cb !== i); | ||
| }; | ||
| } | ||
| function once(event, cb) { | ||
| const unsubscribe = on(event, ((...args) => { | ||
| unsubscribe(); | ||
| return cb(...args); | ||
| })); | ||
| return unsubscribe; | ||
| } | ||
| return { | ||
| _listeners, | ||
| emit, | ||
| emitOnce, | ||
| on, | ||
| once | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/node/agent-args.ts | ||
| /** | ||
| * Map the args payload an agent surface receives (MCP sends an object | ||
| * keyed `arg0`/`arg1`/…, matching the schema the adapter advertises) onto | ||
| * a handler's positional parameters. Shared by the agent host's RPC | ||
| * bridge and the hub's command-derived tools so the coercion cannot | ||
| * drift between them. | ||
| * | ||
| * - an array passes through as-is | ||
| * - `null`/`undefined` become a zero-argument call | ||
| * - with declared schemas, each schema reads its own `argN` key, in order | ||
| * - without schemas, `arg0`/`arg1`/… keys are collected when present | ||
| * - an empty object becomes a zero-argument call | ||
| * - anything else follows the {@link AgentArgsFallback} | ||
| */ | ||
| function coerceAgentPositionalArgs(args, schemas, fallback = "wrap") { | ||
| if (Array.isArray(args)) return args; | ||
| if (args === void 0 || args === null) return []; | ||
| if (typeof args === "object") { | ||
| const obj = args; | ||
| if (schemas && schemas.length) return schemas.map((_, i) => obj[`arg${i}`]); | ||
| if ("arg0" in obj) { | ||
| const out = []; | ||
| let i = 0; | ||
| while (`arg${i}` in obj) { | ||
| out.push(obj[`arg${i}`]); | ||
| i++; | ||
| } | ||
| return out; | ||
| } | ||
| if (Object.keys(obj).length === 0) return []; | ||
| } | ||
| return fallback === "drop" ? [] : [args]; | ||
| } | ||
| //#endregion | ||
| //#region src/node/host-agent.ts | ||
| /** | ||
| * Framework-neutral host aggregating the agent-exposed surface of a | ||
| * devframe. Auto-discovers RPC functions with an `agent` field from | ||
| * `ctx.rpc.definitions`, and accepts plugin-registered tools / | ||
| * resources via `registerTool` / `registerResource`. | ||
| */ | ||
| var DevframeAgentHost = class { | ||
| context; | ||
| events = createEventEmitter(); | ||
| tools = /* @__PURE__ */ new Map(); | ||
| resources = /* @__PURE__ */ new Map(); | ||
| providers = /* @__PURE__ */ new Set(); | ||
| _rpcUnsubscribe; | ||
| constructor(context) { | ||
| this.context = context; | ||
| this._rpcUnsubscribe = context.rpc.onChanged(() => { | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| }); | ||
| } | ||
| registerTool(input) { | ||
| this._validateToolId(input.id); | ||
| const tool = this._projectTool(input); | ||
| this.tools.set(tool.id, { | ||
| tool, | ||
| handler: input.handler | ||
| }); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentToolRegistered, tool); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| return { unregister: () => this.unregisterTool(tool.id) }; | ||
| } | ||
| unregisterTool(id) { | ||
| const existed = this.tools.delete(id); | ||
| if (existed) { | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentToolUnregistered, id); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| } | ||
| return existed; | ||
| } | ||
| registerToolProvider(provider) { | ||
| this.providers.add(provider); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| const notifyChanged = () => { | ||
| if (this.providers.has(provider)) this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| }; | ||
| return { | ||
| notifyChanged, | ||
| unregister: () => { | ||
| if (this.providers.delete(provider)) this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| } | ||
| }; | ||
| } | ||
| registerResource(input) { | ||
| if (this.resources.has(input.id)) throw diagnostics.DF0016({ id: input.id }); | ||
| const resource = { | ||
| id: input.id, | ||
| name: input.name, | ||
| description: input.description, | ||
| mimeType: input.mimeType ?? "application/json", | ||
| uri: input.uri ?? `devframe://resource/${encodeURIComponent(input.id)}` | ||
| }; | ||
| this.resources.set(resource.id, { | ||
| resource, | ||
| read: input.read | ||
| }); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentResourceRegistered, resource); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| return { unregister: () => this.unregisterResource(resource.id) }; | ||
| } | ||
| unregisterResource(id) { | ||
| const existed = this.resources.delete(id); | ||
| if (existed) { | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentResourceUnregistered, id); | ||
| this.events.emit(DEVFRAME_EVENTS.bus.agentManifestChanged); | ||
| } | ||
| return existed; | ||
| } | ||
| list() { | ||
| const rpcTools = this._collectRpcTools(); | ||
| const plainTools = Array.from(this.tools.values()).map((t) => t.tool); | ||
| const resources = Array.from(this.resources.values()).map((r) => r.resource); | ||
| const seen = new Set([...rpcTools, ...plainTools].map((t) => t.id)); | ||
| const providerTools = []; | ||
| for (const { tool } of this._collectProviderTools()) { | ||
| if (seen.has(tool.id)) continue; | ||
| seen.add(tool.id); | ||
| providerTools.push(tool); | ||
| } | ||
| return { | ||
| tools: [ | ||
| ...rpcTools, | ||
| ...plainTools, | ||
| ...providerTools | ||
| ], | ||
| resources | ||
| }; | ||
| } | ||
| getTool(id) { | ||
| const plain = this.tools.get(id); | ||
| if (plain) return plain.tool; | ||
| const rpc = this._collectRpcTools().find((t) => t.id === id); | ||
| if (rpc) return rpc; | ||
| return this._collectProviderTools().find((t) => t.tool.id === id)?.tool; | ||
| } | ||
| getResource(id) { | ||
| return this.resources.get(id)?.resource; | ||
| } | ||
| async invoke(id, args) { | ||
| const plain = this.tools.get(id); | ||
| if (plain?.handler) return await plain.handler(args); | ||
| const rpcDef = this._findRpcDefinition(id); | ||
| if (rpcDef) { | ||
| const positional = coerceAgentPositionalArgs(args, rpcDef.args, "wrap"); | ||
| return await this.context.rpc.invokeLocal(id, ...positional); | ||
| } | ||
| const provided = this._collectProviderTools().find((t) => t.tool.id === id); | ||
| if (provided) return await provided.input.handler(args); | ||
| throw new Error(`[devframe/agent] tool "${id}" not found`); | ||
| } | ||
| async read(id) { | ||
| const entry = this.resources.get(id); | ||
| if (!entry) throw new Error(`[devframe/agent] resource "${id}" not found`); | ||
| return await entry.read(); | ||
| } | ||
| /** @internal */ | ||
| _dispose() { | ||
| this._rpcUnsubscribe?.(); | ||
| this._rpcUnsubscribe = void 0; | ||
| } | ||
| _validateToolId(id) { | ||
| if (this.tools.has(id)) throw diagnostics.DF0015({ id }); | ||
| if (this.context.rpc.definitions.get(id)?.agent) throw diagnostics.DF0015({ id }); | ||
| } | ||
| _projectTool(input) { | ||
| if (!input.description || typeof input.description !== "string") throw diagnostics.DF0014({ name: input.id }); | ||
| return { | ||
| id: input.id, | ||
| kind: "tool", | ||
| title: input.title ?? input.id, | ||
| description: input.description, | ||
| safety: input.safety ?? "action", | ||
| tags: input.tags, | ||
| args: input.args, | ||
| inputSchema: input.inputSchema, | ||
| outputSchema: input.outputSchema, | ||
| examples: input.examples | ||
| }; | ||
| } | ||
| /** Query every registered provider, projecting inputs to serializable tools. */ | ||
| _collectProviderTools() { | ||
| const out = []; | ||
| for (const provider of this.providers) for (const input of provider()) out.push({ | ||
| input, | ||
| tool: this._projectTool(input) | ||
| }); | ||
| return out; | ||
| } | ||
| _collectRpcTools() { | ||
| const out = []; | ||
| for (const [name, def] of this.context.rpc.definitions) { | ||
| const agent = def.agent; | ||
| if (!agent) continue; | ||
| if (!agent.description || typeof agent.description !== "string") throw diagnostics.DF0014({ name }); | ||
| const type = def.type ?? "query"; | ||
| const safety = agent.safety ?? inferSafety(type); | ||
| out.push({ | ||
| id: name, | ||
| kind: "rpc", | ||
| title: agent.title ?? name, | ||
| description: agent.description, | ||
| safety, | ||
| tags: agent.tags, | ||
| rpcName: name, | ||
| examples: agent.examples | ||
| }); | ||
| } | ||
| return out; | ||
| } | ||
| _findRpcDefinition(id) { | ||
| const def = this.context.rpc.definitions.get(id); | ||
| if (def?.agent) return def; | ||
| } | ||
| }; | ||
| function inferSafety(type) { | ||
| if (type === "static" || type === "query") return "read"; | ||
| return "action"; | ||
| } | ||
| //#endregion | ||
| export { coerceAgentPositionalArgs as n, createEventEmitter as r, DevframeAgentHost as t }; |
| import { n as __exportAll } from "./rolldown-runtime-B4iAMlE-.mjs"; | ||
| import { t as Diagnostic } from "./nostics-CzECRXpE.mjs"; | ||
| import { t as DEVFRAME_EVENTS } from "./events-DKfSmoTj.mjs"; | ||
| import { i as isAllowedOrigin } from "./ws-server-BdSLrhxE.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs"; | ||
| import { t as createHostContext } from "./context-CQW0lSw8.mjs"; | ||
| import { t as toAgentToolName } from "./agent-tool-name-EgfoFO8C.mjs"; | ||
| import { randomUUID } from "node:crypto"; | ||
| import process from "node:process"; | ||
| import { join } from "pathe"; | ||
| 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(DEVFRAME_EVENTS.bus.agentManifestChanged, () => { | ||
| 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`); | ||
| } | ||
| }, | ||
| importMetaUrl: definition.importMetaUrl | ||
| }); | ||
| for (const input of definition.services ?? []) ctx.services.install(input, { resolveFrom: definition.importMetaUrl }); | ||
| await ctx.services.ready(); | ||
| 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 { k as SharedState, o as DevframeNodeContext, s as DevframeNodeRpcSession } from "./context-_i51nYOs.mjs"; | ||
| import { n as InternalAnonymousAuthStorage } from "./context-CO7eZHJy.mjs"; | ||
| //#region src/node/auth/revoke.d.ts | ||
| /** | ||
| * Flip `isTrusted` to false on any live WS clients connected with `token` | ||
| * and broadcast the `auth:revoked` event so they can react. | ||
| * | ||
| * Shared between persisted-auth revocation and remote-dock token revocation. | ||
| */ | ||
| declare function revokeActiveConnectionsForToken(context: DevframeNodeContext, token: string): Promise<void>; | ||
| /** | ||
| * Revoke an auth token: remove from storage and notify all connected clients | ||
| * using this token that they are no longer trusted. | ||
| */ | ||
| declare function revokeAuthToken(context: DevframeNodeContext, storage: SharedState<InternalAnonymousAuthStorage>, token: string): Promise<void>; | ||
| //#endregion | ||
| //#region src/node/auth/state.d.ts | ||
| /** | ||
| * The current one-time authentication code. Display this to the user (e.g. in | ||
| * the dev-server terminal) so they can type it into the browser to authenticate. | ||
| */ | ||
| declare function getTempAuthCode(): string; | ||
| /** | ||
| * Rotate the authentication code, resetting its expiry window and failed-attempt | ||
| * counter. Call this when a new authentication flow begins (e.g. when an | ||
| * untrusted client starts authenticating) so the displayed code is freshly | ||
| * valid for its full TTL. | ||
| */ | ||
| declare function refreshTempAuthCode(): string; | ||
| /** | ||
| * Build a "magic link" authentication URL that embeds a one-time code (OTP) in | ||
| * the URL **fragment**. Opening it authenticates the client without typing — | ||
| * print it on startup (devframe stays headless, so the host prints its own | ||
| * banner). Defaults to the current code; the link is subject to the same TTL. | ||
| * | ||
| * The code rides the fragment (`#devframe_otp=…`), not the query string, so it | ||
| * is never sent to the server, written to an access log, or leaked in a | ||
| * `Referer` header — the browser client reads it locally (see | ||
| * `consumeOtpFromUrl`). Any existing fragment parameters are preserved. | ||
| */ | ||
| declare function buildOtpAuthUrl(baseUrl: string, code?: string): string; | ||
| /** | ||
| * Re-authenticate a connection that presents a previously-issued bearer token. | ||
| * Returns `true` and marks the session trusted when the token is known. | ||
| * | ||
| * Used by the `anonymous:devframe:auth` handler so a client that already | ||
| * authenticated (token persisted in the browser) is trusted on reconnect | ||
| * without entering the code again. | ||
| */ | ||
| declare function verifyAuthToken(token: string, session: DevframeNodeRpcSession, storage: SharedState<InternalAnonymousAuthStorage>): boolean; | ||
| /** | ||
| * Exchange a one-time authentication code for a fresh, node-issued bearer token. | ||
| * | ||
| * On success this mints a high-entropy token, records it in the trusted store, | ||
| * marks the calling session trusted, rotates the code, and returns the token | ||
| * for the client to persist. Returns `null` on any failure. | ||
| * | ||
| * Because the code is short and human-typed, verification is hardened against | ||
| * brute force: it enforces a time-to-live, compares in constant time, and | ||
| * rotates the code after {@link TEMP_AUTH_MAX_ATTEMPTS} failed attempts so an | ||
| * attacker cannot keep guessing against the same code. | ||
| */ | ||
| declare function exchangeTempAuthCode(code: string, session: DevframeNodeRpcSession, info: { | ||
| ua: string; | ||
| origin: string; | ||
| }, storage: SharedState<InternalAnonymousAuthStorage>): string | null; | ||
| //#endregion | ||
| export { verifyAuthToken as a, refreshTempAuthCode as i, exchangeTempAuthCode as n, revokeActiveConnectionsForToken as o, getTempAuthCode as r, revokeAuthToken as s, buildOtpAuthUrl as t }; |
| import "./constants.mjs"; | ||
| import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs"; | ||
| import { t as getInternalContext } from "./context-BUJGvlje.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-BjlMQbFB.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-DlHd276T.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-CZXfGEkd.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 }; |
| import { d as DevframeRpcConnection, n as WsOriginRegistry, u as DevframeNodeRpcSessionMeta } from "./ws-server-DLtEoLdR.mjs"; | ||
| import { Ct as DevframeAuthHandler, U as DevframeSseOptions, W as DevframeWsOptions, _t as DevframeRpcServerFunctions, gt as DevframeRpcClientFunctions, o as DevframeNodeContext, s as DevframeNodeRpcSession, t as ConnectionMeta } from "./context-_i51nYOs.mjs"; | ||
| import "./index-CEvd-YXD.mjs"; | ||
| import { BirpcGroup } from "birpc"; | ||
| import { NodeAdapter } from "crossws/adapters/node"; | ||
| import { Buffer } from "node:buffer"; | ||
| import { IncomingMessage, Server, ServerResponse } from "node:http"; | ||
| import { Duplex } from "node:stream"; | ||
| import { H3 } from "h3"; | ||
| //#region src/node/instance-registry.d.ts | ||
| /** | ||
| * One running devframe instance, as recorded in the instance registry. | ||
| * Records are self-describing JSON — additive fields are safe. | ||
| */ | ||
| interface DevframeInstanceRecord { | ||
| /** Process id of the dev server. */ | ||
| pid: number; | ||
| /** Listening port. */ | ||
| port: number; | ||
| /** Dialable HTTP origin, e.g. `http://127.0.0.1:9876`. */ | ||
| origin: string; | ||
| /** Base path the devframe is mounted at (trailing slash). */ | ||
| basePath: string; | ||
| /** Definition id. */ | ||
| id: string; | ||
| /** Definition display name. */ | ||
| name?: string; | ||
| /** Working directory the instance was started from. */ | ||
| rootDir: string; | ||
| /** | ||
| * Absolute URL path of the MCP Streamable-HTTP endpoint on `origin`, or | ||
| * `null` when the instance runs without an MCP route. | ||
| */ | ||
| mcp: { | ||
| path: string; | ||
| } | null; | ||
| /** Epoch-ms timestamp of registration. */ | ||
| startedAt: number; | ||
| } | ||
| /** | ||
| * Handle returned by {@link registerDevframeInstance}. | ||
| */ | ||
| interface DevframeInstanceRegistration { | ||
| /** The registry file backing this registration. */ | ||
| readonly file: string; | ||
| /** Remove the record (idempotent). Call on server close. */ | ||
| unregister: () => void; | ||
| } | ||
| /** | ||
| * Record a running devframe instance in the global instance registry so | ||
| * discovery tooling (`devframe connect`, editor integrations) can find it | ||
| * without port guessing. | ||
| * | ||
| * `createDevServer` registers automatically; custom hosts that serve a | ||
| * devframe in-process (e.g. `@devframes/next`'s host inside a Next dev | ||
| * server) call this explicitly with the origin they are reachable at. | ||
| * | ||
| * The record is written atomically to `<dir>/<pid>-<port>.json` and removed | ||
| * by {@link DevframeInstanceRegistration.unregister}. Records surviving a | ||
| * crash are pruned by readers whose liveness probe fails. Registration never | ||
| * throws — a write failure degrades to a coded warning (`DF0045`), since a | ||
| * dev server must not die over discovery metadata. | ||
| */ | ||
| declare function registerDevframeInstance(record: DevframeInstanceRecord, options?: { | ||
| instancesDir?: string; | ||
| }): DevframeInstanceRegistration; | ||
| /** | ||
| * Read the registry and split records into live and dead by probing each | ||
| * one's `__connection.json`, deleting dead records (prune-on-read). Live | ||
| * records carry the dialable origin the probe confirmed (a `localhost` | ||
| * record may come back as `127.0.0.1` / `[::1]`). | ||
| * | ||
| * A liveness probe only proves *something* answers on the record's port, so | ||
| * records left behind by killed processes shadow the server currently bound | ||
| * there: per `(port, basePath)` only the newest record survives, older | ||
| * ghosts are pruned with the dead. | ||
| */ | ||
| declare function listLiveDevframeInstances(options?: { | ||
| instancesDir?: string; | ||
| timeoutMs?: number; | ||
| }): Promise<{ | ||
| live: DevframeInstanceRecord[]; | ||
| pruned: DevframeInstanceRecord[]; | ||
| }>; | ||
| //#endregion | ||
| //#region src/node/instance-shell.d.ts | ||
| /** | ||
| * The live handle for a bound HTTP + WebSocket RPC server — what the | ||
| * side-car / shared-server tiers produce and what {@link createDevServer} | ||
| * re-exposes through its own return contract. | ||
| */ | ||
| interface StartedServer { | ||
| /** Listening origin, e.g. `http://localhost:9999`. */ | ||
| origin: string; | ||
| port: number; | ||
| app: H3; | ||
| /** | ||
| * The crossws node adapter driving the RPC socket (connected peers, | ||
| * pub/sub). Absent when the WebSocket transport is disabled (`ws: false`). | ||
| */ | ||
| ws?: NodeAdapter; | ||
| rpcGroup: BirpcGroup<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>; | ||
| /** | ||
| * The {@link ConnectionMeta} descriptor for this server — the same shape a | ||
| * `__connection.json` route should serve so a devframe client's | ||
| * `resolveWsUrl` can dial back in. | ||
| */ | ||
| connectionMeta: () => ConnectionMeta; | ||
| close: () => Promise<void>; | ||
| } | ||
| /** | ||
| * How the instance's RPC socket is bound: | ||
| * | ||
| * - `sidecar` — its own HTTP+WS server on a dedicated port (`ws.port` / | ||
| * `ws.sidecar`), advertised with that port. | ||
| * - `server` — a shared upgrade route on the host's `node:http` server. | ||
| * - `external` — no local transport: `ws.url` alone names a server that owns | ||
| * both the socket and its auth. | ||
| * - `unbound` — the transport exists but nothing is bound to it yet; the host | ||
| * drives it through {@link InstanceShell.handleUpgrade} / | ||
| * {@link InstanceShell.attach}. | ||
| * - `disabled` — `ws: false`: no WebSocket at all; clients connect over the | ||
| * SSE endpoint instead (`backend: 'sse'`). | ||
| */ | ||
| type InstanceWsTier = 'sidecar' | 'server' | 'external' | 'unbound' | 'disabled'; | ||
| /** The live shell surface an `init` / `mount` callback can reach. */ | ||
| interface InstanceShellApi { | ||
| /** The normalized mount base, with leading and trailing slash. */ | ||
| base: string; | ||
| /** The h3 app every route is mounted on. */ | ||
| app: H3; | ||
| /** The public origin, once known (pinned, or derived from the first request). */ | ||
| origin: () => string | undefined; | ||
| /** The connection meta, once the transport has resolved. */ | ||
| connectionMeta: () => ConnectionMeta | undefined; | ||
| } | ||
| /** What an instance's own initialization contributes to the shell. */ | ||
| interface InstanceShellInit<TContext extends DevframeNodeContext> { | ||
| /** The context every mounted surface shares. */ | ||
| context: TContext; | ||
| /** The `mcp` entry to advertise, when an MCP route was mounted. */ | ||
| mcp?: ConnectionMeta['mcp']; | ||
| /** Torn down before the transport on `close()` (e.g. MCP sessions). */ | ||
| dispose?: () => Promise<void>; | ||
| } | ||
| interface CreateInstanceShellOptions<TContext extends DevframeNodeContext> { | ||
| /** Normalized mount base (leading and trailing slash). */ | ||
| base: string; | ||
| /** h3 app to mount on. A fresh one is created when omitted. */ | ||
| app?: H3; | ||
| /** Public origin, or a getter. Derived from the first request when omitted. */ | ||
| origin?: string | (() => string); | ||
| /** Resolved auth intent: `undefined`/`true` gates, `false` opts out, a handler installs a scheme. */ | ||
| auth?: boolean | DevframeAuthHandler; | ||
| /** Host `node:http` server to share the WS upgrade with. */ | ||
| server?: Server; | ||
| /** Explicit WebSocket control — see {@link DevframeWsOptions}. `false` disables the socket (SSE-only). */ | ||
| ws?: DevframeWsOptions | false; | ||
| /** SSE endpoint control — enabled by default; `false` disables, an object renames the route. */ | ||
| sse?: boolean | DevframeSseOptions; | ||
| /** Bind host for a side-car WebSocket server. Default: `localhost`. */ | ||
| host?: string; | ||
| /** Extra WS-upgrade origins beyond the loopback default; `false` disables the gate. */ | ||
| allowedOrigins?: readonly string[] | WsOriginRegistry | false; | ||
| /** Destroy off-route upgrades on a shared `server`. */ | ||
| destroyUnmatchedUpgrades?: boolean; | ||
| onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void; | ||
| onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void; | ||
| /** | ||
| * Advertise the WS and SSE routes as base-absolute paths (`<base>__ws` / | ||
| * `<base>__sse`) instead of the base-relative default. A hub serves one | ||
| * meta document from several bases, so its clients need the absolute form | ||
| * to resolve the same endpoints. | ||
| */ | ||
| absoluteWsPath?: boolean; | ||
| /** Pick the first port a `ws.sidecar` server tries. Default: a random free port. */ | ||
| resolveSidecarPort?: (host: string) => Promise<number>; | ||
| /** | ||
| * Publish this instance in the global registry (`~/.devframe/instances/`) | ||
| * once its public origin is known — a dynamic import so the registry code | ||
| * stays out of instances that opt out. Omit to skip registration. | ||
| */ | ||
| register?: InstanceRegisterConfig; | ||
| /** Create the context and mount everything that must precede the transport. */ | ||
| init: (api: InstanceShellApi) => Promise<InstanceShellInit<TContext>>; | ||
| /** Mount the routes that describe the resolved transport (discovery, SPA). */ | ||
| mount?: (context: TContext, meta: ConnectionMeta, api: InstanceShellApi) => void | Promise<void>; | ||
| /** Throw the instance's own diagnostic for `connectionMeta()` before readiness. */ | ||
| onMetaUnavailable: () => never; | ||
| } | ||
| /** | ||
| * The identity a shell needs to publish itself in the global instance | ||
| * registry — the parts it can't derive on its own. The shell fills in | ||
| * `pid` / `origin` / `port` / `basePath` / `mcp` / `startedAt` once the | ||
| * origin resolves, then merges {@link InstanceRegisterConfig.overrides} last. | ||
| */ | ||
| interface InstanceRegisterConfig { | ||
| /** Definition id (or a synthetic one for a hub). */ | ||
| id: string; | ||
| /** Display name. */ | ||
| name?: string; | ||
| /** Working directory the instance runs from. Default: `process.cwd()`. */ | ||
| rootDir?: string; | ||
| /** Fields overriding the shell-derived record (from the public option's object form). */ | ||
| overrides?: Partial<DevframeInstanceRecord>; | ||
| } | ||
| /** | ||
| * 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. | ||
| */ | ||
| declare function resolveInstanceRegister(option: boolean | Partial<DevframeInstanceRecord> | undefined, defaults: { | ||
| id: string; | ||
| name?: string; | ||
| rootDir?: string; | ||
| }): InstanceRegisterConfig | undefined; | ||
| /** Live internals the first-party adapters read off an instance. */ | ||
| interface InstanceShellInternals { | ||
| readonly started?: StartedServer; | ||
| readonly authHandler?: DevframeAuthHandler; | ||
| } | ||
| interface InstanceShell<TContext extends DevframeNodeContext> { | ||
| base: string; | ||
| handler: (request: Request) => Promise<Response>; | ||
| nodeMiddleware: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void; | ||
| ready: Promise<void>; | ||
| context: Promise<TContext>; | ||
| connectionMeta: () => ConnectionMeta; | ||
| /** Complete a host server's `upgrade` event on the instance's socket. */ | ||
| handleUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer) => void; | ||
| /** Route a host server's `upgrade` events to the instance's socket. */ | ||
| attach: (server: Server) => () => void; | ||
| close: () => Promise<void>; | ||
| internals: InstanceShellInternals; | ||
| } | ||
| /** Compare two URL paths ignoring a trailing slash. */ | ||
| declare function samePath(a: string, b: string): boolean; | ||
| /** | ||
| * 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 | ||
| */ | ||
| declare function createInstanceShell<TContext extends DevframeNodeContext>(options: CreateInstanceShellOptions<TContext>): InstanceShell<TContext>; | ||
| //#endregion | ||
| export { InstanceShellInit as a, StartedServer as c, samePath as d, DevframeInstanceRecord as f, registerDevframeInstance as h, InstanceShellApi as i, createInstanceShell as l, listLiveDevframeInstances as m, InstanceRegisterConfig as n, InstanceShellInternals as o, DevframeInstanceRegistration as p, InstanceShell as r, InstanceWsTier as s, CreateInstanceShellOptions as t, resolveInstanceRegister as u }; |
| //#region src/types/remote-assets.d.ts | ||
| /** | ||
| * A version-locked pointer at browser assets published as their own npm | ||
| * package (e.g. `@devframes/plugin-git-client`), served through devframe's | ||
| * caching back-proxy instead of a directory shipped inside the node package. | ||
| * | ||
| * Resolution order at serve time: | ||
| * | ||
| * 1. The package installed locally (resolved from {@link resolveFrom}) | ||
| * — the zero-network / air-gap path. Version skew warns; a major | ||
| * version mismatch throws. | ||
| * 2. The per-file cache under | ||
| * `<storageDir project>/.remote-assets/<package>@<version>/`. | ||
| * 3. The CDN {@link provider} — each requested file streams through to | ||
| * the browser while being written into the cache. | ||
| * | ||
| * Anywhere a static mount accepts a dist directory (`cli.distDir`, | ||
| * `hostStatic`, `mountStatic`) it also accepts this object — see | ||
| * {@link StaticAssetsSource}. | ||
| */ | ||
| interface RemoteAssets { | ||
| /** npm package name that ships the assets, e.g. `@devframes/plugin-git-client`. */ | ||
| package: string; | ||
| /** Exact version to serve, e.g. `1.2.3`. Typically the host package's own version. */ | ||
| version: string; | ||
| /** | ||
| * Subpath inside the package the served assets live under. | ||
| * | ||
| * @default 'dist' | ||
| */ | ||
| path?: string; | ||
| /** | ||
| * CDN that mirrors npm and serves individual package files. | ||
| * | ||
| * @default 'jsdelivr' | ||
| */ | ||
| provider?: RemoteAssetsProvider; | ||
| /** | ||
| * `import.meta.url` of the declaring module. When set, a locally | ||
| * installed copy of {@link package} is resolved from this module's own | ||
| * dependency graph first (works under pnpm's strict layout) and served | ||
| * with zero network. Omitting it skips the installed-package step — | ||
| * cache + CDN still work. | ||
| */ | ||
| resolveFrom?: string | null; | ||
| /** Custom fetch implementation (proxies, tests). Defaults to the global `fetch`. */ | ||
| fetch?: typeof globalThis.fetch; | ||
| /** | ||
| * Never touch the network: serve only from the locally installed package | ||
| * or files already in the cache. | ||
| * | ||
| * @default false | ||
| */ | ||
| offline?: boolean; | ||
| } | ||
| /** | ||
| * Built-in CDN providers (`'jsdelivr'` — default, `'unpkg'`) or a custom | ||
| * provider for corp mirrors. | ||
| */ | ||
| type RemoteAssetsProvider = 'jsdelivr' | 'unpkg' | RemoteAssetsProviderCustom; | ||
| /** A custom {@link RemoteAssets} CDN provider (e.g. an internal npm mirror). */ | ||
| interface RemoteAssetsProviderCustom { | ||
| /** | ||
| * Absolute URL serving `filePath` (package-relative, POSIX, no leading | ||
| * slash) of `pkg@version`. | ||
| */ | ||
| fileUrl: (pkg: string, version: string, filePath: string) => string; | ||
| /** | ||
| * List every file path in `pkg@version` (package-relative, no leading | ||
| * slash). Powers request-path resolution (correct 404s / SPA fallback) | ||
| * and build-time materialization. When omitted, requests are resolved by | ||
| * probing {@link fileUrl} directly and builds cannot materialize from | ||
| * this provider. | ||
| */ | ||
| listFiles?: (pkg: string, version: string, fetchImpl: typeof globalThis.fetch) => Promise<string[]>; | ||
| } | ||
| /** | ||
| * What every static-assets seam accepts: a local dist directory, or a | ||
| * {@link RemoteAssets} pointer served through the caching back-proxy. | ||
| */ | ||
| type StaticAssetsSource = string | RemoteAssets; | ||
| /** | ||
| * What the remote-assets fallback page posts to `window.parent` when a | ||
| * devframe's client assets could be served from neither a local install nor | ||
| * their provider. A viewer embedding the devframe in an iframe listens for | ||
| * `DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE` (`devframe/constants`) and can | ||
| * render the failure in its own design, with the two ways out the page also | ||
| * spells out: install `package@version` locally, or restore network access. | ||
| */ | ||
| interface RemoteAssetsErrorMessage { | ||
| type: 'devframe:remote-assets-error'; | ||
| /** npm package the assets are published as. */ | ||
| package: string; | ||
| /** Exact version the devframe asked for. */ | ||
| version: string; | ||
| /** Why the fetch failed, as reported by the provider or the network stack. */ | ||
| reason: string; | ||
| } | ||
| /** | ||
| * A resolved, servable handle over a {@link RemoteAssets} declaration — | ||
| * produced by `resolveStaticAssetsSource()` (`devframe/utils/remote-assets`) | ||
| * and consumed by the static-serving engine (`devframe/utils/serve-static`). | ||
| */ | ||
| interface RemoteAssetsStore { | ||
| /** The declaration this store serves (with defaults applied). */ | ||
| readonly assets: RemoteAssets & { | ||
| path: string; | ||
| }; | ||
| /** | ||
| * Resolve a request path (relative to the mount base, SPA fallback to | ||
| * `index.html`) and return a `Response`: streamed from the cache when | ||
| * present, otherwise through the provider while being written into the | ||
| * cache. `null` on a miss (404); throws on provider/network failure. | ||
| */ | ||
| serve: (urlPath: string) => Promise<Response | null>; | ||
| /** | ||
| * Download every listed file under `assets.path` into `targetDir` | ||
| * (paths relative to `assets.path`). Requires a provider file listing. | ||
| */ | ||
| materialize: (targetDir: string) => Promise<void>; | ||
| } | ||
| //#endregion | ||
| export { RemoteAssetsStore as a, RemoteAssetsProviderCustom as i, RemoteAssetsErrorMessage as n, StaticAssetsSource as o, RemoteAssetsProvider as r, RemoteAssets as t }; |
| import { t as DEVFRAME_EVENTS } from "./events-DKfSmoTj.mjs"; | ||
| //#region src/node/auth/revoke.ts | ||
| /** | ||
| * Flip `isTrusted` to false on any live WS clients connected with `token` | ||
| * and broadcast the `auth:revoked` event so they can react. | ||
| * | ||
| * Shared between persisted-auth revocation and remote-dock token revocation. | ||
| */ | ||
| async function revokeActiveConnectionsForToken(context, token) { | ||
| const rpcHost = context.rpc; | ||
| if (!rpcHost?._rpcGroup) return; | ||
| const affectedSessionIds = /* @__PURE__ */ new Set(); | ||
| for (const client of rpcHost._rpcGroup.clients) if (client.$meta.clientAuthToken === token) { | ||
| affectedSessionIds.add(client.$meta.id); | ||
| client.$meta.isTrusted = false; | ||
| client.$meta.clientAuthToken = void 0; | ||
| } | ||
| if (affectedSessionIds.size === 0) return; | ||
| await rpcHost.broadcast({ | ||
| method: DEVFRAME_EVENTS.broadcast.authRevoked, | ||
| args: [], | ||
| filter: (client) => affectedSessionIds.has(client.$meta.id) | ||
| }); | ||
| } | ||
| /** | ||
| * Revoke an auth token: remove from storage and notify all connected clients | ||
| * using this token that they are no longer trusted. | ||
| */ | ||
| async function revokeAuthToken(context, storage, token) { | ||
| storage.mutate((state) => { | ||
| delete state.trusted[token]; | ||
| }); | ||
| await revokeActiveConnectionsForToken(context, token); | ||
| } | ||
| //#endregion | ||
| export { revokeAuthToken as n, revokeActiveConnectionsForToken as t }; |
| import { d as DevframeRpcConnection, u as DevframeNodeRpcSessionMeta } from "./ws-server-DLtEoLdR.mjs"; | ||
| import { Ct as DevframeAuthHandler, _t as DevframeRpcServerFunctions, gt as DevframeRpcClientFunctions, o as DevframeNodeContext, s as DevframeNodeRpcSession } from "./context-_i51nYOs.mjs"; | ||
| import "./index-CEvd-YXD.mjs"; | ||
| import { BirpcGroup, EventOptions } from "birpc"; | ||
| //#region src/node/rpc-core.d.ts | ||
| interface CreateContextRpcServerOptions { | ||
| context: DevframeNodeContext; | ||
| /** | ||
| * Auth intent: `true`/omitted gates by default, `false` opts out (auto-trust | ||
| * handshake shim), a {@link DevframeAuthHandler} installs a custom scheme. | ||
| */ | ||
| auth?: boolean | DevframeAuthHandler; | ||
| /** Lower-level per-call gate by method name and session, without a full handler. */ | ||
| authorize?: (methodName: string, session: DevframeNodeRpcSession) => boolean; | ||
| /** Called once per new RPC connection, right after its session is created. */ | ||
| onPeerConnect?: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void; | ||
| /** Called once per closed RPC connection, after the transport's disconnect bookkeeping. */ | ||
| onPeerDisconnect?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void; | ||
| /** Forwarded verbatim to birpc's `rpcOptions` so a host keeps seeing RPC failures. */ | ||
| rpcOptions?: Pick<EventOptions<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>, 'onFunctionError' | 'onGeneralError'>; | ||
| } | ||
| interface ContextRpcServer { | ||
| rpcGroup: BirpcGroup<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>; | ||
| /** The resolved auth handler when `auth` was passed as one. */ | ||
| authHandler?: DevframeAuthHandler; | ||
| /** | ||
| * Connection lifecycle handlers to wire into a transport binding | ||
| * (`attachWsRpcTransport`'s `onConnected` / `onDisconnected`, or any other | ||
| * crossws adapter's peer hooks via `createWsRpcPeerHooks`). | ||
| */ | ||
| onConnected?: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void; | ||
| onDisconnected: (connection: DevframeRpcConnection, meta: DevframeNodeRpcSessionMeta) => void; | ||
| } | ||
| /** | ||
| * Bind a devframe context's registered RPC functions to a birpc group, | ||
| * transport-agnostically — the shared core under the instance shell's own | ||
| * HTTP+WS binding (Node http + WS) and the Bun fetch-upgrade tier of | ||
| * `createHandler`. | ||
| * | ||
| * Owns everything about serving RPC that is independent of *how* peers | ||
| * connect: the auth handler's function registration, the | ||
| * `AsyncLocalStorage`-based session resolver (so | ||
| * `ctx.rpc.getCurrentRpcSession()` works inside handlers), the | ||
| * `authorize` gate, and the `auth: false` auto-trust handshake shim. | ||
| */ | ||
| declare function createContextRpcServer(options: CreateContextRpcServerOptions): ContextRpcServer; | ||
| //#endregion | ||
| export { CreateContextRpcServerOptions as n, createContextRpcServer as r, ContextRpcServer as t }; |
Sorry, the diff of this file is too big to display
833849
0.23%14931
0.08%