devframe 0.6.0-beta.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/adapters/build.d.mts +1 -1
  2. package/dist/adapters/build.mjs +2 -2
  3. package/dist/adapters/cli.d.mts +1 -1
  4. package/dist/adapters/cli.mjs +9 -6
  5. package/dist/adapters/dev.d.mts +9 -2
  6. package/dist/adapters/dev.mjs +1 -1
  7. package/dist/adapters/embedded.d.mts +1 -1
  8. package/dist/adapters/mcp.d.mts +1 -1
  9. package/dist/adapters/mcp.mjs +1 -337
  10. package/dist/build-server-0jWgMrz-.mjs +338 -0
  11. package/dist/client/index.d.mts +2 -2
  12. package/dist/client/index.mjs +56 -22
  13. package/dist/constants.d.mts +31 -1
  14. package/dist/constants.mjs +33 -1
  15. package/dist/{context-3x_bgBgZ.mjs → context-CKJheEtf.mjs} +2 -2
  16. package/dist/{context-BEVByy_Z.d.mts → context-UX9qUDK7.d.mts} +1 -1
  17. package/dist/{dev-BaX8fLH2.mjs → dev-zC5542OV.mjs} +49 -6
  18. package/dist/{devframe-D-gr9sBx.d.mts → devframe-BtVHhmwM.d.mts} +76 -4
  19. package/dist/{dump-DoXkj4ku.mjs → dump-D09PZb7T.mjs} +14 -4
  20. package/dist/{hash-DFc5WwhO.mjs → hash-PAfnyu2k.mjs} +1 -1
  21. package/dist/helpers/vite.d.mts +1 -1
  22. package/dist/helpers/vite.mjs +2 -2
  23. package/dist/{host-h3-C5SZlk03.mjs → host-h3-Dsf-Tgwx.mjs} +37 -11
  24. package/dist/http-COX960WK.mjs +114 -0
  25. package/dist/index-ByaMT1Xp.d.mts +107 -0
  26. package/dist/{index-CxaPKDXX.d.mts → index-Dy6GFDRf.d.mts} +1 -0
  27. package/dist/index.d.mts +2 -2
  28. package/dist/{launch-editor-DPfltGA4.mjs → launch-editor-DIzaS5EG.mjs} +58 -43
  29. package/dist/node/auth.d.mts +2 -64
  30. package/dist/node/auth.mjs +2 -100
  31. package/dist/node/hub-internals.d.mts +2 -2
  32. package/dist/node/hub-internals.mjs +1 -1
  33. package/dist/node/index.d.mts +3 -3
  34. package/dist/node/index.mjs +3 -3
  35. package/dist/recipes/interactive-auth.d.mts +53 -0
  36. package/dist/recipes/interactive-auth.mjs +132 -0
  37. package/dist/recipes/open-helpers.mjs +2 -2
  38. package/dist/{revoke-ENfxWkFK.mjs → revoke-BtQDKTp7.mjs} +1 -1
  39. package/dist/rpc/dump.mjs +1 -1
  40. package/dist/rpc/index.d.mts +2 -2
  41. package/dist/rpc/index.mjs +4 -1
  42. package/dist/rpc/transports/ws-client.mjs +21 -8
  43. package/dist/{server-Dl6TU5LE.mjs → server-CAsvfJNK.mjs} +51 -15
  44. package/dist/{server-rX781kz-.d.mts → server-Cfr1NrGQ.d.mts} +52 -10
  45. package/dist/{shared-state-D4PPieA0.mjs → shared-state-QtMas0NG.mjs} +34 -4
  46. package/dist/state-WX3HT5a3.mjs +101 -0
  47. package/dist/{storage-l2ezeend.mjs → storage-yrUZaiib.mjs} +55 -6
  48. package/dist/types/index.d.mts +2 -2
  49. package/dist/utils/crypto-token.mjs +1 -1
  50. package/dist/utils/events.d.mts +1 -1
  51. package/dist/utils/hash.mjs +1 -1
  52. package/dist/utils/launch-editor.mjs +1 -1
  53. package/dist/utils/open.mjs +1 -1
  54. package/dist/utils/serve-static.mjs +2 -1
  55. package/dist/utils/shared-state.d.mts +1 -1
  56. package/dist/utils/shared-state.mjs +1 -1
  57. package/dist/utils/streaming-channel.d.mts +1 -1
  58. package/dist/utils/when.d.mts +1 -1
  59. package/package.json +10 -9
  60. package/dist/{open-Dusa2Zzd.mjs → open-Deb5xmIT.mjs} +1 -1
