sunpeak 0.20.18 → 0.20.19
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.
- package/bin/commands/inspect.mjs +112 -15
- package/bin/lib/sandbox-server.mjs +8 -2
- package/dist/chatgpt/index.cjs +1 -1
- package/dist/chatgpt/index.js +1 -1
- package/dist/claude/index.cjs +1 -1
- package/dist/claude/index.js +1 -1
- package/dist/index.cjs +2 -3321
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -3320
- package/dist/index.js.map +1 -1
- package/dist/inspector/index.cjs +1 -1
- package/dist/inspector/index.js +1 -1
- package/dist/{inspector-Dnxjqhu3.cjs → inspector-BLOY4Qj6.cjs} +3359 -6
- package/dist/inspector-BLOY4Qj6.cjs.map +1 -0
- package/dist/{inspector-l1Eo18H7.js → inspector-DMdmJlDA.js} +3354 -7
- package/dist/inspector-DMdmJlDA.js.map +1 -0
- package/dist/lib/utils.d.ts +11 -0
- package/package.json +1 -1
- package/template/dist/albums/albums.json +1 -1
- package/template/dist/carousel/carousel.json +1 -1
- package/template/dist/map/map.json +1 -1
- package/template/dist/review/review.json +1 -1
- package/dist/inspector-Dnxjqhu3.cjs.map +0 -1
- package/dist/inspector-l1Eo18H7.js.map +0 -1
package/bin/commands/inspect.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
const { existsSync, readdirSync, readFileSync } = fs;
|
|
19
|
-
const { join, resolve, dirname } = path;
|
|
19
|
+
const { join, resolve, dirname, sep } = path;
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
21
21
|
import { createServer as createHttpServer } from 'http';
|
|
22
22
|
import { getPort } from '../lib/get-port.mjs';
|
|
@@ -716,11 +716,40 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
716
716
|
// and clientInformation).
|
|
717
717
|
/** @type {Map<string, { serverUrl: string, oauthState: any }>} */
|
|
718
718
|
const pendingOAuthFlows = new Map();
|
|
719
|
+
/**
|
|
720
|
+
* Reject requests where the Origin header doesn't match the Host header.
|
|
721
|
+
* This blocks browser-issued cross-origin requests (CSRF) and DNS rebinding
|
|
722
|
+
* attacks that would otherwise reach the privileged /__sunpeak/* endpoints.
|
|
723
|
+
* Requests without an Origin header (curl, Node fetch without origin) are
|
|
724
|
+
* allowed because they cannot be triggered cross-origin from a browser.
|
|
725
|
+
* @param {import('http').IncomingMessage} req
|
|
726
|
+
* @param {import('http').ServerResponse} res
|
|
727
|
+
*/
|
|
728
|
+
function requireSameOrigin(req, res) {
|
|
729
|
+
const origin = req.headers.origin;
|
|
730
|
+
if (!origin) return true;
|
|
731
|
+
let originHost;
|
|
732
|
+
try {
|
|
733
|
+
originHost = new URL(origin).host;
|
|
734
|
+
} catch {
|
|
735
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
736
|
+
res.end(JSON.stringify({ error: 'Forbidden: invalid Origin header' }));
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
if (originHost !== req.headers.host) {
|
|
740
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
741
|
+
res.end(JSON.stringify({ error: 'Forbidden: cross-origin request blocked' }));
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
|
|
719
747
|
return {
|
|
720
748
|
name: 'sunpeak-inspect-endpoints',
|
|
721
749
|
configureServer(server) {
|
|
722
750
|
// List tools from connected server (with automatic session recovery)
|
|
723
|
-
server.middlewares.use('/__sunpeak/list-tools', async (
|
|
751
|
+
server.middlewares.use('/__sunpeak/list-tools', async (req, res) => {
|
|
752
|
+
if (!requireSameOrigin(req, res)) return;
|
|
724
753
|
try {
|
|
725
754
|
const client = getClient();
|
|
726
755
|
const result = await client.listTools();
|
|
@@ -742,7 +771,8 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
742
771
|
});
|
|
743
772
|
|
|
744
773
|
// List resources from connected server
|
|
745
|
-
server.middlewares.use('/__sunpeak/list-resources', async (
|
|
774
|
+
server.middlewares.use('/__sunpeak/list-resources', async (req, res) => {
|
|
775
|
+
if (!requireSameOrigin(req, res)) return;
|
|
746
776
|
try {
|
|
747
777
|
const client = getClient();
|
|
748
778
|
const result = await client.listResources();
|
|
@@ -757,6 +787,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
757
787
|
|
|
758
788
|
// Call tool on connected server
|
|
759
789
|
server.middlewares.use('/__sunpeak/call-tool', async (req, res) => {
|
|
790
|
+
if (!requireSameOrigin(req, res)) return;
|
|
760
791
|
if (req.method !== 'POST') {
|
|
761
792
|
res.writeHead(405);
|
|
762
793
|
res.end('Method not allowed');
|
|
@@ -804,6 +835,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
804
835
|
// Used by the Prod Tools Run button so the real handler executes even
|
|
805
836
|
// when the MCP server would return simulation fixture data.
|
|
806
837
|
server.middlewares.use('/__sunpeak/call-tool-direct', async (req, res) => {
|
|
838
|
+
if (!requireSameOrigin(req, res)) return;
|
|
807
839
|
if (req.method !== 'POST') {
|
|
808
840
|
res.writeHead(405);
|
|
809
841
|
res.end('Method not allowed');
|
|
@@ -852,6 +884,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
852
884
|
// Reconnect to a new MCP server URL.
|
|
853
885
|
// Creates a new MCP client connection and replaces the current one.
|
|
854
886
|
server.middlewares.use('/__sunpeak/connect', async (req, res) => {
|
|
887
|
+
if (!requireSameOrigin(req, res)) return;
|
|
855
888
|
if (req.method !== 'POST') {
|
|
856
889
|
res.writeHead(405);
|
|
857
890
|
res.end('Method not allowed');
|
|
@@ -883,6 +916,17 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
883
916
|
return;
|
|
884
917
|
}
|
|
885
918
|
|
|
919
|
+
// Only http(s) URLs are accepted via the HTTP endpoint. Stdio servers
|
|
920
|
+
// (which spawn child processes) are reachable only by the CLI caller of
|
|
921
|
+
// `inspectServer()`, never by an HTTP client — otherwise a malicious
|
|
922
|
+
// page or untrusted app iframe could trigger arbitrary command
|
|
923
|
+
// execution via this endpoint.
|
|
924
|
+
if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) {
|
|
925
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
926
|
+
res.end(JSON.stringify({ error: 'Only http(s) URLs are allowed' }));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
886
930
|
try {
|
|
887
931
|
// Close old connection (best effort)
|
|
888
932
|
try { await getClient().close(); } catch { /* ignore */ }
|
|
@@ -922,6 +966,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
922
966
|
|
|
923
967
|
// Start OAuth: discover metadata, register client, return authorization URL
|
|
924
968
|
server.middlewares.use('/__sunpeak/oauth/start', async (req, res) => {
|
|
969
|
+
if (!requireSameOrigin(req, res)) return;
|
|
925
970
|
if (req.method !== 'POST') {
|
|
926
971
|
res.writeHead(405);
|
|
927
972
|
res.end('Method not allowed');
|
|
@@ -1108,6 +1153,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
1108
1153
|
|
|
1109
1154
|
// Complete OAuth: exchange authorization code for tokens and connect
|
|
1110
1155
|
server.middlewares.use('/__sunpeak/oauth/complete', async (req, res) => {
|
|
1156
|
+
if (!requireSameOrigin(req, res)) return;
|
|
1111
1157
|
if (req.method !== 'POST') {
|
|
1112
1158
|
res.writeHead(405);
|
|
1113
1159
|
res.end('Method not allowed');
|
|
@@ -1187,6 +1233,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
1187
1233
|
|
|
1188
1234
|
// Read resource from connected server
|
|
1189
1235
|
server.middlewares.use('/__sunpeak/read-resource', async (req, res) => {
|
|
1236
|
+
if (!requireSameOrigin(req, res)) return;
|
|
1190
1237
|
const url = new URL(req.url, 'http://localhost');
|
|
1191
1238
|
const uri = url.searchParams.get('uri');
|
|
1192
1239
|
if (!uri) {
|
|
@@ -1205,7 +1252,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
1205
1252
|
return;
|
|
1206
1253
|
}
|
|
1207
1254
|
|
|
1208
|
-
const mimeType = content.mimeType
|
|
1255
|
+
const mimeType = sanitizeMimeType(content.mimeType);
|
|
1209
1256
|
res.writeHead(200, {
|
|
1210
1257
|
'Content-Type': `${mimeType}; charset=utf-8`,
|
|
1211
1258
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -1235,7 +1282,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
1235
1282
|
const retryResult = await getClient().readResource({ uri });
|
|
1236
1283
|
const retryContent = retryResult.contents?.[0];
|
|
1237
1284
|
if (retryContent) {
|
|
1238
|
-
const mimeType = retryContent.mimeType
|
|
1285
|
+
const mimeType = sanitizeMimeType(retryContent.mimeType);
|
|
1239
1286
|
res.writeHead(200, {
|
|
1240
1287
|
'Content-Type': `${mimeType}; charset=utf-8`,
|
|
1241
1288
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -1253,6 +1300,42 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
1253
1300
|
};
|
|
1254
1301
|
}
|
|
1255
1302
|
|
|
1303
|
+
/**
|
|
1304
|
+
* Parse the SUNPEAK_ALLOWED_HOSTS env var into a value Vite accepts for its
|
|
1305
|
+
* `server.allowedHosts` option. Empty/undefined → use Vite's default
|
|
1306
|
+
* (localhost loopback only). The literal string "all" maps to Vite's
|
|
1307
|
+
* "allow everything" mode, which disables DNS-rebinding protection.
|
|
1308
|
+
* Otherwise the value is split on commas and trimmed.
|
|
1309
|
+
*
|
|
1310
|
+
* @param {string | undefined} raw
|
|
1311
|
+
*/
|
|
1312
|
+
function parseAllowedHosts(raw) {
|
|
1313
|
+
if (!raw) return undefined;
|
|
1314
|
+
const trimmed = raw.trim();
|
|
1315
|
+
if (!trimmed) return undefined;
|
|
1316
|
+
if (trimmed === 'all') return 'all';
|
|
1317
|
+
return trimmed.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Validate and normalize a Content-Type value supplied by the upstream MCP
|
|
1322
|
+
* server. The mimeType is reflected back into our HTTP response, so a
|
|
1323
|
+
* malformed or unexpected value would let an attacker influence how callers
|
|
1324
|
+
* interpret the response (e.g. force `text/html` rendering of opaque blobs).
|
|
1325
|
+
*
|
|
1326
|
+
* Accepts simple `type/subtype` shapes only (RFC 7231 token chars).
|
|
1327
|
+
* Anything else falls back to `text/html`, which is the protocol's documented
|
|
1328
|
+
* default mime type for resources that omit one.
|
|
1329
|
+
*
|
|
1330
|
+
* @param {unknown} mimeType
|
|
1331
|
+
*/
|
|
1332
|
+
function sanitizeMimeType(mimeType) {
|
|
1333
|
+
if (typeof mimeType !== 'string' || mimeType.length === 0) return 'text/html';
|
|
1334
|
+
// RFC 7231 token chars, no parameters/whitespace allowed here.
|
|
1335
|
+
if (!/^[\w.+-]+\/[\w.+-]+$/.test(mimeType)) return 'text/html';
|
|
1336
|
+
return mimeType;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1256
1339
|
/**
|
|
1257
1340
|
* Read the full body of an HTTP request.
|
|
1258
1341
|
*/
|
|
@@ -1493,9 +1576,21 @@ export async function inspectServer(opts) {
|
|
|
1493
1576
|
...(projectRoot ? [{
|
|
1494
1577
|
name: 'sunpeak-dist-serve',
|
|
1495
1578
|
configureServer(server) {
|
|
1579
|
+
const distRoot = resolve(projectRoot, 'dist');
|
|
1496
1580
|
server.middlewares.use((req, res, next) => {
|
|
1497
1581
|
if (!req.url?.startsWith('/dist/') || !req.url.endsWith('.html')) return next();
|
|
1498
|
-
|
|
1582
|
+
// Strip query/hash before joining to avoid `?` or `#` confusing path parsers.
|
|
1583
|
+
const pathOnly = req.url.split('?')[0].split('#')[0];
|
|
1584
|
+
// Resolve the target path and require it to stay inside `<projectRoot>/dist`.
|
|
1585
|
+
// Without this, a request like `/dist/../../etc/anything.html` would resolve
|
|
1586
|
+
// outside the project and serve arbitrary readable files as HTML.
|
|
1587
|
+
const filePath = resolve(projectRoot, pathOnly.replace(/^\/+/, ''));
|
|
1588
|
+
const distRootWithSep = distRoot.endsWith(sep) ? distRoot : distRoot + sep;
|
|
1589
|
+
if (filePath !== distRoot && !filePath.startsWith(distRootWithSep)) {
|
|
1590
|
+
res.writeHead(403);
|
|
1591
|
+
res.end('Forbidden');
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1499
1594
|
if (existsSync(filePath)) {
|
|
1500
1595
|
const content = readFileSync(filePath, 'utf-8');
|
|
1501
1596
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -1574,15 +1669,17 @@ export async function inspectServer(opts) {
|
|
|
1574
1669
|
// ERR_CONNECTION_REFUSED. When auto-discovered via getPort(), the port is
|
|
1575
1670
|
// already free so this doesn't apply.
|
|
1576
1671
|
...(explicitPort ? { strictPort: true } : {}),
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
host: '
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
|
|
1672
|
+
// Bind to 127.0.0.1 by default so the inspector is not reachable from the
|
|
1673
|
+
// LAN. The /__sunpeak/* endpoints can call the connected MCP server, so
|
|
1674
|
+
// exposing them on 0.0.0.0 lets any device on the same network drive the
|
|
1675
|
+
// developer's tools. Set SUNPEAK_HOST=0.0.0.0 (or another address) to opt in.
|
|
1676
|
+
host: process.env.SUNPEAK_HOST || '127.0.0.1',
|
|
1677
|
+
// Vite's DNS-rebinding protection rejects requests whose Host header
|
|
1678
|
+
// isn't in this allowlist, which closes the residual rebinding attack
|
|
1679
|
+
// even when the server is bound to 0.0.0.0. Set SUNPEAK_ALLOWED_HOSTS
|
|
1680
|
+
// (comma-separated, or "all") to allow tunnels, containers, or custom
|
|
1681
|
+
// /etc/hosts entries.
|
|
1682
|
+
allowedHosts: parseAllowedHosts(process.env.SUNPEAK_ALLOWED_HOSTS),
|
|
1586
1683
|
open: open ?? (!process.env.CI && !process.env.SUNPEAK_LIVE_TEST),
|
|
1587
1684
|
},
|
|
1588
1685
|
optimizeDeps: {
|
|
@@ -81,8 +81,14 @@ export async function startSandboxServer({ preferredPort = 24680 } = {}) {
|
|
|
81
81
|
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
// Bind to loopback by default so the proxy server is not reachable from the
|
|
85
|
+
// LAN. The proxy serves iframe HTML that brokers PostMessage between host
|
|
86
|
+
// and app — exposing it on 0.0.0.0 lets any LAN device serve crafted
|
|
87
|
+
// postMessage relays into the developer's browser. Override with
|
|
88
|
+
// SUNPEAK_HOST when LAN access is intentional.
|
|
89
|
+
const bindHost = process.env.SUNPEAK_HOST || '127.0.0.1';
|
|
84
90
|
await new Promise((resolve, reject) => {
|
|
85
|
-
server.listen(port, () => resolve());
|
|
91
|
+
server.listen(port, bindHost, () => resolve());
|
|
86
92
|
server.on('error', reject);
|
|
87
93
|
});
|
|
88
94
|
|
|
@@ -342,6 +348,6 @@ const MOCK_OPENAI_SCRIPT = [
|
|
|
342
348
|
'requestDisplayMode:function(p){console.log("[Inspector] requestDisplayMode:",p.mode);',
|
|
343
349
|
'return Promise.resolve()},',
|
|
344
350
|
'sendFollowUpMessage:function(p){console.log("[Inspector] sendFollowUpMessage:",p.prompt)},',
|
|
345
|
-
'openExternal:function(p){console.log("[Inspector] openExternal:",p.href);window.open(p.href,"_blank")}',
|
|
351
|
+
'openExternal:function(p){console.log("[Inspector] openExternal:",p.href);try{var u=new URL(p.href);if(u.protocol!=="http:"&&u.protocol!=="https:"){console.warn("[Inspector] openExternal blocked non-http(s) URL:",p.href);return}window.open(p.href,"_blank","noopener,noreferrer")}catch(e){console.warn("[Inspector] openExternal blocked invalid URL:",p.href)}}',
|
|
346
352
|
'};',
|
|
347
353
|
].join('');
|
package/dist/chatgpt/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../chunk-CoPdw6nB.cjs");
|
|
3
|
-
const require_inspector = require("../inspector-
|
|
3
|
+
const require_inspector = require("../inspector-BLOY4Qj6.cjs");
|
|
4
4
|
const require_inspector_url = require("../inspector-url-CCgv8H74.cjs");
|
|
5
5
|
const require_discovery = require("../discovery-C9fQVb1u.cjs");
|
|
6
6
|
//#region src/chatgpt/index.ts
|
package/dist/chatgpt/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { I as __exportAll } from "../v4-COy4jjxX.js";
|
|
2
|
-
import { _ as McpAppHost, d as ThemeProvider, f as useThemeContext, g as extractResourceCSP, h as IframeResource, n as resolveServerToolResult, t as Inspector, v as SCREEN_WIDTHS } from "../inspector-
|
|
2
|
+
import { _ as McpAppHost, d as ThemeProvider, f as useThemeContext, g as extractResourceCSP, h as IframeResource, n as resolveServerToolResult, t as Inspector, v as SCREEN_WIDTHS } from "../inspector-DMdmJlDA.js";
|
|
3
3
|
import { t as createInspectorUrl } from "../inspector-url-CyQcuBI9.js";
|
|
4
4
|
import { c as toPascalCase, i as findResourceKey, n as extractSimulationKey, r as findResourceDirs, s as getComponentName, t as extractResourceKey } from "../discovery-Cgoegt62.js";
|
|
5
5
|
//#region src/chatgpt/index.ts
|
package/dist/claude/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
require("../chunk-CoPdw6nB.cjs");
|
|
3
|
-
const require_inspector = require("../inspector-
|
|
3
|
+
const require_inspector = require("../inspector-BLOY4Qj6.cjs");
|
|
4
4
|
exports.Inspector = require_inspector.Inspector;
|
package/dist/claude/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as Inspector } from "../inspector-
|
|
1
|
+
import { t as Inspector } from "../inspector-DMdmJlDA.js";
|
|
2
2
|
export { Inspector };
|