devframe 0.9.7 → 0.9.9

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 (116) hide show
  1. package/dist/{_shared-BM3PdYli.mjs → _shared-C08SdQRl.mjs} +15 -4
  2. package/dist/{_shared-BJ6AmwTu.d.mts → _shared-CK3czkH4.d.mts} +25 -2
  3. package/dist/adapters/build.d.mts +6 -6
  4. package/dist/adapters/build.mjs +32 -22
  5. package/dist/adapters/cac.d.mts +16 -1
  6. package/dist/adapters/cac.mjs +8 -7
  7. package/dist/adapters/dev.d.mts +13 -13
  8. package/dist/adapters/dev.mjs +2 -2
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/initiate.d.mts +28 -26
  11. package/dist/adapters/initiate.mjs +1 -1
  12. package/dist/adapters/mcp.d.mts +28 -14
  13. package/dist/adapters/mcp.mjs +75 -39
  14. package/dist/{agent-tool-name-EgfoFO8C.mjs → agent-tool-name-DGzeuWcO.mjs} +2 -2
  15. package/dist/cli/main.d.mts +1 -1
  16. package/dist/cli/main.mjs +41 -17
  17. package/dist/client/index.d.mts +36 -36
  18. package/dist/client/index.mjs +83 -51
  19. package/dist/client-assets-DNdhagGm.mjs +16 -0
  20. package/dist/constants.d.mts +8 -7
  21. package/dist/constants.mjs +7 -6
  22. package/dist/{context-BhBzOXVa.d.mts → context-4AIVRLYF.d.mts} +173 -122
  23. package/dist/{context-omlma_1n.mjs → context-CwPUinjn.mjs} +126 -88
  24. package/dist/{context-KUqAfawQ.mjs → context-DIUvJW4D.mjs} +3 -3
  25. package/dist/{context-HOnG-WvG.d.mts → context-rgBSPFHS.d.mts} +5 -5
  26. package/dist/{crypto-token-XCqTSMg9.mjs → crypto-token-TVbGGFuj.mjs} +1 -1
  27. package/dist/{dev-Czit5IP4.mjs → dev-Eqq5PX0k.mjs} +37 -18
  28. package/dist/{diagnostics-BXWW3VK-.mjs → diagnostics-BMfN8mb1.mjs} +30 -17
  29. package/dist/{diagnostics-CD8nlgll.mjs → diagnostics-D0EVm5M6.mjs} +2 -2
  30. package/dist/{events-4daw0uK9.mjs → events-BV4Dj59a.mjs} +2 -2
  31. package/dist/{host-h3-fRbF9yor.mjs → host-h3-BJ4siyz7.mjs} +1 -1
  32. package/dist/in-page-channel/index.d.mts +59 -22
  33. package/dist/in-page-channel/index.mjs +71 -56
  34. package/dist/{index-BzyopqxV.d.mts → index-pIVt92jP.d.mts} +9 -9
  35. package/dist/{index-DcKxIMoI.d.mts → index-pKl67H6w.d.mts} +4 -4
  36. package/dist/index.d.mts +11 -13
  37. package/dist/index.mjs +6 -7
  38. package/dist/{instance-registry-BjlMQbFB.mjs → instance-registry-C7-41RSp.mjs} +8 -8
  39. package/dist/{instance-shell-Bh4l9Vz_.d.mts → instance-shell-7Z52PE4_.d.mts} +18 -18
  40. package/dist/{instance-shell-CpVW4zG0.mjs → instance-shell-cdVETpfz.mjs} +227 -58
  41. package/dist/internal/index.d.mts +51 -21
  42. package/dist/internal/index.mjs +10 -9
  43. package/dist/node/auth.d.mts +2 -2
  44. package/dist/node/auth.mjs +2 -2
  45. package/dist/node/hub-internals.d.mts +2 -2
  46. package/dist/node/hub-internals.mjs +2 -2
  47. package/dist/node/index.d.mts +5 -5
  48. package/dist/node/index.mjs +2 -2
  49. package/dist/{nostics-SPGDgHEP.d.mts → nostics-0sKe5HIj.d.mts} +2 -2
  50. package/dist/{nostics-CzECRXpE.mjs → nostics-D0PvLZsn.mjs} +1 -1
  51. package/dist/origin-DGDH8BI9.d.mts +45 -0
  52. package/dist/origin-DZit9g29.mjs +80 -0
  53. package/dist/recipes/common-rpc-functions.d.mts +5 -5
  54. package/dist/recipes/common-rpc-functions.mjs +3 -3
  55. package/dist/recipes/interactive-auth.d.mts +6 -6
  56. package/dist/recipes/interactive-auth.mjs +6 -6
  57. package/dist/{remote-assets-DezQmPQU.mjs → remote-assets-BIFWOlBW.mjs} +26 -6
  58. package/dist/{remote-assets-B13_vRkJ.d.mts → remote-assets-eBvSKZMS.d.mts} +7 -7
  59. package/dist/{revoke-COLW0cOi.mjs → revoke-cqztiCX4.mjs} +1 -1
  60. package/dist/rpc/dump.d.mts +2 -11
  61. package/dist/rpc/dump.mjs +1 -1
  62. package/dist/rpc/index.d.mts +2 -2
  63. package/dist/rpc/index.mjs +3 -3
  64. package/dist/rpc/transports/sse-client.d.mts +1 -1
  65. package/dist/rpc/transports/sse-client.mjs +30 -14
  66. package/dist/rpc/transports/sse-server.d.mts +4 -4
  67. package/dist/rpc/transports/sse-server.mjs +7 -7
  68. package/dist/rpc/transports/ws-bun.d.mts +5 -5
  69. package/dist/rpc/transports/ws-bun.mjs +8 -2
  70. package/dist/rpc/transports/ws-client.d.mts +1 -1
  71. package/dist/rpc/transports/ws-client.mjs +2 -2
  72. package/dist/rpc/transports/ws-deno.d.mts +4 -4
  73. package/dist/rpc/transports/ws-deno.mjs +8 -2
  74. package/dist/rpc/transports/ws-server.d.mts +2 -1
  75. package/dist/rpc/transports/ws-server.mjs +232 -1
  76. package/dist/{rpc-core-BHUsDl7s.d.mts → rpc-core-Ceu6AUz8.d.mts} +4 -4
  77. package/dist/{rpc-core-DlHd276T.mjs → rpc-core-DVQ7xi89.mjs} +14 -2
  78. package/dist/{serialization-BGzEwAdr.mjs → serialization-DTZnPQ1-.mjs} +20 -18
  79. package/dist/{services-install-CqqQVls5.mjs → services-install-P_DV7VYL.mjs} +32 -21
  80. package/dist/session-DPfj6zQG.mjs +16 -0
  81. package/dist/{shared-state-DoXutg_U.mjs → shared-state-CaxB1q0h.mjs} +4 -3
  82. package/dist/{simple-schema-DQPZrAaZ.mjs → simple-schema-Bi40l-P6.mjs} +2 -2
  83. package/dist/{sse-client-DwX021ST.d.mts → sse-client-BQ4jS3eO.d.mts} +4 -4
  84. package/dist/{state-CK9LjrnT.mjs → state-CeQQWzM-.mjs} +4 -4
  85. package/dist/{static-DV_qUSRS.mjs → static-DZ8P2Kn7.mjs} +130 -135
  86. package/dist/{storage-BXNVhKyR.mjs → storage-Dccf_IyD.mjs} +1 -1
  87. package/dist/types/index.d.mts +6 -6
  88. package/dist/{types-CMdPYhOx.d.mts → types-Dq4i156u.d.mts} +17 -17
  89. package/dist/utils/agent-tool-name.d.mts +2 -2
  90. package/dist/utils/agent-tool-name.mjs +2 -2
  91. package/dist/utils/colors.d.mts +1 -1
  92. package/dist/utils/crypto-token.d.mts +1 -1
  93. package/dist/utils/crypto-token.mjs +1 -1
  94. package/dist/utils/events.d.mts +1 -1
  95. package/dist/utils/nostics.d.mts +1 -1
  96. package/dist/utils/nostics.mjs +1 -1
  97. package/dist/utils/open.mjs +1 -1
  98. package/dist/utils/origin.d.mts +2 -0
  99. package/dist/utils/origin.mjs +80 -0
  100. package/dist/utils/remote-assets.d.mts +2 -2
  101. package/dist/utils/remote-assets.mjs +1 -1
  102. package/dist/utils/serve-static.d.mts +4 -4
  103. package/dist/utils/serve-static.mjs +56 -19
  104. package/dist/utils/shared-state.d.mts +1 -1
  105. package/dist/utils/shared-state.mjs +1 -1
  106. package/dist/utils/simple-schema.d.mts +5 -5
  107. package/dist/utils/simple-schema.mjs +2 -2
  108. package/dist/utils/streaming-channel.d.mts +1 -1
  109. package/dist/utils/streaming-channel.mjs +2 -2
  110. package/dist/utils/when.d.mts +5 -5
  111. package/dist/{validation-YrZH6atx.mjs → validation-BOy8IoFZ.mjs} +2 -2
  112. package/dist/{wire-codec-0K-o5MYW.mjs → wire-codec-dcCeXcSo.mjs} +3 -3
  113. package/dist/{ws-client-7_lZi-B9.d.mts → ws-client-Xpxt0oPA.d.mts} +3 -3
  114. package/dist/{ws-server-Di2oqr4q.d.mts → ws-server-D2nv68Hg.d.mts} +13 -22
  115. package/package.json +6 -5
  116. package/dist/ws-server-BdSLrhxE.mjs +0 -267