@@ -0,0 +1,338 @@
1
+ import { a as diagnostics } from "./storage-yrUZaiib.mjs";
2
+ import { n as createHostContext } from "./host-h3-Dsf-Tgwx.mjs";
3
+ import { join } from "pathe";
4
+ import process from "node:process";
5
+ import { homedir } from "node:os";
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
8
+ import { toJsonSchema } from "@valibot/to-json-schema";
9
+ //#region src/adapters/mcp/stringify.ts
10
+ /**
11
+ * JSON-coercing serializer for MCP text payloads.
12
+ *
13
+ * MCP carries tool results and resource reads as plain text over a
14
+ * JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
15
+ * format the WS RPC transport falls back to for non-JSON values. Instead,
16
+ * we coerce common non-JSON types into JSON-friendly forms so the LLM
17
+ * client sees something useful instead of `[object Object]`.
18
+ *
19
+ * Coercions:
20
+ * - `BigInt` → `"123n"`
21
+ * - `Date` → ISO string (via the native `toJSON`)
22
+ * - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
23
+ * - `Set` → `{ __type: 'Set', entries: [v, …] }`
24
+ * - `Error` → `{ name, message, stack, cause? }` (cause recurses)
25
+ * - `Function` → `"[Function: name]"`
26
+ * - `Symbol` → `value.toString()`
27
+ * - cycles → `"[Circular]"`
28
+ */
29
+ function stringifyForMcp(value) {
30
+ if (value === void 0) return "undefined";
31
+ if (typeof value === "string") return value;
32
+ const seen = /* @__PURE__ */ new WeakSet();
33
+ return JSON.stringify(value, (_key, val) => {
34
+ if (typeof val === "bigint") return `${val}n`;
35
+ if (val instanceof Error) {
36
+ const out = {
37
+ name: val.name,
38
+ message: val.message,
39
+ stack: val.stack
40
+ };
41
+ if (val.cause !== void 0) out.cause = val.cause;
42
+ return out;
43
+ }
44
+ if (val instanceof Map) return {
45
+ __type: "Map",
46
+ entries: [...val.entries()]
47
+ };
48
+ if (val instanceof Set) return {
49
+ __type: "Set",
50
+ entries: [...val]
51
+ };
52
+ if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
53
+ if (typeof val === "symbol") return val.toString();
54
+ if (val !== null && typeof val === "object") {
55
+ if (seen.has(val)) return "[Circular]";
56
+ seen.add(val);
57
+ }
58
+ return val;
59
+ }, 2);
60
+ }
61
+ /**
62
+ * Format a thrown value for an MCP `isError` text payload. Surfaces the
63
+ * `Error.name`/`message`, and one level of `cause.message` so context
64
+ * isn't dropped silently.
65
+ */
66
+ function formatMcpError(error) {
67
+ if (!(error instanceof Error)) return String(error);
68
+ const cause = error.cause;
69
+ const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
70
+ return `${error.name}: ${error.message}${causeText}`;
71
+ }
72
+ //#endregion
73
+ //#region src/adapters/mcp/to-json-schema.ts
74
+ const FALLBACK_OBJECT_SCHEMA = Object.freeze({
75
+ type: "object",
76
+ additionalProperties: true
77
+ });
78
+ /**
79
+ * Convert a valibot return schema to JSON Schema.
80
+ * @internal
81
+ */
82
+ function valibotReturnToJsonSchema(schema) {
83
+ if (!schema) return void 0;
84
+ try {
85
+ return toJsonSchema(schema);
86
+ } catch {
87
+ return FALLBACK_OBJECT_SCHEMA;
88
+ }
89
+ }
90
+ /**
91
+ * Convert positional RPC args schemas to a single MCP-friendly object
92
+ * schema. When the RPC declares `args: [v.object(...)]`, unwrap the
93
+ * single-object schema directly (nicer agent UX than `{ arg0: {...} }`).
94
+ *
95
+ * Returns `undefined` when there are no args (the MCP SDK treats this
96
+ * as `{ type: 'object', properties: {} }`).
97
+ * @internal
98
+ */
99
+ function valibotArgsToJsonSchema(args) {
100
+ if (!args || args.length === 0) return {
101
+ schema: {
102
+ type: "object",
103
+ properties: {}
104
+ },
105
+ unwrapped: false
106
+ };
107
+ if (args.length === 1) {
108
+ const inner = safeToJsonSchema(args[0]);
109
+ if (isObjectJsonSchema(inner)) return {
110
+ schema: inner,
111
+ unwrapped: true
112
+ };
113
+ }
114
+ const properties = {};
115
+ const required = [];
116
+ for (let i = 0; i < args.length; i++) {
117
+ const key = `arg${i}`;
118
+ properties[key] = safeToJsonSchema(args[i]);
119
+ required.push(key);
120
+ }
121
+ return {
122
+ schema: {
123
+ type: "object",
124
+ properties,
125
+ required,
126
+ additionalProperties: false
127
+ },
128
+ unwrapped: false
129
+ };
130
+ }
131
+ function safeToJsonSchema(schema) {
132
+ try {
133
+ return toJsonSchema(schema);
134
+ } catch {
135
+ return FALLBACK_OBJECT_SCHEMA;
136
+ }
137
+ }
138
+ function isObjectJsonSchema(value) {
139
+ return !!value && typeof value === "object" && value.type === "object";
140
+ }
141
+ //#endregion
142
+ //#region src/adapters/mcp/build-server.ts
143
+ /**
144
+ * Wire an MCP {@link Server} to a devframe context. Returns the server
145
+ * plus a disposal function for the subscriptions it sets up. The
146
+ * transport is the caller's responsibility — `createMcpServer` connects
147
+ * stdio; tests can connect an {@link InMemoryTransport} instead.
148
+ *
149
+ * @internal
150
+ */
151
+ function buildMcpServerFromContext(ctx, options) {
152
+ const server = new Server({
153
+ name: options.serverName,
154
+ version: options.serverVersion
155
+ }, { capabilities: {
156
+ tools: { listChanged: true },
157
+ resources: { listChanged: true }
158
+ } });
159
+ registerToolHandlers(server, ctx);
160
+ registerResourceHandlers(server, ctx, options.exposeSharedState);
161
+ const notify = (method) => {
162
+ server.notification({ method }).catch(() => {});
163
+ };
164
+ const offManifest = ctx.agent.events.on("agent:manifest:changed", () => {
165
+ notify("notifications/tools/list_changed");
166
+ notify("notifications/resources/list_changed");
167
+ });
168
+ const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
169
+ notify("notifications/resources/list_changed");
170
+ });
171
+ return {
172
+ server,
173
+ dispose: () => {
174
+ offManifest();
175
+ offKeyAdded();
176
+ }
177
+ };
178
+ }
179
+ /**
180
+ * Build an MCP server over the agent surface of a devframe definition.
181
+ * Currently supports `stdio` transport only.
182
+ *
183
+ * @experimental The agent-native surface is experimental and may change
184
+ * without a major version bump until it stabilizes.
185
+ */
186
+ async function createMcpServer(definition, options = {}) {
187
+ const transport = options.transport ?? "stdio";
188
+ if (transport !== "stdio") throw diagnostics.DF0017({
189
+ transport,
190
+ reason: "Only stdio transport is supported in this release."
191
+ });
192
+ const ctx = await createHostContext({
193
+ cwd: process.cwd(),
194
+ mode: "dev",
195
+ host: {
196
+ mountStatic: () => {},
197
+ resolveOrigin: () => "mcp://devframe",
198
+ getStorageDir: (scope) => scope === "workspace" ? join(process.cwd(), `node_modules/.${definition.id}/devframe`) : join(homedir(), `.${definition.id}/devframe`)
199
+ }
200
+ });
201
+ await definition.setup(ctx);
202
+ const { server, dispose } = buildMcpServerFromContext(ctx, {
203
+ serverName: options.serverName ?? `${definition.id} (devframe)`,
204
+ serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
205
+ exposeSharedState: options.exposeSharedState ?? true
206
+ });
207
+ const { startStdioTransport } = await import("./transports-BmDVn4SF.mjs");
208
+ let stop;
209
+ try {
210
+ stop = await startStdioTransport(server);
211
+ } catch (error) {
212
+ const reason = error instanceof Error ? error.message : String(error);
213
+ throw diagnostics.DF0017({
214
+ transport,
215
+ reason,
216
+ cause: error
217
+ });
218
+ }
219
+ options.onReady?.({ transport: "stdio" });
220
+ return { async stop() {
221
+ dispose();
222
+ await stop();
223
+ } };
224
+ }
225
+ function registerToolHandlers(server, ctx) {
226
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
227
+ return { tools: ctx.agent.list().tools.map((tool) => projectTool(tool, ctx)) };
228
+ });
229
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
230
+ const { name, arguments: args } = request.params;
231
+ try {
232
+ return { content: [{
233
+ type: "text",
234
+ text: stringifyForMcp(await ctx.agent.invoke(name, args ?? {}))
235
+ }] };
236
+ } catch (error) {
237
+ return {
238
+ isError: true,
239
+ content: [{
240
+ type: "text",
241
+ text: `Error invoking "${name}": ${formatMcpError(error)}`
242
+ }]
243
+ };
244
+ }
245
+ });
246
+ }
247
+ function registerResourceHandlers(server, ctx, exposeSharedState) {
248
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
249
+ const resources = ctx.agent.list().resources.map((resource) => ({
250
+ uri: resource.uri,
251
+ name: resource.name,
252
+ description: resource.description,
253
+ mimeType: resource.mimeType
254
+ }));
255
+ if (exposeSharedState !== false) {
256
+ const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
257
+ for (const key of ctx.rpc.sharedState.keys()) {
258
+ if (!filter(key)) continue;
259
+ resources.push({
260
+ uri: `devframe://state/${encodeURIComponent(key)}`,
261
+ name: key,
262
+ description: `Shared state: ${key}`,
263
+ mimeType: "application/json"
264
+ });
265
+ }
266
+ }
267
+ return { resources };
268
+ });
269
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
270
+ const { uri } = request.params;
271
+ const parsed = parseResourceUri(uri);
272
+ if (parsed.kind === "resource") {
273
+ const content = await ctx.agent.read(parsed.id);
274
+ return { contents: [{
275
+ uri,
276
+ mimeType: content.mimeType ?? "application/json",
277
+ text: content.text ?? stringifyForMcp(content.json)
278
+ }] };
279
+ }
280
+ if (parsed.kind === "state") return { contents: [{
281
+ uri,
282
+ mimeType: "application/json",
283
+ text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
284
+ }] };
285
+ throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
286
+ });
287
+ }
288
+ function projectTool(tool, ctx) {
289
+ const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
290
+ const outputSchema = tool.outputSchema ?? computeOutputSchema(tool, ctx);
291
+ return {
292
+ name: tool.id,
293
+ title: tool.title,
294
+ description: tool.description,
295
+ inputSchema,
296
+ ...outputSchema ? { outputSchema } : {},
297
+ annotations: {
298
+ title: tool.title,
299
+ readOnlyHint: tool.safety === "read",
300
+ destructiveHint: tool.safety === "destructive"
301
+ }
302
+ };
303
+ }
304
+ function computeInputSchema(tool, ctx) {
305
+ if (tool.kind !== "rpc" || !tool.rpcName) return {
306
+ type: "object",
307
+ properties: {}
308
+ };
309
+ const def = ctx.rpc.definitions.get(tool.rpcName);
310
+ if (!def) return {
311
+ type: "object",
312
+ properties: {}
313
+ };
314
+ const args = def.args;
315
+ return valibotArgsToJsonSchema(args).schema;
316
+ }
317
+ function computeOutputSchema(tool, ctx) {
318
+ if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
319
+ const def = ctx.rpc.definitions.get(tool.rpcName);
320
+ if (!def) return void 0;
321
+ return valibotReturnToJsonSchema(def.returns);
322
+ }
323
+ function parseResourceUri(uri) {
324
+ const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
325
+ if (!match) return { kind: "unknown" };
326
+ const [, kind, rest] = match;
327
+ const decoded = decodeURIComponent(rest);
328
+ if (kind === "resource") return {
329
+ kind: "resource",
330
+ id: decoded
331
+ };
332
+ return {
333
+ kind: "state",
334
+ key: decoded
335
+ };
336
+ }
337
+ //#endregion
338
+ export { createMcpServer as n, buildMcpServerFromContext as t };
@@ -1,6 +1,6 @@
1
- import { $ as ScopedServerFunctions, D as StreamReader, J as DevframeSettings, L as SharedState, O as StreamSink, bt as EventEmitter, d as ConnectionMeta, et as ScopedSharedStates, it as DevframeRpcServerFunctions, rt as DevframeRpcClientFunctions, tt as SettingsForNamespace, v as RpcSharedStateGetOptions, y as RpcSharedStateHost } from "../devframe-D-gr9sBx.mjs";
1
+ import { O as StreamReader, R as SharedState, Y as DevframeSettings, at as DevframeRpcServerFunctions, b as RpcSharedStateHost, et as ScopedServerFunctions, f as ConnectionMeta, it as DevframeRpcClientFunctions, k as StreamSink, nt as SettingsForNamespace, tt as ScopedSharedStates, xt as EventEmitter, y as RpcSharedStateGetOptions } from "../devframe-BtVHhmwM.mjs";
2
2
  import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-DZEx4ffs.mjs";
