devframe 0.9.19 → 0.9.20

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.
@@ -1,7 +1,7 @@
1
1
  import { s as colors } from "../nostics-D0PvLZsn.mjs";
2
2
  import { i as resolveDevServerPort, s as importRuntimeModule } from "../_shared-U8D-xzrp.mjs";
3
3
  import { createBuild } from "./build.mjs";
4
- import { t as createDevServer } from "../dev-DbWecpjA.mjs";
4
+ import { t as createDevServer } from "../dev-BdKKOH8S.mjs";
5
5
  import process from "node:process";
6
6
  import cac$1 from "cac";
7
7
  //#region src/adapters/flags.ts
@@ -1,3 +1,3 @@
1
1
  import { i as resolveDevServerPort, o as resolveMcpConnectionMeta } from "../_shared-U8D-xzrp.mjs";
2
- import { t as createDevServer } from "../dev-DbWecpjA.mjs";
2
+ import { t as createDevServer } from "../dev-BdKKOH8S.mjs";
3
3
  export { createDevServer, resolveDevServerPort, resolveMcpConnectionMeta };
@@ -1,2 +1,2 @@
1
- import { n as getInstanceInternals, r as initDevframe } from "../dev-DbWecpjA.mjs";
1
+ import { n as getInstanceInternals, r as initDevframe } from "../dev-BdKKOH8S.mjs";
2
2
  export { getInstanceInternals, initDevframe };
@@ -63,13 +63,26 @@ interface CreateMcpFetchHandlerOptions {
63
63
  */
64
64
  allowedOrigins?: readonly string[] | false;
65
65
  }
66
+ /** Connection facts a host knows about a request beyond the `Request` itself. */
67
+ interface McpConnectionInfo {
68
+ /**
69
+ * The connecting peer's remote address (a node socket's `remoteAddress`),
70
+ * used to prove a same-machine caller when the endpoint relies on the
71
+ * loopback origin default with no identity check. A host that can resolve a
72
+ * trustworthy peer address (the h3/node mount) supplies it; when it's
73
+ * omitted the origin gate stays the only locality signal.
74
+ */
75
+ remoteAddress?: string;
76
+ }
66
77
  interface McpFetchHandler {
67
78
  /**
68
79
  * WHATWG-`fetch` handler for the MCP endpoint. Hand every method
69
80
  * (POST/GET/DELETE) on the endpoint's path to it; routing by path is the
70
- * host's job.
81
+ * host's job. Pass {@link McpConnectionInfo} when the host can resolve the
82
+ * peer address so the default trust boundary can enforce same-machine
83
+ * locality.
71
84
  */
72
- fetch: (request: Request) => Promise<Response>;
85
+ fetch: (request: Request, connection?: McpConnectionInfo) => Promise<Response>;
73
86
  /** Tear down the handler (aborts in-flight exchanges, drops the change bridge). */
74
87
  dispose: () => Promise<void>;
75
88
  }