@@ -1,9 +1,10 @@
1
- import { t as Diagnostic } from "../nostics-CzECRXpE.mjs";
2
- import { t as DEVFRAME_EVENTS } from "../events-4daw0uK9.mjs";
3
- import { i as isAllowedOrigin } from "../ws-server-BdSLrhxE.mjs";
4
- import { t as diagnostics } from "../diagnostics-BXWW3VK-.mjs";
5
- import { t as createHostContext } from "../context-omlma_1n.mjs";
6
- import { t as toAgentToolName } from "../agent-tool-name-EgfoFO8C.mjs";
1
+ import { t as Diagnostic } from "../nostics-D0PvLZsn.mjs";
2
+ import { t as DEVFRAME_EVENTS } from "../events-BV4Dj59a.mjs";
3
+ import { t as isAllowedOrigin } from "../origin-DZit9g29.mjs";
4
+ import { r as timingSafeEqual } from "../crypto-token-TVbGGFuj.mjs";
5
+ import { t as diagnostics } from "../diagnostics-BMfN8mb1.mjs";
6
+ import { t as createHostContext } from "../context-CwPUinjn.mjs";
7
+ import { t as toAgentToolName } from "../agent-tool-name-DGzeuWcO.mjs";
7
8
  import process from "node:process";
8
9
  import { join } from "pathe";
9
10
  import { homedir } from "node:os";