3
- import { C as RpcCacheManager, w as RpcCacheOptions } from "../index-CxaPKDXX.mjs";
3
+ import { C as RpcCacheManager, w as RpcCacheOptions } from "../index-Dy6GFDRf.mjs";
4
4
  import { t as WsRpcChannelOptions } from "../ws-client-D0z0pOhn.mjs";
5
5
  import { BirpcOptions, BirpcReturn } from "birpc";
6
6
 
@@ -1,7 +1,7 @@
1
- import { t as hash } from "../hash-DFc5WwhO.mjs";
1
+ import { t as hash } from "../hash-PAfnyu2k.mjs";
2
2
  import { colors } from "../utils/colors.mjs";
3
3
  import { createEventEmitter } from "../utils/events.mjs";
4
- import { t as createSharedState } from "../shared-state-D4PPieA0.mjs";
4
+ import { t as createSharedState } from "../shared-state-QtMas0NG.mjs";
5
5
  import { i as structuredCloneStringify, n as structuredCloneParse, t as structuredCloneDeserialize } from "../structured-clone-CeZOHvkd.mjs";
6
6
  import { createStreamReader, createStreamSink } from "../utils/streaming-channel.mjs";
7
7
  import { promiseWithResolver } from "../utils/promise.mjs";
@@ -21,6 +21,14 @@ const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = "__rpc-dump/index.json";
21
21
  * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