@@ -89,13 +102,19 @@ interface McpFetchHandler {
89
102
  *
90
103
  * The origin gate guards every request: loopback-default DNS-rebinding
91
104
  * protection that (unlike the WS upgrade's `isAllowedOrigin`) also rejects
92
- * `Origin`-less requests, so a route-based endpoint isn't reachable by a
93
- * browser or a remote host (a disallowed origin gets `403`). It trusts
94
- * same-machine callers by default. When that isn't your trust boundary, add
95
- * an optional identity gate ({@link CreateMcpFetchHandlerOptions.authorization}),
96
- * checked after the origin gate: a bearer/callback check that proves *who* is
97
- * calling (a missing/invalid credential gets `401` with a
98
- * `WWW-Authenticate: Bearer` challenge).
105
+ * `Origin`-less requests, so a browser can't reach the route across origins (a
106
+ * disallowed origin gets `403`). The `Origin` header is only browser hardening:
107
+ * a non-browser client forges it. So on the zero-config default (no widened
108
+ * `allowedOrigins`, no identity check) a second locality gate requires the
109
+ * connected peer to be loopback, proven from the host-supplied
110
+ * {@link McpConnectionInfo.remoteAddress} (which a client cannot forge), so the
111
+ * "trusts same-machine callers" default holds against a remote raw client.
112
+ * When same-machine isn't your trust boundary, add an identity gate
113
+ * ({@link CreateMcpFetchHandlerOptions.authorization}), checked after the
114
+ * origin gate: a bearer/callback check that proves *who* is calling (a
115
+ * missing/invalid credential gets `401` with a `WWW-Authenticate: Bearer`
116
+ * challenge), which also lifts the loopback-peer restriction for authenticated
117
+ * callers.
99
118
  */
100
119
  export declare function createMcpFetchHandler(ctx: DevframeNodeContext, options: CreateMcpFetchHandlerOptions): McpFetchHandler;
101
120
  //#endregion
@@ -120,4 +139,4 @@ interface MountedMcpHttp {
120
139
  */
121
140
  export declare function mountMcpHttp(app: H3, ctx: DevframeNodeContext, path: string, options: MountMcpHttpOptions): MountedMcpHttp;
122
141
  //#endregion
123
- export type { CreateMcpFetchHandlerOptions, CreateMcpServerOptions, McpFetchHandler, McpServerHandle, MountMcpHttpOptions, MountedMcpHttp };
142
+ export type { CreateMcpFetchHandlerOptions, CreateMcpServerOptions, McpConnectionInfo, McpFetchHandler, McpServerHandle, MountMcpHttpOptions, MountedMcpHttp };
@@ -1,6 +1,6 @@
1
1
  import { t as Diagnostic } from "../nostics-D0PvLZsn.mjs";
2
2
  import { t as DEVFRAME_EVENTS } from "../events-BV4Dj59a.mjs";
3
- import { t as isAllowedOrigin } from "../origin-DZit9g29.mjs";
3
+ import { n as isLoopbackAddress, t as isAllowedOrigin } from "../origin-D_eBKEzr.mjs";
4
4
  import { r as timingSafeEqual } from "../crypto-token-TVbGGFuj.mjs";
5
5
  import { t as diagnostics } from "../diagnostics-DZA6KQ_Z.mjs";
6
6
  import { t as createHostContext } from "../context-BLXXjoBW.mjs";
@@ -8,7 +8,7 @@ import { t as toAgentToolName } from "../agent-tool-name-DGzeuWcO.mjs";
8
8
  import { join } from "pathe";
9
9
  import process from "node:process";
10
10
  import { homedir } from "node:os";
11
- import { defineHandler } from "h3";
11
+ import { defineHandler, getRequestIP } from "h3";
12
12
  import { Server, createMcpHandler } from "@modelcontextprotocol/server";
13
13
  //#region src/adapters/mcp/stringify.ts
14
14
  /**
@@ -512,13 +512,19 @@ async function isAuthorized(req, authorization) {
512
512
  *
513
513
  * The origin gate guards every request: loopback-default DNS-rebinding
514
514
  * protection that (unlike the WS upgrade's `isAllowedOrigin`) also rejects
515
- * `Origin`-less requests, so a route-based endpoint isn't reachable by a
516
- * browser or a remote host (a disallowed origin gets `403`). It trusts
517
- * same-machine callers by default. When that isn't your trust boundary, add
518
- * an optional identity gate ({@link CreateMcpFetchHandlerOptions.authorization}),
519
- * checked after the origin gate: a bearer/callback check that proves *who* is
520
- * calling (a missing/invalid credential gets `401` with a
521
- * `WWW-Authenticate: Bearer` challenge).
515
+ * `Origin`-less requests, so a browser can't reach the route across origins (a
516
+ * disallowed origin gets `403`). The `Origin` header is only browser hardening:
517
+ * a non-browser client forges it. So on the zero-config default (no widened
518
+ * `allowedOrigins`, no identity check) a second locality gate requires the
519
+ * connected peer to be loopback, proven from the host-supplied
520
+ * {@link McpConnectionInfo.remoteAddress} (which a client cannot forge), so the
521
+ * "trusts same-machine callers" default holds against a remote raw client.
522
+ * When same-machine isn't your trust boundary, add an identity gate
523
+ * ({@link CreateMcpFetchHandlerOptions.authorization}), checked after the
524
+ * origin gate: a bearer/callback check that proves *who* is calling (a
525
+ * missing/invalid credential gets `401` with a `WWW-Authenticate: Bearer`
526
+ * challenge), which also lifts the loopback-peer restriction for authenticated
527
+ * callers.
522
528
  */
523
529
  function createMcpFetchHandler(ctx, options) {
524
530
  const allowedOrigins = options.allowedOrigins;
@@ -536,9 +542,11 @@ function createMcpFetchHandler(ctx, options) {
536
542
  handler.notify.resourcesChanged();
537
543
  }
538
544
  });
539
- async function handle(req) {
545
+ const originOnlyDefault = allowedOrigins === void 0 && authorization === false;
546
+ async function handle(req, connection) {
540
547
  const origin = req.headers.get("origin") ?? void 0;
541
548
  if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden", { status: 403 });
549
+ if (originOnlyDefault && connection?.remoteAddress !== void 0 && !isLoopbackAddress(connection.remoteAddress)) return new Response("Forbidden", { status: 403 });
542
550
  if (!await isAuthorized(req, authorization)) return new Response("Unauthorized", {
543
551
  status: 401,
544
552
  headers: { "WWW-Authenticate": "Bearer" }
@@ -570,7 +578,7 @@ function createMcpFetchHandler(ctx, options) {
570
578
  */
571
579
  function mountMcpHttp(app, ctx, path, options) {
572
580
  const handler = createMcpFetchHandler(ctx, options);
573
- app.use(path, defineHandler(async (event) => respond(event, await handler.fetch(event.req))));
581
+ app.use(path, defineHandler(async (event) => respond(event, await handler.fetch(event.req, { remoteAddress: getRequestIP(event) }))));
574
582
  return { dispose: handler.dispose };
575
583
  }
576
584
  /**
@@ -5,7 +5,7 @@ import { t as createHostContext } from "./context-BLXXjoBW.mjs";
5
5
  import { t as resolveStaticAssetsSource } from "./remote-assets-D0StJVLp.mjs";
6
6
  import { a as resolveMcpConfig, i as resolveDevServerPort, n as normalizeBasePath, r as resolveBasePath, s as importRuntimeModule, t as loadAutoMcpAdapter } from "./_shared-U8D-xzrp.mjs";
7
7
  import { t as createH3DevframeHost } from "./host-h3-BROI6SQ4.mjs";
8
- import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, t as createInstanceShell } from "./instance-shell-370Oee9K.mjs";
8
+ import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, t as createInstanceShell } from "./instance-shell-yfrM3_vK.mjs";
9
9
  import { open } from "./utils/open.mjs";
10
10
  import { mountStaticHandler } from "./utils/serve-static.mjs";
11
11
  import { resolve } from "pathe";
@@ -1,5 +1,5 @@
1
1
  import "./constants.mjs";
2
- import { r as validateOriginCandidate } from "./origin-DZit9g29.mjs";
2
+ import { i as validateOriginCandidate } from "./origin-D_eBKEzr.mjs";
3
3
  import { t as diagnostics } from "./diagnostics-DZA6KQ_Z.mjs";
4
4
  import { t as getInternalContext } from "./context-jXO5vzjH.mjs";
5
5
  import { createInteractiveAuth } from "./recipes/interactive-auth.mjs";
@@ -5,6 +5,6 @@ import { n as coerceAgentPositionalArgs, r as toolInputToCommandArgs, t as Devfr
5
5
  import { a as resolveMcpConfig, n as normalizeBasePath, r as resolveBasePath, s as importRuntimeModule, t as loadAutoMcpAdapter } from "../_shared-U8D-xzrp.mjs";
6
6
  import { t as createH3DevframeHost } from "../host-h3-BROI6SQ4.mjs";
7
7
  import { i as registerDevframeInstance, n as listLiveDevframeInstances } from "../instance-registry-DCq762YJ.mjs";
8
- import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, r as samePath, t as createInstanceShell } from "../instance-shell-370Oee9K.mjs";
8
+ import { i as normalizeHttpServerUrl, n as resolveInstanceRegister, r as samePath, t as createInstanceShell } from "../instance-shell-yfrM3_vK.mjs";
9
9
  import { t as createContextRpcServer } from "../rpc-core-CvkU23bQ.mjs";
10
10
  export { DevframeAgentHost, coerceAgentPositionalArgs, createContextRpcServer, createH3DevframeHost, createInstanceShell, createRpcWireCodec, diagnostics, importRuntimeModule, listLiveDevframeInstances, loadAutoMcpAdapter, normalizeBasePath, normalizeHttpServerUrl, peekRpcWireFrame, registerDevframeInstance, resolveBasePath, resolveClientAssets, resolveInstanceRegister, resolveMcpConfig, samePath, toolInputToCommandArgs };
@@ -22,6 +22,19 @@
22
22
  * those DNS names.
23
23
  */
24
24
  declare function isLoopbackHostname(hostname: string): boolean;
25
+ /**
26
+ * Whether `address` is a loopback peer address as reported by a socket
27
+ * (`net.Socket.remoteAddress`): the IPv6 loopback `::1`, an IPv4 literal in
28
+ * `127.0.0.0/8`, or an IPv4-mapped IPv6 form of one (`::ffff:127.0.0.1`).
29
+ *
30
+ * Unlike {@link isLoopbackHostname} this takes a raw address, not a hostname:
31
+ * it never accepts a `localhost`-style name (a socket peer is always a literal
32
+ * address) and understands the IPv4-mapped IPv6 form the OS hands back on a
33
+ * dual-stack listener. Used to prove a same-machine caller from the connected
34
+ * peer, which a client cannot forge, rather than from the `Origin` header,
35
+ * which it can.
36
+ */
37
+ declare function isLoopbackAddress(address: string): boolean;
25
38
  /**
26
39
  * Default origin policy for a localhost dev tool: allow requests with no
27
40
  * `Origin` header (native, non-browser clients), allow any loopback origin
@@ -42,4 +55,4 @@ declare function isAllowedOrigin(origin: string | undefined, allowedOrigins: rea
42
55
  */
43
56
  declare function validateOriginCandidate(candidate: string, allowedOrigins?: readonly string[]): string | undefined;
44
57
  //#endregion
45
- export { isLoopbackHostname as n, validateOriginCandidate as r, isAllowedOrigin as t };
58
+ export { validateOriginCandidate as i, isLoopbackAddress as n, isLoopbackHostname as r, isAllowedOrigin as t };
@@ -26,6 +26,26 @@ function isLoopbackHostname(hostname) {
26
26
  if (h === "localhost" || h.endsWith(".localhost") || h === "::1") return true;
27
27
  return isLoopbackIPv4(h);
28
28
  }
29
+ /**
30
+ * Whether `address` is a loopback peer address as reported by a socket
31
+ * (`net.Socket.remoteAddress`): the IPv6 loopback `::1`, an IPv4 literal in
32
+ * `127.0.0.0/8`, or an IPv4-mapped IPv6 form of one (`::ffff:127.0.0.1`).
33
+ *
34
+ * Unlike {@link isLoopbackHostname} this takes a raw address, not a hostname:
35
+ * it never accepts a `localhost`-style name (a socket peer is always a literal
36
+ * address) and understands the IPv4-mapped IPv6 form the OS hands back on a
37
+ * dual-stack listener. Used to prove a same-machine caller from the connected
38
+ * peer, which a client cannot forge, rather than from the `Origin` header,
39
+ * which it can.
40
+ */
41
+ function isLoopbackAddress(address) {
42
+ let h = address.trim().replace(/^\[|\]$/g, "");
43
+ const zone = h.indexOf("%");
44
+ if (zone !== -1) h = h.slice(0, zone);
45
+ if (h === "::1") return true;
46
+ const mapped = /^::ffff:(.+)$/i.exec(h);
47
+ return isLoopbackIPv4(mapped ? mapped[1] : h);
48
+ }
29
49
  /** A canonical dotted-decimal IPv4 literal in `127.0.0.0/8`. */
30
50
  function isLoopbackIPv4(hostname) {
31
51
  const octets = hostname.split(".");
@@ -77,4 +97,4 @@ function validateOriginCandidate(candidate, allowedOrigins) {
77
97
  if (isLoopbackHostname(url.hostname) || allowedOrigins?.includes(canonical)) return canonical;
78
98
  }
79
99
  //#endregion
80
- export { isLoopbackHostname as n, validateOriginCandidate as r, isAllowedOrigin as t };
100
+ export { validateOriginCandidate as i, isLoopbackAddress as n, isLoopbackHostname as r, isAllowedOrigin as t };
@@ -1,6 +1,6 @@
1
1
  import { DEVFRAME_SSE_SESSION_HEADER } from "../../constants.mjs";
2
2
  import { n as peekRpcWireFrame, t as createRpcWireCodec } from "../../wire-codec-B1W3zH3L.mjs";
3
- import { t as isAllowedOrigin } from "../../origin-DZit9g29.mjs";
3
+ import { t as isAllowedOrigin } from "../../origin-D_eBKEzr.mjs";
4
4
  import { t as createRpcSessionMeta } from "../../session-DPfj6zQG.mjs";
5
5
  //#region src/rpc/transports/sse-server.ts
6
6
  const SSE_STREAM_HEADERS = {
@@ -1,4 +1,4 @@
1
- import { t as isAllowedOrigin } from "../../origin-DZit9g29.mjs";
1
+ import { t as isAllowedOrigin } from "../../origin-D_eBKEzr.mjs";
2
2
  import { createWsRpcPeerHooks } from "./ws-server.mjs";
3
3
  //#region src/rpc/transports/ws-bun.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as isAllowedOrigin } from "../../origin-DZit9g29.mjs";
1
+ import { t as isAllowedOrigin } from "../../origin-D_eBKEzr.mjs";
2
2
  import { createWsRpcPeerHooks } from "./ws-server.mjs";
3
3
  //#region src/rpc/transports/ws-deno.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
1
  import { a as DevframeWsPeer, i as DevframeRpcTransportKind, n as DevframeRpcConnection, r as DevframeRpcConnectionRequest, t as DevframeNodeRpcSessionMeta } from "../../session-r6PDixP6.mjs";
2
- import { n as isLoopbackHostname, t as isAllowedOrigin } from "../../origin-DGDH8BI9.mjs";
2
+ import { r as isLoopbackHostname, t as isAllowedOrigin } from "../../origin-CY6BBHOy.mjs";
3
3
  import { a as attachWsRpcTransport, i as WsRpcTransportOptions, n as WsOriginRegistry, o as createWsOriginRegistry, r as WsRpcTransport, s as createWsRpcPeerHooks, t as CreateWsOriginRegistryOptions } from "../../ws-server-BB8jhxeT.mjs";
4
4
  export { CreateWsOriginRegistryOptions, type DevframeNodeRpcSessionMeta, type DevframeRpcConnection, type DevframeRpcConnectionRequest, type DevframeRpcTransportKind, type DevframeWsPeer, WsOriginRegistry, WsRpcTransport, WsRpcTransportOptions, attachWsRpcTransport, createWsOriginRegistry, createWsRpcPeerHooks, isAllowedOrigin, isLoopbackHostname };
@@ -1,6 +1,6 @@
1
1
  import "../../constants.mjs";
2
2
  import { t as createRpcWireCodec } from "../../wire-codec-B1W3zH3L.mjs";
3
- import { n as isLoopbackHostname, t as isAllowedOrigin } from "../../origin-DZit9g29.mjs";
3
+ import { r as isLoopbackHostname, t as isAllowedOrigin } from "../../origin-D_eBKEzr.mjs";
4
4
  import { t as createRpcSessionMeta } from "../../session-DPfj6zQG.mjs";
5
5
  import { n as randomToken, r as timingSafeEqual } from "../../crypto-token-TVbGGFuj.mjs";
6
6
  import { createServer } from "node:http";
@@ -1,2 +1,2 @@
1
- import { n as isLoopbackHostname, r as validateOriginCandidate, t as isAllowedOrigin } from "../origin-DGDH8BI9.mjs";
2
- export { isAllowedOrigin, isLoopbackHostname, validateOriginCandidate };
1
+ import { i as validateOriginCandidate, n as isLoopbackAddress, r as isLoopbackHostname, t as isAllowedOrigin } from "../origin-CY6BBHOy.mjs";
2
+ export { isAllowedOrigin, isLoopbackAddress, isLoopbackHostname, validateOriginCandidate };
@@ -26,6 +26,26 @@ function isLoopbackHostname(hostname) {
26
26
  if (h === "localhost" || h.endsWith(".localhost") || h === "::1") return true;
27
27
  return isLoopbackIPv4(h);
28
28
  }
29
+ /**
30
+ * Whether `address` is a loopback peer address as reported by a socket
31
+ * (`net.Socket.remoteAddress`): the IPv6 loopback `::1`, an IPv4 literal in
32
+ * `127.0.0.0/8`, or an IPv4-mapped IPv6 form of one (`::ffff:127.0.0.1`).
33
+ *
34
+ * Unlike {@link isLoopbackHostname} this takes a raw address, not a hostname:
35
+ * it never accepts a `localhost`-style name (a socket peer is always a literal
36
+ * address) and understands the IPv4-mapped IPv6 form the OS hands back on a
37
+ * dual-stack listener. Used to prove a same-machine caller from the connected
38
+ * peer, which a client cannot forge, rather than from the `Origin` header,
39
+ * which it can.
40
+ */
41
+ function isLoopbackAddress(address) {
42
+ let h = address.trim().replace(/^\[|\]$/g, "");
43
+ const zone = h.indexOf("%");
44
+ if (zone !== -1) h = h.slice(0, zone);
45
+ if (h === "::1") return true;
46
+ const mapped = /^::ffff:(.+)$/i.exec(h);
47
+ return isLoopbackIPv4(mapped ? mapped[1] : h);
48
+ }
29
49
  /** A canonical dotted-decimal IPv4 literal in `127.0.0.0/8`. */
30
50
  function isLoopbackIPv4(hostname) {
31
51
  const octets = hostname.split(".");
@@ -77,4 +97,4 @@ function validateOriginCandidate(candidate, allowedOrigins) {
77
97
  if (isLoopbackHostname(url.hostname) || allowedOrigins?.includes(canonical)) return canonical;
78
98
  }
79
99
  //#endregion
80
- export { isAllowedOrigin, isLoopbackHostname, validateOriginCandidate };
100
+ export { isAllowedOrigin, isLoopbackAddress, isLoopbackHostname, validateOriginCandidate };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devframe",
3
3
  "type": "module",
4
- "version": "0.9.19",
4
+ "version": "0.9.20",
5
5
  "description": "Framework for building one portable devtool integration that runs in any viewer.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",