@@ -65,7 +66,7 @@ function stringifyForMcp(value) {
65
66
  * Format a thrown value for an MCP `isError` text payload.
66
67
  *
67
68
  * A nostics `Diagnostic` (every coded devframe error) becomes structured
68
- * JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
69
+ * JSON (`{ error: { code, message, fix?, docs? } }`), so an agent receives
69
70
  * the actionable next step (`fix`) and the docs URL instead of a bare
70
71
  * message string. Other errors surface `Error.name`/`message`, plus one
71
72
  * level of `cause.message` so context isn't dropped silently.
@@ -93,7 +94,7 @@ const FALLBACK_OBJECT_SCHEMA = Object.freeze({
93
94
  *
94
95
  * Devframe stays validator-neutral, so conversion uses the schema's own
95
96
  * [Standard JSON Schema](https://standardschema.dev/) converter
96
- * (`~standard.jsonSchema`) when the validator provides one — zod 4 does,
97
+ * (`~standard.jsonSchema`) when the validator provides one; zod 4 does,
97
98
  * for example. Validators without a native converter (e.g. valibot) degrade
98
99
  * to a permissive object schema rather than pulling in a converter library.
99
100
  */
@@ -116,7 +117,7 @@ function returnToJsonSchema(schema) {
116
117
  }
117
118
  /**
118
119
  * JSON Schema for an RPC function's positional args on the agent/MCP
119
- * surface. Each positional arg is advertised under `arg0` / `arg1` / … —
120
+ * surface. Each positional arg is advertised under `arg0` / `arg1` / …,
120
121
  * matching how the agent bridge coerces the incoming object payload back
121
122
  * into positional arguments.
122
123
  *
@@ -152,7 +153,7 @@ function argsToJsonSchema(args) {
152
153
  //#region src/adapters/mcp/build-server.ts
153
154
  /**
154
155
  * Build a fresh MCP {@link Server} over a devframe context, registering its
155
- * tool and resource handlers. This is a pure factory — it sets up no
156
+ * tool and resource handlers. This is a pure factory: it sets up no
156
157
  * long-lived subscriptions and holds no per-connection state, so it is safe
157
158
  * to call once per request under `createMcpHandler` or once per connection
158
159
  * under `serveStdio`. Change notifications are published separately: over
@@ -265,9 +266,9 @@ async function createMcpServer(definition, options = {}) {
265
266
  } };
266
267
  }
267
268
  /**
268
- * Id of the built-in shared-state read tool — namespaced like every other
269
+ * Id of the built-in shared-state read tool, namespaced like every other
269
270
  * built-in (`devframe:<area>:<fn>`). Tool-shaped access matters because many
270
- * MCP clients only consume tools — the parallel `devframe://state/<key>`
271
+ * MCP clients only consume tools; the parallel `devframe://state/<key>`
271
272
  * resource projection stays for the clients that do read resources.
272
273
  */
273
274
  const READ_STATE_TOOL = "devframe:state:read";
@@ -310,8 +311,8 @@ function registerToolHandlers(server, ctx, exposeSharedState) {
310
311
  const warnedCollisions = /* @__PURE__ */ new Set();
311
312
  /**
312
313
  * Resolve a wire tool name back to the registered {@link AgentTool}.
313
- * Wire-name matching runs first, in manifest order — the same tool the
314
- * list projection advertises under that name — with a raw-id fallback so
314
+ * Wire-name matching runs first, in manifest order (the same tool the
315
+ * list projection advertises under that name), with a raw-id fallback so
315
316
  * a colon-namespaced id keeps working as a call name.
316
317
  */
317
318
  const resolveTool = (name) => {
@@ -375,6 +376,7 @@ function registerToolHandlers(server, ctx, exposeSharedState) {
375
376
  });
376
377
  }
377
378
  function registerResourceHandlers(server, ctx, exposeSharedState) {
379
+ const stateFilter = sharedStateFilter(exposeSharedState);
378
380
  server.setRequestHandler("resources/list", async () => {
379
381
  const resources = ctx.agent.list().resources.map((resource) => ({
380
382
  uri: resource.uri,
@@ -382,17 +384,14 @@ function registerResourceHandlers(server, ctx, exposeSharedState) {
382
384
  description: resource.description,
383
385
  mimeType: resource.mimeType
384
386
  }));
385
- if (exposeSharedState !== false) {
386
- const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
387
- for (const key of ctx.rpc.sharedState.keys()) {
388
- if (!filter(key)) continue;
389
- resources.push({
390
- uri: `devframe://state/${encodeURIComponent(key)}`,
391
- name: key,
392
- description: `Shared state: ${key}`,
393
- mimeType: "application/json"
394
- });
395
- }
387
+ if (stateFilter) for (const key of ctx.rpc.sharedState.keys()) {
388
+ if (!stateFilter(key)) continue;
389
+ resources.push({
390
+ uri: `devframe://state/${encodeURIComponent(key)}`,
391
+ name: key,
392
+ description: `Shared state: ${key}`,
393
+ mimeType: "application/json"
394
+ });
396
395
  }
397
396
  return { resources };
398
397
  });
@@ -407,17 +406,20 @@ function registerResourceHandlers(server, ctx, exposeSharedState) {
407
406
  text: content.text ?? stringifyForMcp(content.json)
408
407
  }] };
409
408
  }
410
- if (parsed.kind === "state") return { contents: [{
411
- uri,
412
- mimeType: "application/json",
413
- text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
414
- }] };
409
+ if (parsed.kind === "state") {
410
+ if (!stateFilter || !stateFilter(parsed.key)) throw diagnostics.DF0048({ key: parsed.key });
411
+ return { contents: [{
412
+ uri,
413
+ mimeType: "application/json",
414
+ text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
415
+ }] };
416
+ }
415
417
  throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
416
418
  });
417
419
  }
418
420
  /**
419
421
  * MCP constrains a tool's `outputSchema` to a JSON Schema of `type:
420
- * "object"` — clients (the SDK included) reject anything else. Non-object
422
+ * "object"`; clients (the SDK included) reject anything else. Non-object
421
423
  * return schemas (e.g. a schema for `void` / a bare string) simply project
422
424
  * no output schema; the text content still carries the result.
423
425
  */
@@ -477,26 +479,56 @@ function parseResourceUri(uri) {
477
479
  //#endregion
478
480
  //#region src/adapters/mcp/fetch.ts
479
481
  /**
482
+ * Parse exactly one `Authorization: Bearer <token>` credential, returning the
483
+ * token or `undefined` for a missing, malformed, empty, or multi-credential
484
+ * header. The token itself is never logged. `\S+` rejects the whitespace that
485
+ * a second credential (fetch merges duplicate headers as `a, b`) or an empty
486
+ * value would introduce.
487
+ */
488
+ function parseBearerToken(header) {
489
+ if (!header) return void 0;
490
+ const match = /^Bearer (\S+)$/i.exec(header.trim());
491
+ return match ? match[1] : void 0;
492
+ }
493
+ /**
494
+ * Resolve the identity gate for one request. `false` is the origin-only
495
+ * opt-out; a callback delegates identity; a string requires a constant-time
496
+ * bearer match. Never reveals whether a supplied token was close to correct.
497
+ */
498
+ async function isAuthorized(req, authorization) {
499
+ if (authorization === false) return true;
500
+ if (typeof authorization === "function") return await authorization(req) === true;
501
+ const token = parseBearerToken(req.headers.get("authorization"));
502
+ if (token === void 0) return false;
503
+ return timingSafeEqual(token, authorization);
504
+ }
505
+ /**
480
506
  * Build a framework-agnostic MCP endpoint over a devframe context: a
481
- * web-standard `Request → Response` handler any host can mount — h3 (see
507
+ * web-standard `Request → Response` handler any host can mount: h3 (see
482
508
  * `mountMcpHttp`), a Next.js App Router route, or any other fetch-shaped
483
509
  * server.
484
510
  *
485
511
  * The endpoint is **stateless**: it serves the 2026-07-28 revision per request
486
512
  * through the SDK's {@link createMcpHandler}, which builds a fresh MCP server
487
513
  * (from the shared, live `ctx` via `buildMcpServerFromContext`) for each
488
- * request — no `Mcp-Session-Id` registry, no session-local routing, no
514
+ * request: no `Mcp-Session-Id` registry, no session-local routing, no
489
515
  * GET/DELETE teardown protocol. 2025-era clients are still served through the
490
516
  * SDK's default stateless legacy path. `list_changed` events reach modern
491
517
  * `subscriptions/listen` streams through the handler's `notify` bus.
492
518
  *
493
519
  * The origin gate guards every request: loopback-default DNS-rebinding
494
- * protection that — unlike the WS upgrade's `isAllowedOrigin` — also rejects
495
- * `Origin`-less requests, so a route-based endpoint isn't reachable by an
496
- * arbitrary local process.
520
+ * protection that (unlike the WS upgrade's `isAllowedOrigin`) also rejects
521
+ * `Origin`-less requests, so a route-based endpoint isn't reachable by a
522
+ * browser or a remote host (a disallowed origin gets `403`). It trusts
523
+ * same-machine callers by default. When that isn't your trust boundary, add
524
+ * an optional identity gate ({@link CreateMcpFetchHandlerOptions.authorization}),
525
+ * checked after the origin gate: a bearer/callback check that proves *who* is
526
+ * calling (a missing/invalid credential gets `401` with a
527
+ * `WWW-Authenticate: Bearer` challenge).
497
528
  */
498
529
  function createMcpFetchHandler(ctx, options) {
499
530
  const allowedOrigins = options.allowedOrigins;
531
+ const authorization = options.authorization ?? false;
500
532
  const handler = createMcpHandler(() => buildMcpServerFromContext(ctx, {
501
533
  serverName: options.serverName,
502
534
  serverVersion: options.serverVersion,
@@ -512,7 +544,11 @@ function createMcpFetchHandler(ctx, options) {
512
544
  });
513
545
  async function handle(req) {
514
546
  const origin = req.headers.get("origin") ?? void 0;
515
- if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
547
+ if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden", { status: 403 });
548
+ if (!await isAuthorized(req, authorization)) return new Response("Unauthorized", {
549
+ status: 401,
550
+ headers: { "WWW-Authenticate": "Bearer" }
551
+ });
516
552
  return handler.fetch(req);
517
553
  }
518
554
  return {
@@ -526,11 +562,11 @@ function createMcpFetchHandler(ctx, options) {
526
562
  //#endregion
527
563
  //#region src/adapters/mcp/http.ts
528
564
  /**
529
- * Mount a stateless MCP endpoint on an h3 app at `path` — the h3 binding over
565
+ * Mount a stateless MCP endpoint on an h3 app at `path`: the h3 binding over
530
566
  * {@link createMcpFetchHandler}, which owns the per-request serving, the
531
567
  * origin gate, and the transport plumbing.
532
568
  *
533
- * The handler is web-standard — it takes the h3 event's web `Request` and
569
+ * The handler is web-standard: it takes the h3 event's web `Request` and
534
570
  * returns a web `Response` (an SSE `ReadableStream` body for a
535
571
  * `subscriptions/listen` stream). We copy that response onto `event.res` and
536
572
  * return its body rather than returning the `Response` object directly, so an
@@ -7,13 +7,13 @@ const MAX_TOOL_NAME_LENGTH = 128;
7
7
  /**
8
8
  * Derive the wire-safe agent tool name for an internal tool id.
9
9
  *
10
- * Devframe tool ids are colon-namespaced — `devframe:<area>:<fn>` for
10
+ * Devframe tool ids are colon-namespaced: `devframe:<area>:<fn>` for
11
11
  * built-ins, `devframes:plugin:<slug>:<fn>` for plugin RPCs, and hub
12
12
  * command ids for command-derived tools. MCP clients constrain tool names
13
13
  * to `^[a-zA-Z0-9_-]{1,128}$`, so the agent/MCP boundary derives the wire
14
14
  * name automatically: every run of characters outside `[a-zA-Z0-9_-]`
15
15
  * becomes a single `_`, truncated to 128 characters. Internal ids never
16
- * change — resolution back to the id happens at the boundary.
16
+ * change; resolution back to the id happens at the boundary.
17
17
  *
18
18
  * ```
19
19
  * devframe:state:read → devframe_state_read
@@ -1,6 +1,6 @@
1
1
  //#region src/cli/main.d.ts
2
2
  /**
3
- * The `devframe` bin — the framework's own CLI, distinct from the per-app
3
+ * The `devframe` bin is the framework's own CLI, distinct from the per-app
4
4
  * CLI shells authors build with `createCac(definition)`. It hosts the
5
5
  * app-independent commands; today that is `connect`, the MCP connector.
6
6
  */
package/dist/cli/main.mjs CHANGED
@@ -1,18 +1,37 @@
1
- import { t as Diagnostic } from "../nostics-CzECRXpE.mjs";
2
- import { t as diagnostics } from "../diagnostics-BXWW3VK-.mjs";
3
- import { n as listLiveDevframeInstances, r as probeDevframeOrigin } from "../instance-registry-BjlMQbFB.mjs";
4
- import { t as toAgentToolName } from "../agent-tool-name-EgfoFO8C.mjs";
1
+ import { t as Diagnostic } from "../nostics-D0PvLZsn.mjs";
2
+ import { t as diagnostics } from "../diagnostics-BMfN8mb1.mjs";
3
+ import { n as listLiveDevframeInstances, r as probeDevframeOrigin } from "../instance-registry-C7-41RSp.mjs";
4
+ import { t as toAgentToolName } from "../agent-tool-name-DGzeuWcO.mjs";
5
5
  import process from "node:process";
6
6
  import { joinURL } from "ufo";
7
7
  import { cac } from "cac";
8
8
  //#region src/cli/connect.ts
9
+ /**
10
+ * Resolve the per-record bearer from the {@link ConnectServerOptions.authToken}
11
+ * option. Exported for focused tests of the credential resolution.
12
+ */
13
+ function resolveAuthToken(authToken, record) {
14
+ return typeof authToken === "function" ? authToken(record) : authToken;
15
+ }
16
+ /**
17
+ * Build the request headers the connector sends to one instance's MCP route:
18
+ * the instance's own (loopback) `origin` so the route's origin gate accepts
19
+ * this native client, plus `Authorization: Bearer <token>` when a bearer is
20
+ * configured. The bearer appears **only** here, never in the connection URL,
21
+ * the registry records, or the indexed results. Exported for focused tests.
22
+ */
23
+ function buildInstanceRequestHeaders(url, token) {
24
+ const headers = { origin: new URL(url).origin };
25
+ if (token) headers.authorization = `Bearer ${token}`;
26
+ return headers;
27
+ }
9
28
  const INDEX_TOOL = toAgentToolName("devframe:connect:list-instances");
10
29
  const CALL_TOOL = toAgentToolName("devframe:connect:call-tool");
11
30
  const MCP_DISABLED_HINT = "This instance runs without an MCP route. Restart it with the --mcp flag (or set `cli.mcp: true` on its definition) to expose its tools, then list instances again.";
12
31
  const GATEWAY_TOOLS = [{
13
32
  name: INDEX_TOOL,
14
33
  title: "Discover running devframes",
15
- description: "Discover every running devframe dev server on this machine and list each one's MCP tools. Call this FIRST, before assuming which devtools are available — the result names the instance (id, project root, origin) and the port to pass to the call tool. Safe to call freely.",
34
+ description: "Discover every running devframe dev server on this machine and list each one's MCP tools. Call this FIRST, before assuming which devtools are available; the result names the instance (id, project root, origin) and the port to pass to the call tool. Safe to call freely.",
16
35
  inputSchema: {
17
36
  type: "object",
18
37
  properties: {}
@@ -48,12 +67,12 @@ const GATEWAY_TOOLS = [{
48
67
  /**
49
68
  * Start the devframe MCP connector on stdio: a thin discovery + proxy server
50
69
  * in the shape Vercel's next-devtools-mcp (https://github.com/vercel/next-devtools-mcp)
51
- * validated — credit due there for the architecture this connector follows.
52
- * It exposes two gateway tools —
70
+ * validated; credit is due there for the architecture this connector follows.
71
+ * It exposes two gateway tools:
53
72
  * `devframe_connect_list-instances` (discover running devframe instances via
54
73
  * the instance registry and list each one's MCP tools) and
55
74
  * `devframe_connect_call-tool` (invoke one tool on one instance over its
56
- * Streamable-HTTP endpoint) — and holds no domain knowledge of its own.
75
+ * Streamable-HTTP endpoint), and holds no domain knowledge of its own.
57
76
  */
58
77
  async function startConnectServer(options = {}) {
59
78
  const sdk = await importSdk();
@@ -128,7 +147,7 @@ async function index(sdk, options) {
128
147
  try {
129
148
  entry.mcp = {
130
149
  url,
131
- tools: await listInstanceTools(sdk, url)
150
+ tools: await listInstanceTools(sdk, url, resolveAuthToken(options.authToken, record))
132
151
  };
133
152
  } catch (error) {
134
153
  entry.mcp = {
@@ -163,8 +182,8 @@ async function probePort(port, timeoutMs) {
163
182
  startedAt: 0
164
183
  };
165
184
  }
166
- async function listInstanceTools(sdk, url) {
167
- return withInstanceClient(sdk, url, async (client) => {
185
+ async function listInstanceTools(sdk, url, token) {
186
+ return withInstanceClient(sdk, url, token, async (client) => {
168
187
  return (await client.listTools()).tools.map((tool) => ({
169
188
  name: tool.name,
170
189
  description: tool.description
@@ -180,7 +199,7 @@ async function call(sdk, options, args) {
180
199
  const record = live.find((r) => r.port === args.port) ?? await probePort(args.port, options.timeoutMs);
181
200
  if (!record) throw diagnostics.DF0050({ port: args.port });
182
201
  if (!record.mcp) throw diagnostics.DF0051({ port: args.port });
183
- return withInstanceClient(sdk, `${record.origin}${record.mcp.path}`, async (client) => {
202
+ return withInstanceClient(sdk, `${record.origin}${record.mcp.path}`, resolveAuthToken(options.authToken, record), async (client) => {
184
203
  const result = await client.callTool({
185
204
  name: args.tool,
186
205
  arguments: args.args ?? {}
@@ -197,9 +216,8 @@ async function call(sdk, options, args) {
197
216
  };
198
217
  });
199
218
  }
200
- async function withInstanceClient(sdk, url, fn) {
201
- const origin = new URL(url).origin;
202
- const transport = new sdk.StreamableHTTPClientTransport(new URL(url), { requestInit: { headers: { origin } } });
219
+ async function withInstanceClient(sdk, url, token, fn) {
220
+ const transport = new sdk.StreamableHTTPClientTransport(new URL(url), { requestInit: { headers: buildInstanceRequestHeaders(url, token) } });
203
221
  const client = new sdk.Client({
204
222
  name: "devframe-connect",
205
223
  version: "0.0.0"
@@ -254,7 +272,7 @@ function keepAlive() {
254
272
  //#endregion
255
273
  //#region src/cli/main.ts
256
274
  /**
257
- * The `devframe` bin — the framework's own CLI, distinct from the per-app
275
+ * The `devframe` bin is the framework's own CLI, distinct from the per-app
258
276
  * CLI shells authors build with `createCac(definition)`. It hosts the
259
277
  * app-independent commands; today that is `connect`, the MCP connector.
260
278
  */
@@ -264,7 +282,13 @@ async function runDevframeCli(argv = process.argv) {
264
282
  await startConnectServer({
265
283
  ports: parsePortsFlag(options.port),
266
284
  instancesDir: options.instancesDir,
267
- timeoutMs: options.timeout
285
+ timeoutMs: options.timeout,
286
+ /**
287
+ * The bearer for authenticated instance MCP routes comes from the
288
+ * environment, never a CLI flag: command-line arguments are visible to
289
+ * any process on the machine (`ps`, `/proc`), which would defeat it.
290
+ */
291
+ authToken: process.env.DEVFRAME_MCP_AUTH_TOKEN
268
292
  });
269
293
  keepAlive();
270
294
  });
@@ -1,8 +1,8 @@
1
- import { f as RpcCacheManager, p as RpcCacheOptions } from "../index-BzyopqxV.mjs";
2
- import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-CMdPYhOx.mjs";
3
- 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-BhBzOXVa.mjs";
4
- import { t as SseRpcChannelOptions } from "../sse-client-DwX021ST.mjs";
5
- import { t as WsRpcChannelOptions } from "../ws-client-7_lZi-B9.mjs";
1
+ import { f as RpcCacheManager, p as RpcCacheOptions } from "../index-pIVt92jP.mjs";
2
+ import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-Dq4i156u.mjs";
3
+ import { $ as DevframeServiceScopeOf, Bt as EventEmitter, Z as DevframeServiceMeta, _t as DevframeRpcClientFunctions, b as StreamSink, d as RpcSharedStateHost, ft as ScopedRpcFn, ht as SettingsForNamespace, k as SharedState, mt as ScopedSharedStates, pt as ScopedServerFunctions, rt as DevframeServicesState, st as DevframeSettings, t as ConnectionMeta, u as RpcSharedStateGetOptions, vt as DevframeRpcServerFunctions, y as StreamReader } from "../context-4AIVRLYF.mjs";
4
+ import { t as SseRpcChannelOptions } from "../sse-client-BQ4jS3eO.mjs";
5
+ import { t as WsRpcChannelOptions } from "../ws-client-Xpxt0oPA.mjs";
6
6
  import { BirpcOptions, BirpcReturn } from "birpc";
7
7
  //#region src/client/connection.d.ts
8
8
  interface DevframeConnection {
@@ -50,15 +50,15 @@ declare function setupDevframeConnection(options?: SetupDevframeConnectionOption
50
50
  * trust handshake, so a viewer never has to reason about the two dimensions
51
51
  * separately.
52
52
  *
53
- * - `connecting` — establishing the WebSocket / running the initial trust
53
+ * - `connecting`: establishing the WebSocket / running the initial trust
54
54
  * handshake. Calls issued here queue until the socket opens.
55
- * - `connected` — socket open and trusted; RPC calls will be served.
56
- * - `unauthorized` — socket open but the server rejected trust (no valid token,
55
+ * - `connected`: socket open and trusted; RPC calls will be served.
56
+ * - `unauthorized`: socket open but the server rejected trust (no valid token,
57
57
  * or an auth-enforcing host refused it). Calls fail fast with an auth error;
58
58
  * the UI should prompt for re-authentication or a reload.
59
- * - `disconnected` — the socket closed (dropped mid-session, or never opened).
59
+ * - `disconnected`: the socket closed (dropped mid-session, or never opened).
60
60
  * Pending and new calls fail fast until the page reconnects.
61
- * - `error` — a fatal connection error (e.g. the WebSocket errored, or the
61
+ * - `error`: a fatal connection error (e.g. the WebSocket errored, or the
62
62
  * connection meta could not be loaded).
63
63
  *
64
64
  * A `static` backend has no live socket, so it reports `connected` for its
@@ -67,9 +67,9 @@ declare function setupDevframeConnection(options?: SetupDevframeConnectionOption
67
67
  type DevframeConnectionStatus = 'connecting' | 'connected' | 'unauthorized' | 'disconnected' | 'error';
68
68
  /**
69
69
  * What kind of failure a {@link DevframeConnectionError} describes:
70
- * - `connection` — the transport dropped, errored, or never opened.
71
- * - `auth` — the server rejected trust for this client.
72
- * - `timeout` — a call exceeded its {@link DevframeRpcClientOptions.callTimeout}.
70
+ * - `connection`: the transport dropped, errored, or never opened.
71
+ * - `auth`: the server rejected trust for this client.
72
+ * - `timeout`: a call exceeded its {@link DevframeRpcClientOptions.callTimeout}.
73
73
  */
74
74
  type DevframeConnectionErrorKind = 'connection' | 'auth' | 'timeout';
75
75
  /**
@@ -199,7 +199,7 @@ declare function createScopedClientContext<NS extends string = string>(rpc: Devf
199
199
  //#endregion
200
200
  //#region src/client/rpc-services.d.ts
201
201
  /**
202
- * A typed handle on one advertised wire service — the service's
202
+ * A typed handle on one advertised wire service: the service's
203
203
  * advertisement meta plus an RPC surface scoped to its namespace, so
204
204
  * `handle.rpc.call('fn-name', …)` targets `<scope>:fn-name`. Service
205
205
  * packages type the calls by augmenting `DevframeRpcServerFunctions` with
@@ -214,7 +214,7 @@ interface DevframeServiceClientHandle<NS extends string = string> extends Devfra
214
214
  /**
215
215
  * Client-side view of the server's wire-service registry, mirrored through
216
216
  * the reactive `devframe:services` shared state. The accessors are
217
- * synchronous snapshots — before the first sync lands (or on a server with
217
+ * synchronous snapshots; before the first sync lands (or on a server with
218
218
  * no services) they read as empty. For reactive UI (e.g. hiding an
219
219
  * "open in editor" button until the service appears), subscribe to the
220
220
  * shared state itself via {@link DevframeServicesClient.state}.
@@ -223,14 +223,14 @@ interface DevframeServicesClient {
223
223
  /** Whether the service package is advertised as installed. */
224
224
  has: (pkg: string) => boolean;
225
225
  /**
226
- * A typed handle on an advertised service — its meta plus a scoped RPC
227
- * surface — or `undefined` while it isn't available (never throws).
226
+ * A typed handle on an advertised service (its meta plus a scoped RPC
227
+ * surface), or `undefined` while it isn't available (never throws).
228
228
  */
229
229
  get: <PKG extends string>(pkg: PKG) => DevframeServiceClientHandle<DevframeServiceScopeOf<PKG>> | undefined;
230
230
  /** Package names of every advertised service. */
231
231
  keys: () => string[];
232
232
  /**
233
- * The mirrored `devframe:services` shared state — subscribe to its
233
+ * The mirrored `devframe:services` shared state; subscribe to its
234
234
  * `updated` event for reactivity.
235
235
  */
236
236
  state: () => Promise<SharedState<DevframeServicesState>>;
@@ -257,7 +257,7 @@ interface RpcClientEvents {
257
257
  */
258
258
  'connection:error': (error: Error) => void;
259
259
  /**
260
- * An RPC call rejected — either from the server, or because the connection
260
+ * An RPC call rejected, either from the server, or because the connection
261
261
  * was down / timed out. Useful for a global error feed or toast surface.
262
262
  */
263
263
  'rpc:error': (error: Error, method: string) => void;
@@ -271,7 +271,7 @@ interface DevframeRpcClientOptions extends SetupDevframeConnectionOptions {
271
271
  * Query-param name on the page URL carrying a one-time authentication code
272
272
  * (OTP) for "magic link" auth (e.g. a link the dev server prints). When
273
273
  * present, the client exchanges the code for a token and removes the parameter
274
- * from the URL. Set `false` to disable — e.g. integrations that drive their
274
+ * from the URL. Set `false` to disable, e.g. integrations that drive their
275
275
  * own authentication via `authenticateWithUrlOtp`.
276
276
  *
277
277
  * @default 'devframe_otp'
@@ -281,7 +281,7 @@ interface DevframeRpcClientOptions extends SetupDevframeConnectionOptions {
281
281
  * Fall back to a native browser `prompt()` for the one-time authentication
282
282
  * code when the server refuses trust and no other credential succeeds (a
283
283
  * stored token, an injected token, or a magic-link OTP). The prompt fires
284
- * only on a **top-level, unframed** page — a framed plugin (e.g. mounted in
284
+ * only on a **top-level, unframed** page; a framed plugin (e.g. mounted in
285
285
  * a hub dock) never prompts, since a hub pre-authorizes it and browsers
286
286
  * block `prompt()` in cross-origin frames anyway.
287
287
  *
@@ -294,20 +294,20 @@ interface DevframeRpcClientOptions extends SetupDevframeConnectionOptions {
294
294
  /**
295
295
  * Which live transport to connect over:
296
296
  *
297
- * - `'auto'` (default) — trust the server's advertisement: its declared
297
+ * - `'auto'` (default) trusts the server's advertisement: its declared
298
298
  * primary (`backend`), preferring the WebSocket when both endpoints are
299
299
  * present. A server that couldn't bind a socket advertises SSE as its
300
300
  * primary, so no client-side fallback probing is needed.
301
- * - `'websocket'` / `'sse'` — pin one transport; connecting fails with a
301
+ * - `'websocket'` / `'sse'` pins one transport; connecting fails with a
302
302
  * clear error when the server doesn't advertise it. Reach for
303
- * `transport: 'sse'` when an intermediary silently strips WS upgrades —
303
+ * `transport: 'sse'` when an intermediary silently strips WS upgrades,
304
304
  * something the server cannot detect.
305
305
  *
306
306
  * A `static` backend ignores this option (there is no live transport).
307
307
  */
308
308
  transport?: 'auto' | 'websocket' | 'sse';
309
309
  wsOptions?: Partial<WsRpcChannelOptions>;
310
- /** Channel overrides for the SSE transport — the `wsOptions` counterpart. */
310
+ /** Channel overrides for the SSE transport, the `wsOptions` counterpart. */
311
311
  sseOptions?: Partial<SseRpcChannelOptions>;
312
312
  rpcOptions?: Partial<BirpcOptions<DevframeRpcServerFunctions, DevframeRpcClientFunctions, boolean>>;
313
313
  cacheOptions?: boolean | Partial<RpcCacheOptions>;
@@ -315,8 +315,8 @@ interface DevframeRpcClientOptions extends SetupDevframeConnectionOptions {
315
315
  * Reject a pending `rpc.call(...)` if the server hasn't answered within this
316
316
  * many milliseconds, with a {@link DevframeConnectionError} of kind
317
317
  * `'timeout'`. Guards against a live-but-unresponsive server hanging the UI.
318
- * Omit (or `0`) to wait indefinitely. Calls always fail fast — regardless of
319
- * this option — once the socket closes or trust is refused.
318
+ * Omit (or `0`) to wait indefinitely. Calls always fail fast, regardless of
319
+ * this option, once the socket closes or trust is refused.
320
320
  */
321
321
  callTimeout?: number;
322
322
  }
@@ -344,7 +344,7 @@ interface DevframeRpcClient {
344
344
  */
345
345
  readonly connectionError: Error | null;
346
346
  /**
347
- * The transport this client is actually connected over — `'websocket'`,
347
+ * The transport this client is actually connected over: `'websocket'`,
348
348
  * `'sse'`, or `'static'`. Reflects the resolution of the `transport`
349
349
  * option against the server's advertisement.
350
350
  */
@@ -404,7 +404,7 @@ interface DevframeRpcClient {
404
404
  sharedState: RpcSharedStateHost;
405
405
  /**
406
406
  * The server's advertised wire services (mirrored `devframe:services`
407
- * shared state) — feature-detect a capability with
407
+ * shared state); feature-detect a capability with
408
408
  * `rpc.services.has('@devframes/service-x')` and get a scoped, typed RPC
409
409
  * handle with `rpc.services.get(...)`. See {@link DevframeServicesClient}.
410
410
  */
@@ -437,11 +437,11 @@ interface DevframeRpcClient {
437
437
  * a `websocket` backend closes the underlying `WebSocket`, which the server observes as a
438
438
  * normal disconnect. Mirrors {@link WsRpcTransport.close} on the server side.
439
439
  *
440
- * There is no corresponding "reconnect" — a closed client is done. Discard it and call
440
+ * There is no corresponding "reconnect"; a closed client is done. Discard it and call
441
441
  * {@link getDevframeRpcClient} again to reconnect.
442
442
  *
443
- * Optional so a `DevframeRpcClientMode` implemented before this method existed — a custom
444
- * transport, a hand-typed mock — still satisfies the interface; an absent `close` is treated
443
+ * Optional so a `DevframeRpcClientMode` implemented before this method existed (a custom
444
+ * transport, a hand-typed mock) still satisfies the interface; an absent `close` is treated
445
445
  * as nothing to close.
446
446
  */
447
447
  close?: () => void;
@@ -449,7 +449,7 @@ interface DevframeRpcClient {
449
449
  interface DevframeRpcClientMode {
450
450
  /**
451
451
  * The transport this mode speaks. Optional so a mode implemented before
452
- * this field existed — a custom transport, a hand-typed mock — still
452
+ * this field existed (a custom transport, a hand-typed mock) still
453
453
  * satisfies the interface; an absent value reads as `'websocket'`.
454
454
  */
455
455
  readonly transport?: 'websocket' | 'sse' | 'static';
@@ -472,7 +472,7 @@ interface DevframeRpcClientMode {
472
472
  }
473
473
  /**
474
474
  * Resolve the requested `transport` option against what the server
475
- * advertises. `'auto'` trusts the advertisement — the server's declared
475
+ * advertises. `'auto'` trusts the advertisement: the server's declared
476
476
  * primary (`backend`), preferring the WebSocket when both endpoints are
477
477
  * present; an explicit `'websocket'` / `'sse'` pins that transport and
478
478
  * throws when the server doesn't advertise it.
@@ -502,7 +502,7 @@ declare function consumeOtpFromUrl(param?: string): string | undefined;
502
502
  *
503
503
  * Higher-level integrations (e.g. Vite DevTools) that want to drive their own
504
504
  * authentication UI can disable `connectDevframe`'s built-in handling with
505
- * `otpParam: false` and call this — or {@link consumeOtpFromUrl} — themselves.
505
+ * `otpParam: false` and call this (or {@link consumeOtpFromUrl}) themselves.
506
506
  */
507
507
  declare function authenticateWithUrlOtp(rpc: Pick<DevframeRpcClient, 'isTrusted' | 'requestTrustWithCode'>, options?: {
508
508
  param?: string;
@@ -523,7 +523,7 @@ interface WsUrlLocation {
523
523
  * The object / relative-path forms connect to the page's own origin (only the
524
524
  * `http`→`ws` protocol swap is applied), resolving the path against where
525
525
  * `__connection.json` was loaded. This is deliberately host-agnostic so the
526
- * connection survives a reverse proxy that changes the domain or port — the
526
+ * connection survives a reverse proxy that changes the domain or port, since the
527
527
  * client trusts its own location, never a server-baked hostname. An explicit
528
528
  * `port`/`host` (or a full `ws(s)://` URL string) opts into a cross-origin
529
529
  * endpoint, e.g. a side-car server on its own port.