22
22
  */
23
23
  const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
24
+ /**
25
+ * WS upgrade-URL query parameter carrying a previously-issued bearer token.
26
+ * Set by `createWsRpcChannel` (browser transport) whenever `authToken` is
27
+ * passed; read at connect time by a host's connect-time trust hook (see
28
+ * `recipes/interactive-auth`'s `onConnect`) so a returning client can be
29
+ * trusted before its own `anonymous:devframe:auth` handshake call arrives.
30
+ */
31
+ const DEVFRAME_AUTH_TOKEN_QUERY_PARAM = "devframe_auth_token";
24
32
  //#endregion
25
33
  //#region src/utils/diagnostics-reporter.ts
26
34
  const formatAnsi = ansiFormatter(colors);
@@ -83,14 +91,24 @@ async function getRpcResolvedSetupResult(definition, context) {
83
91
  if (!definition.setup) return {};
84
92
  if (typeof context === "object" && context !== null) {
85
93
  definition.__cache ??= /* @__PURE__ */ new WeakMap();
86
- let promise = definition.__cache.get(context);
94
+ const cache = definition.__cache;
95
+ let promise = cache.get(context);
87
96
  if (!promise) {
88
97
  promise = Promise.resolve(definition.setup(context));
89
- definition.__cache.set(context, promise);
98
+ promise.catch(() => {
99
+ if (cache.get(context) === promise) cache.delete(context);
100
+ });
101
+ cache.set(context, promise);
90
102
  }
91
103
  return await promise;
92
104
  }
93
- definition.__promise ??= Promise.resolve(definition.setup(context));
105
+ if (!definition.__promise) {
106
+ const promise = Promise.resolve(definition.setup(context));
107
+ promise.catch(() => {
108
+ if (definition.__promise === promise) definition.__promise = void 0;
109
+ });
110
+ definition.__promise = promise;
111
+ }
94
112
  return await definition.__promise;
95
113
  }
96
114
  async function getRpcHandler(definition, context) {
@@ -122,6 +140,9 @@ var RpcCacheManager = class {
122
140
  const methodCache = this.cacheMap.get(m);
123
141
  if (methodCache) return methodCache.get(this.keySerializer(a));
124
142
  }
143
+ has(m, a) {
144
+ return this.cacheMap.get(m)?.has(this.keySerializer(a)) ?? false;
145
+ }
125
146
  apply(req, res) {
126
147
  const methodCache = this.cacheMap.get(req.m) || /* @__PURE__ */ new Map();
127
148
  methodCache.set(this.keySerializer(req.a), res);
@@ -398,8 +419,12 @@ function createRpcSharedStateClientHost(rpc) {
398
419
  return new Promise((resolve) => {
399
420
  if (!rpc.isTrusted) {
400
421
  resolve(state);
422
+ let initialized = false;
401
423
  rpc.events.on("rpc:is-trusted:updated", (isTrusted) => {
402
- if (isTrusted) initSharedState();
424
+ if (isTrusted && !initialized) {
425
+ initialized = true;
426
+ initSharedState();
427
+ }
403
428
  });
404
429
  } else initSharedState().then(resolve);
405
430
  });
@@ -611,14 +636,15 @@ const EMPTY_DEFS = /* @__PURE__ */ new Map();
611
636
  */
612
637
  function createWsRpcChannel(options) {
613
638
  let url = options.url;
614
- if (options.authToken) url = `${url}?devframe_auth_token=${encodeURIComponent(options.authToken)}`;
639
+ if (options.authToken) url = `${url}?${DEVFRAME_AUTH_TOKEN_QUERY_PARAM}=${encodeURIComponent(options.authToken)}`;
615
640
  const ws = new WebSocket(url);
616
641
  const { onConnected = NOOP, onError = NOOP, onDisconnected = NOOP, definitions = EMPTY_DEFS } = options;
617
642
  ws.addEventListener("open", (e) => {
618
643
  onConnected(e);
619
644
  });
620
645
  ws.addEventListener("error", (e) => {
621
- onError(e instanceof Error ? e : new Error(e.type));
646
+ const _e = e instanceof Error ? e : new Error(e.type);
647
+ onError(_e);
622
648
  });
623
649
  ws.addEventListener("close", (e) => {
624
650
  onDisconnected(e);
@@ -631,14 +657,25 @@ function createWsRpcChannel(options) {
631
657
  });
632
658
  },
633
659
  post: (data) => {
634
- if (ws.readyState === WebSocket.OPEN) ws.send(data);
635
- else {
636
- function handler() {
637
- ws.send(data);
638
- ws.removeEventListener("open", handler);
660
+ if (ws.readyState === WebSocket.OPEN) {
661
+ ws.send(data);
662
+ return;
663
+ }
664
+ if (ws.readyState === WebSocket.CONNECTING) {
665
+ const onOpen = () => {
666
+ cleanup();
667
+ if (ws.readyState === WebSocket.OPEN) ws.send(data);
668
+ };
669
+ const onClose = () => cleanup();
670
+ function cleanup() {
671
+ ws.removeEventListener("open", onOpen);
672
+ ws.removeEventListener("close", onClose);
639
673
  }
640
- ws.addEventListener("open", handler);
674
+ ws.addEventListener("open", onOpen);
675
+ ws.addEventListener("close", onClose);
676
+ return;
641
677
  }
678
+ onError(/* @__PURE__ */ new Error("Devframe WebSocket is not open; message dropped"));
642
679
  },
643
680
  serialize: (msg) => {
644
681
  let method;
@@ -1899,7 +1936,7 @@ function createWsRpcClientMode(options) {
1899
1936
  }
1900
1937
  async function requestTrustWithToken(token) {
1901
1938
  currentAuthToken = token;
1902
- const result = await serverRpc.$call("devframe:anonymous:auth", {
1939
+ const result = await serverRpc.$call("anonymous:devframe:auth", {
1903
1940
  authToken: token,
1904
1941
  ua: describeUA(),
1905
1942
  origin: location.origin
@@ -1910,7 +1947,7 @@ function createWsRpcClientMode(options) {
1910
1947
  return result.isTrusted;
1911
1948
  }
1912
1949
  async function requestTrustWithCode(code) {
1913
- const token = (await serverRpc.$call("devframe:auth:exchange", {
1950
+ const token = (await serverRpc.$call("anonymous:devframe:auth:exchange", {
1914
1951
  code,
1915
1952
  ua: describeUA(),
1916
1953
  origin: location.origin
@@ -2124,12 +2161,9 @@ async function getDevframeRpcClient(options = {}) {
2124
2161
  async onRequest(req, next, resolve) {
2125
2162
  await rpcOptions.onRequest?.call(this, req, next, resolve);
2126
2163
  if (cacheOptions && cacheManager?.validate(req.m)) {
2127
- const cached = cacheManager.cached(req.m, req.a);
2128
- if (cached) return resolve(cached);
2129
- else {
2130
- const res = await next(req);
2131
- cacheManager?.apply(req, res);
2132
- }
2164
+ if (cacheManager.has(req.m, req.a)) return resolve(cacheManager.cached(req.m, req.a));
2165
+ const res = await next(req);
2166
+ cacheManager.apply(req, res);
2133
2167
  } else await next(req);
2134
2168
  }
2135
2169
  },
@@ -11,6 +11,13 @@ declare const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
11
11
  * handler here without colliding with its own routes (HMR, asset serving).
12
12
  */
13
13
  declare const DEVFRAME_WS_ROUTE = "__devframe_ws";
14
+ /**
15
+ * Route the Streamable-HTTP MCP endpoint is bound to, relative to a
16
+ * devframe's base path. Sits next to `__connection.json` and the WS route
17
+ * so an MCP client reaches it on the same origin the SPA loaded from — the
18
+ * dev server shares one port for HTTP, WS, and MCP. Opt-in via `cli.mcp`.
19
+ */
20
+ declare const DEVFRAME_MCP_ROUTE = "__mcp";
14
21
  declare const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = "__rpc-dump/index.json";
15
22
  declare const DEVFRAME_DOCK_IMPORTS_FILENAME = "__client-imports.js";
16
23
  declare const DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID = "/__devframe-client-imports.js";
@@ -29,5 +36,28 @@ declare const REMOTE_CONNECTION_KEY = "devframe-remote-connection";
29
36
  * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
30
37
  */
31
38
  declare const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
39
+ /**
40
+ * WS upgrade-URL query parameter carrying a previously-issued bearer token.
41
+ * Set by `createWsRpcChannel` (browser transport) whenever `authToken` is
42
+ * passed; read at connect time by a host's connect-time trust hook (see
43
+ * `recipes/interactive-auth`'s `onConnect`) so a returning client can be
44
+ * trusted before its own `anonymous:devframe:auth` handshake call arrives.
45
+ */
46
+ declare const DEVFRAME_AUTH_TOKEN_QUERY_PARAM = "devframe_auth_token";
47
+ /**
48
+ * Prefix that marks an RPC method as callable before a connection is
49
+ * trusted. This is the *only* rule the pre-trust gate applies — there is no
50
+ * per-method allowlist. Any handshake method a host adapter needs to reach
51
+ * before authentication must be named `anonymous:<rest>` (e.g.
52
+ * `anonymous:devframe:auth`).
53
+ */
54
+ declare const ANONYMOUS_RPC_PREFIX = "anonymous:";
55
+ /**
56
+ * Whether `name` is callable before a connection is trusted, i.e. it starts
57
+ * with {@link ANONYMOUS_RPC_PREFIX}. Used by the resolver gate in
58
+ * `startHttpAndWs` (via an `authorize` function) and by host adapters that
59
+ * implement their own transport.
60
+ */
61
+ declare function isAnonymousRpcMethod(name: string): boolean;
32
62
  //#endregion
33
- export { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_OTP_URL_PARAM, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, DEVFRAME_WS_ROUTE, REMOTE_CONNECTION_KEY };
63
+ export { ANONYMOUS_RPC_PREFIX, DEVFRAME_AUTH_TOKEN_QUERY_PARAM, DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MCP_ROUTE, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_OTP_URL_PARAM, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, DEVFRAME_WS_ROUTE, REMOTE_CONNECTION_KEY, isAnonymousRpcMethod };
@@ -11,6 +11,13 @@ const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
11
11
  * handler here without colliding with its own routes (HMR, asset serving).
12
12
  */
13
13
  const DEVFRAME_WS_ROUTE = "__devframe_ws";
14
+ /**
15
+ * Route the Streamable-HTTP MCP endpoint is bound to, relative to a
16
+ * devframe's base path. Sits next to `__connection.json` and the WS route
17
+ * so an MCP client reaches it on the same origin the SPA loaded from — the
18
+ * dev server shares one port for HTTP, WS, and MCP. Opt-in via `cli.mcp`.
19
+ */
20
+ const DEVFRAME_MCP_ROUTE = "__mcp";
14
21
  const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = "__rpc-dump/index.json";
15
22
  const DEVFRAME_DOCK_IMPORTS_FILENAME = "__client-imports.js";
16
23
  const DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID = "/__devframe-client-imports.js";
@@ -29,5 +36,30 @@ const REMOTE_CONNECTION_KEY = "devframe-remote-connection";
29
36
  * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
30
37
  */
31
38
  const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
39
+ /**
40
+ * WS upgrade-URL query parameter carrying a previously-issued bearer token.
41
+ * Set by `createWsRpcChannel` (browser transport) whenever `authToken` is
42
+ * passed; read at connect time by a host's connect-time trust hook (see
43
+ * `recipes/interactive-auth`'s `onConnect`) so a returning client can be
44
+ * trusted before its own `anonymous:devframe:auth` handshake call arrives.
45
+ */
46
+ const DEVFRAME_AUTH_TOKEN_QUERY_PARAM = "devframe_auth_token";
47
+ /**
48
+ * Prefix that marks an RPC method as callable before a connection is
49
+ * trusted. This is the *only* rule the pre-trust gate applies — there is no
50
+ * per-method allowlist. Any handshake method a host adapter needs to reach
51
+ * before authentication must be named `anonymous:<rest>` (e.g.
52
+ * `anonymous:devframe:auth`).
53
+ */
54
+ const ANONYMOUS_RPC_PREFIX = "anonymous:";
55
+ /**
56
+ * Whether `name` is callable before a connection is trusted, i.e. it starts
57
+ * with {@link ANONYMOUS_RPC_PREFIX}. Used by the resolver gate in
58
+ * `startHttpAndWs` (via an `authorize` function) and by host adapters that
59
+ * implement their own transport.
60
+ */
61
+ function isAnonymousRpcMethod(name) {
62
+ return name.startsWith(ANONYMOUS_RPC_PREFIX);
63
+ }
32
64
  //#endregion
33
- export { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_OTP_URL_PARAM, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, DEVFRAME_WS_ROUTE, REMOTE_CONNECTION_KEY };
65
+ export { ANONYMOUS_RPC_PREFIX, DEVFRAME_AUTH_TOKEN_QUERY_PARAM, DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MCP_ROUTE, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_OTP_URL_PARAM, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, DEVFRAME_WS_ROUTE, REMOTE_CONNECTION_KEY, isAnonymousRpcMethod };
@@ -1,5 +1,5 @@
1
- import { t as createStorage } from "./storage-l2ezeend.mjs";
2
- import { i as randomToken, n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-ENfxWkFK.mjs";
1
+ import { t as createStorage } from "./storage-yrUZaiib.mjs";
2
+ import { i as randomToken, n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-BtQDKTp7.mjs";
3
3
  import { join } from "pathe";
4
4
  //#region src/node/hub-internals/context.ts
5
5
  const internalContextMap = /* @__PURE__ */ new WeakMap();
@@ -1,4 +1,4 @@
1
- import { L as SharedState, m as DevframeNodeContext } from "./devframe-D-gr9sBx.mjs";
1
+ import { R as SharedState, h as DevframeNodeContext } from "./devframe-BtVHhmwM.mjs";
2
2
 
3
3
  //#region src/node/hub-internals/context.d.ts
4
4
  interface InternalAnonymousAuthStorage {