sunpeak 0.18.2 → 0.18.7
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/dev.mjs +6 -2
- package/bin/commands/inspect.mjs +405 -5
- package/bin/commands/new.mjs +5 -0
- package/bin/lib/dev-overlay.mjs +50 -0
- package/bin/lib/live/live-config.d.mts +3 -0
- package/bin/lib/live/live-config.mjs +3 -1
- package/bin/lib/sandbox-server.mjs +11 -1
- package/dist/chatgpt/index.cjs +2 -2
- package/dist/chatgpt/index.js +2 -2
- package/dist/claude/index.cjs +1 -1
- package/dist/claude/index.js +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/use-request-teardown.d.ts +21 -0
- package/dist/host/chatgpt/index.cjs +1 -1
- package/dist/host/chatgpt/index.js +1 -1
- package/dist/index.cjs +110 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +68 -31
- package/dist/index.js.map +1 -1
- package/dist/inspector/hosts.d.ts +12 -0
- package/dist/inspector/index.cjs +2 -2
- package/dist/inspector/index.js +2 -2
- package/dist/inspector/inspector-url.d.ts +13 -0
- package/dist/inspector/mcp-app-host.d.ts +3 -0
- package/dist/inspector/simple-sidebar.d.ts +1 -1
- package/dist/inspector/use-inspector-state.d.ts +2 -0
- package/dist/inspector/use-mcp-connection.d.ts +9 -1
- package/dist/{inspector-CByJjmPD.cjs → inspector-CKc58UuI.cjs} +557 -136
- package/dist/inspector-CKc58UuI.cjs.map +1 -0
- package/dist/{inspector-ClhpqKLi.js → inspector-DZrN0kej.js} +556 -135
- package/dist/inspector-DZrN0kej.js.map +1 -0
- package/dist/{inspector-url-7qhtJwY6.cjs → inspector-url-C3LTKgXt.cjs} +3 -1
- package/dist/inspector-url-C3LTKgXt.cjs.map +1 -0
- package/dist/{inspector-url-DuEFmxLP.js → inspector-url-CyQcuBI9.js} +3 -1
- package/dist/inspector-url-CyQcuBI9.js.map +1 -0
- package/dist/mcp/index.cjs +119 -40
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +116 -37
- package/dist/mcp/index.js.map +1 -1
- package/dist/style.css +12 -0
- package/dist/{use-app-X7JbGskk.js → use-app-BNbz1uzj.js} +51 -42
- package/dist/use-app-BNbz1uzj.js.map +1 -0
- package/dist/{use-app-D2h-aiyr.cjs → use-app-Dqh20JPP.cjs} +93 -72
- package/dist/use-app-Dqh20JPP.cjs.map +1 -0
- package/package.json +2 -2
- package/template/dist/albums/albums.html +3 -3
- package/template/dist/albums/albums.json +1 -1
- package/template/dist/carousel/carousel.html +3 -3
- package/template/dist/carousel/carousel.json +1 -1
- package/template/dist/map/map.html +3 -3
- package/template/dist/map/map.json +1 -1
- package/template/dist/review/review.html +3 -3
- package/template/dist/review/review.json +1 -1
- package/template/node_modules/.vite/deps/_metadata.json +3 -3
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +51 -42
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +56 -50
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +52 -43
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/_metadata.json +22 -22
- package/template/tests/e2e/albums.spec.ts +20 -16
- package/template/tests/e2e/carousel.spec.ts +1 -1
- package/template/tests/e2e/dev-overlay.spec.ts +118 -0
- package/template/tests/e2e/helpers.ts +13 -0
- package/template/tests/e2e/map.spec.ts +1 -1
- package/template/tests/e2e/review.spec.ts +1 -1
- package/template/tests/live/albums.spec.ts +10 -0
- package/template/tests/live/playwright.config.ts +1 -1
- package/dist/inspector-CByJjmPD.cjs.map +0 -1
- package/dist/inspector-ClhpqKLi.js.map +0 -1
- package/dist/inspector-url-7qhtJwY6.cjs.map +0 -1
- package/dist/inspector-url-DuEFmxLP.js.map +0 -1
- package/dist/use-app-D2h-aiyr.cjs.map +0 -1
- package/dist/use-app-X7JbGskk.js.map +0 -1
package/bin/commands/dev.mjs
CHANGED
|
@@ -412,6 +412,7 @@ if (!Component) {
|
|
|
412
412
|
if (import.meta.hot) {
|
|
413
413
|
import.meta.hot.accept();
|
|
414
414
|
}
|
|
415
|
+
|
|
415
416
|
`;
|
|
416
417
|
}
|
|
417
418
|
},
|
|
@@ -547,11 +548,14 @@ if (import.meta.hot) {
|
|
|
547
548
|
if (typeof mod.default !== 'function') {
|
|
548
549
|
throw new Error(`Tool "${name}" has no default export handler`);
|
|
549
550
|
}
|
|
551
|
+
const startTime = performance.now();
|
|
550
552
|
const result = await mod.default(args, {});
|
|
553
|
+
const durationMs = Math.round((performance.now() - startTime) * 10) / 10;
|
|
551
554
|
if (typeof result === 'string') {
|
|
552
|
-
return { content: [{ type: 'text', text: result }] };
|
|
555
|
+
return { content: [{ type: 'text', text: result }], _meta: { _sunpeak: { requestTimeMs: durationMs } } };
|
|
553
556
|
}
|
|
554
|
-
|
|
557
|
+
const typed = result ?? {};
|
|
558
|
+
return { ...typed, _meta: { ...typed._meta, _sunpeak: { requestTimeMs: durationMs } } };
|
|
555
559
|
}
|
|
556
560
|
throw new Error(`Tool "${name}" not found`);
|
|
557
561
|
},
|
package/bin/commands/inspect.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const { join, resolve, dirname } = path;
|
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
21
21
|
import { getPort } from '../lib/get-port.mjs';
|
|
22
22
|
import { startSandboxServer } from '../lib/sandbox-server.mjs';
|
|
23
|
+
import { getDevOverlayScript } from '../lib/dev-overlay.mjs';
|
|
23
24
|
|
|
24
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const SUNPEAK_PKG_DIR = resolve(__dirname, '..', '..');
|
|
@@ -76,12 +77,96 @@ Examples:
|
|
|
76
77
|
`);
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Create an in-memory OAuth client provider for the inspector.
|
|
82
|
+
* The provider stores tokens, client info, and code verifier in memory.
|
|
83
|
+
* When `redirectToAuthorization()` is called, it stores the URL for retrieval.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} redirectUrl - The callback URL for OAuth redirects
|
|
86
|
+
* @param {{ clientId?: string, clientSecret?: string }} [opts]
|
|
87
|
+
* @returns {{ provider: import('@modelcontextprotocol/sdk/client/auth.js').OAuthClientProvider, getAuthUrl: () => URL | undefined }}
|
|
88
|
+
*/
|
|
89
|
+
function createInMemoryOAuthProvider(redirectUrl, opts = {}) {
|
|
90
|
+
let _tokens;
|
|
91
|
+
let _clientInfo;
|
|
92
|
+
let _codeVerifier;
|
|
93
|
+
let _authUrl;
|
|
94
|
+
let _discoveryState;
|
|
95
|
+
// Cryptographic state parameter for CSRF protection on the OAuth callback.
|
|
96
|
+
const _stateParam = crypto.randomUUID();
|
|
97
|
+
|
|
98
|
+
// If pre-registered client credentials were provided, seed the client info
|
|
99
|
+
// so the SDK skips dynamic client registration.
|
|
100
|
+
if (opts.clientId) {
|
|
101
|
+
_clientInfo = {
|
|
102
|
+
client_id: opts.clientId,
|
|
103
|
+
...(opts.clientSecret ? { client_secret: opts.clientSecret } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const provider = {
|
|
108
|
+
get redirectUrl() {
|
|
109
|
+
return redirectUrl;
|
|
110
|
+
},
|
|
111
|
+
get clientMetadata() {
|
|
112
|
+
return {
|
|
113
|
+
redirect_uris: [new URL(redirectUrl)],
|
|
114
|
+
client_name: 'sunpeak Inspector',
|
|
115
|
+
token_endpoint_auth_method: opts.clientSecret ? 'client_secret_post' : 'none',
|
|
116
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
117
|
+
response_types: ['code'],
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
// Return the state parameter so the SDK includes it in the authorization URL.
|
|
121
|
+
state() {
|
|
122
|
+
return _stateParam;
|
|
123
|
+
},
|
|
124
|
+
clientInformation() {
|
|
125
|
+
return _clientInfo;
|
|
126
|
+
},
|
|
127
|
+
saveClientInformation(info) {
|
|
128
|
+
_clientInfo = info;
|
|
129
|
+
},
|
|
130
|
+
tokens() {
|
|
131
|
+
return _tokens;
|
|
132
|
+
},
|
|
133
|
+
saveTokens(tokens) {
|
|
134
|
+
_tokens = tokens;
|
|
135
|
+
},
|
|
136
|
+
redirectToAuthorization(url) {
|
|
137
|
+
_authUrl = url;
|
|
138
|
+
},
|
|
139
|
+
saveCodeVerifier(verifier) {
|
|
140
|
+
_codeVerifier = verifier;
|
|
141
|
+
},
|
|
142
|
+
codeVerifier() {
|
|
143
|
+
return _codeVerifier;
|
|
144
|
+
},
|
|
145
|
+
// Cache discovery state so the second auth() call (token exchange)
|
|
146
|
+
// doesn't re-discover metadata from scratch.
|
|
147
|
+
saveDiscoveryState(state) {
|
|
148
|
+
_discoveryState = state;
|
|
149
|
+
},
|
|
150
|
+
discoveryState() {
|
|
151
|
+
return _discoveryState;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
provider,
|
|
157
|
+
getAuthUrl: () => _authUrl,
|
|
158
|
+
hasTokens: () => !!_tokens,
|
|
159
|
+
stateParam: _stateParam,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
79
163
|
/**
|
|
80
164
|
* Create an MCP client connection.
|
|
81
165
|
* @param {string} serverArg - URL or command string
|
|
166
|
+
* @param {{ type?: 'none' | 'bearer' | 'oauth', bearerToken?: string, authProvider?: import('@modelcontextprotocol/sdk/client/auth.js').OAuthClientProvider }} [authConfig]
|
|
82
167
|
* @returns {Promise<{ client: import('@modelcontextprotocol/sdk/client/index.js').Client, transport: import('@modelcontextprotocol/sdk/types.js').Transport }>}
|
|
83
168
|
*/
|
|
84
|
-
async function createMcpConnection(serverArg) {
|
|
169
|
+
async function createMcpConnection(serverArg, authConfig) {
|
|
85
170
|
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
86
171
|
const client = new Client({ name: 'sunpeak-inspector', version: '1.0.0' });
|
|
87
172
|
|
|
@@ -90,7 +175,18 @@ async function createMcpConnection(serverArg) {
|
|
|
90
175
|
const { StreamableHTTPClientTransport } = await import(
|
|
91
176
|
'@modelcontextprotocol/sdk/client/streamableHttp.js'
|
|
92
177
|
);
|
|
93
|
-
|
|
178
|
+
|
|
179
|
+
const transportOpts = {};
|
|
180
|
+
|
|
181
|
+
if (authConfig?.type === 'bearer' && authConfig.bearerToken) {
|
|
182
|
+
transportOpts.requestInit = {
|
|
183
|
+
headers: { Authorization: `Bearer ${authConfig.bearerToken}` },
|
|
184
|
+
};
|
|
185
|
+
} else if (authConfig?.type === 'oauth' && authConfig.authProvider) {
|
|
186
|
+
transportOpts.authProvider = authConfig.authProvider;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const transport = new StreamableHTTPClientTransport(new URL(serverArg), transportOpts);
|
|
94
190
|
await client.connect(transport);
|
|
95
191
|
return { client, transport };
|
|
96
192
|
} else {
|
|
@@ -283,6 +379,16 @@ root.render(
|
|
|
283
379
|
* @param {{ callToolDirect?: (name: string, args: Record<string, unknown>) => Promise<object>, simulationsDir?: string | null }} [pluginOpts]
|
|
284
380
|
*/
|
|
285
381
|
function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
382
|
+
// In-memory OAuth state keyed by server URL, persisted across reconnects.
|
|
383
|
+
/** @type {Map<string, { provider: any, getAuthUrl: () => URL | undefined, hasTokens: () => boolean, stateParam: string }>} */
|
|
384
|
+
const oauthProviders = new Map();
|
|
385
|
+
// Map OAuth state parameter → { serverUrl, oauthState } for CSRF-safe callback matching.
|
|
386
|
+
// Stores a direct reference to the provider that initiated the flow, so even if
|
|
387
|
+
// oauthProviders[serverUrl] is overwritten by a concurrent flow, the callback
|
|
388
|
+
// still completes with the correct provider (which holds the right codeVerifier
|
|
389
|
+
// and clientInformation).
|
|
390
|
+
/** @type {Map<string, { serverUrl: string, oauthState: any }>} */
|
|
391
|
+
const pendingOAuthFlows = new Map();
|
|
286
392
|
return {
|
|
287
393
|
name: 'sunpeak-inspect-endpoints',
|
|
288
394
|
configureServer(server) {
|
|
@@ -421,8 +527,21 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
421
527
|
// Close old connection (best effort)
|
|
422
528
|
try { await getClient().close(); } catch { /* ignore */ }
|
|
423
529
|
|
|
530
|
+
// Build auth config from request
|
|
531
|
+
const authConfig = parsed.auth;
|
|
532
|
+
let connectionAuth;
|
|
533
|
+
if (authConfig?.type === 'bearer' && authConfig.bearerToken) {
|
|
534
|
+
connectionAuth = { type: 'bearer', bearerToken: authConfig.bearerToken };
|
|
535
|
+
} else if (authConfig?.type === 'oauth') {
|
|
536
|
+
// Reuse existing OAuth provider if we have one for this server
|
|
537
|
+
const existing = oauthProviders.get(url);
|
|
538
|
+
if (existing?.hasTokens()) {
|
|
539
|
+
connectionAuth = { type: 'oauth', authProvider: existing.provider };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
424
543
|
// Create new connection
|
|
425
|
-
const newConnection = await createMcpConnection(url);
|
|
544
|
+
const newConnection = await createMcpConnection(url, connectionAuth);
|
|
426
545
|
setClient(newConnection.client);
|
|
427
546
|
|
|
428
547
|
// Discover tools and resources from the new server
|
|
@@ -439,6 +558,273 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
439
558
|
}
|
|
440
559
|
});
|
|
441
560
|
|
|
561
|
+
// ── OAuth flow endpoints ──
|
|
562
|
+
|
|
563
|
+
// Start OAuth: discover metadata, register client, return authorization URL
|
|
564
|
+
server.middlewares.use('/__sunpeak/oauth/start', async (req, res) => {
|
|
565
|
+
if (req.method !== 'POST') {
|
|
566
|
+
res.writeHead(405);
|
|
567
|
+
res.end('Method not allowed');
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const body = await readRequestBody(req);
|
|
572
|
+
let parsed;
|
|
573
|
+
try { parsed = JSON.parse(body); } catch {
|
|
574
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
575
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const { url: serverUrl, scope, clientId, clientSecret } = parsed;
|
|
580
|
+
if (!serverUrl) {
|
|
581
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
582
|
+
res.end(JSON.stringify({ error: 'Missing url' }));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
// Determine callback URL from the Vite server's address
|
|
588
|
+
const addr = server.httpServer?.address();
|
|
589
|
+
const port = typeof addr === 'object' && addr ? addr.port : 3000;
|
|
590
|
+
const callbackUrl = `http://localhost:${port}/__sunpeak/oauth/callback`;
|
|
591
|
+
|
|
592
|
+
// Check if we already have a working provider with tokens for this server.
|
|
593
|
+
// If so, try to connect directly before creating a fresh provider.
|
|
594
|
+
const existingState = oauthProviders.get(serverUrl);
|
|
595
|
+
if (existingState?.hasTokens()) {
|
|
596
|
+
try {
|
|
597
|
+
// Close old connection (best effort)
|
|
598
|
+
try { await getClient().close(); } catch { /* ignore */ }
|
|
599
|
+
|
|
600
|
+
const newConnection = await createMcpConnection(serverUrl, {
|
|
601
|
+
type: 'oauth',
|
|
602
|
+
authProvider: existingState.provider,
|
|
603
|
+
});
|
|
604
|
+
setClient(newConnection.client);
|
|
605
|
+
const simulations = await discoverSimulations(newConnection.client);
|
|
606
|
+
if (pluginOpts.simulationsDir) {
|
|
607
|
+
mergeSimulationFixtures(pluginOpts.simulationsDir, simulations);
|
|
608
|
+
}
|
|
609
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
610
|
+
res.end(JSON.stringify({ status: 'authorized', simulations }));
|
|
611
|
+
return;
|
|
612
|
+
} catch {
|
|
613
|
+
// Tokens may be expired, fall through to fresh auth below
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Always create a fresh provider for an explicit Authorize click.
|
|
618
|
+
// This ensures the user's current credentials (or lack thereof) are
|
|
619
|
+
// used, not stale ones from a previous attempt.
|
|
620
|
+
const oauthState = createInMemoryOAuthProvider(callbackUrl, { clientId, clientSecret });
|
|
621
|
+
oauthProviders.set(serverUrl, oauthState);
|
|
622
|
+
|
|
623
|
+
// Run the SDK auth flow — will call redirectToAuthorization() if needed
|
|
624
|
+
const { auth } = await import('@modelcontextprotocol/sdk/client/auth.js');
|
|
625
|
+
const result = await auth(oauthState.provider, {
|
|
626
|
+
serverUrl,
|
|
627
|
+
scope,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
if (result === 'REDIRECT') {
|
|
631
|
+
const authUrl = oauthState.getAuthUrl();
|
|
632
|
+
if (!authUrl) {
|
|
633
|
+
throw new Error('OAuth flow requested redirect but no authorization URL was generated');
|
|
634
|
+
}
|
|
635
|
+
// Register the state parameter so the callback can find the right provider.
|
|
636
|
+
// Clean up any stale pending flows for the same server URL first
|
|
637
|
+
// (e.g., user closed the popup without completing the previous attempt).
|
|
638
|
+
for (const [key, val] of pendingOAuthFlows) {
|
|
639
|
+
if (val.serverUrl === serverUrl) pendingOAuthFlows.delete(key);
|
|
640
|
+
}
|
|
641
|
+
pendingOAuthFlows.set(oauthState.stateParam, { serverUrl, oauthState });
|
|
642
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
643
|
+
res.end(JSON.stringify({ status: 'redirect', authUrl: authUrl.toString() }));
|
|
644
|
+
} else {
|
|
645
|
+
// AUTHORIZED — tokens were already available (shouldn't normally happen on first call)
|
|
646
|
+
try { await getClient().close(); } catch { /* ignore */ }
|
|
647
|
+
const newConnection = await createMcpConnection(serverUrl, {
|
|
648
|
+
type: 'oauth',
|
|
649
|
+
authProvider: oauthState.provider,
|
|
650
|
+
});
|
|
651
|
+
setClient(newConnection.client);
|
|
652
|
+
const simulations = await discoverSimulations(newConnection.client);
|
|
653
|
+
if (pluginOpts.simulationsDir) {
|
|
654
|
+
mergeSimulationFixtures(pluginOpts.simulationsDir, simulations);
|
|
655
|
+
}
|
|
656
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
657
|
+
res.end(JSON.stringify({ status: 'authorized', simulations }));
|
|
658
|
+
}
|
|
659
|
+
} catch (err) {
|
|
660
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
661
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// OAuth callback: serves an HTML page that sends the code back to the inspector.
|
|
666
|
+
// The state parameter is validated server-side in /__sunpeak/oauth/complete.
|
|
667
|
+
server.middlewares.use('/__sunpeak/oauth/callback', async (req, res) => {
|
|
668
|
+
// Parse code + state from query params
|
|
669
|
+
const reqUrl = new URL(req.url, 'http://localhost');
|
|
670
|
+
const code = reqUrl.searchParams.get('code');
|
|
671
|
+
const state = reqUrl.searchParams.get('state');
|
|
672
|
+
const error = reqUrl.searchParams.get('error');
|
|
673
|
+
const errorDescription = reqUrl.searchParams.get('error_description');
|
|
674
|
+
|
|
675
|
+
// Escape values for safe embedding in <script> — JSON.stringify alone
|
|
676
|
+
// doesn't escape "</script>" sequences which would break out of the tag.
|
|
677
|
+
const safeJson = (val) => JSON.stringify(val).replace(/</g, '\\u003c');
|
|
678
|
+
|
|
679
|
+
const html = `<!DOCTYPE html>
|
|
680
|
+
<html><head><title>OAuth Callback</title></head>
|
|
681
|
+
<body>
|
|
682
|
+
<script>
|
|
683
|
+
(function() {
|
|
684
|
+
var code = ${safeJson(code)};
|
|
685
|
+
var state = ${safeJson(state)};
|
|
686
|
+
var error = ${safeJson(error)};
|
|
687
|
+
var errorDescription = ${safeJson(errorDescription)};
|
|
688
|
+
|
|
689
|
+
// Use our own origin as the postMessage targetOrigin to prevent leaking data cross-origin.
|
|
690
|
+
var origin = location.origin;
|
|
691
|
+
|
|
692
|
+
// Send a message to the opener window. Uses postMessage when window.opener is
|
|
693
|
+
// available, falls back to BroadcastChannel for OAuth providers that set
|
|
694
|
+
// Cross-Origin-Opener-Policy (COOP) which nullifies window.opener.
|
|
695
|
+
function notify(msg) {
|
|
696
|
+
if (window.opener) {
|
|
697
|
+
window.opener.postMessage(msg, origin);
|
|
698
|
+
} else if (typeof BroadcastChannel !== 'undefined') {
|
|
699
|
+
var bc = new BroadcastChannel('sunpeak-oauth');
|
|
700
|
+
bc.postMessage(msg);
|
|
701
|
+
bc.close();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (error) {
|
|
706
|
+
notify({ type: 'sunpeak-oauth-callback', error: error, errorDescription: errorDescription });
|
|
707
|
+
document.body.textContent = 'Authorization failed: ' + (errorDescription || error);
|
|
708
|
+
setTimeout(function() { window.close(); }, 2000);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (!code) {
|
|
713
|
+
document.body.textContent = 'No authorization code received.';
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
document.body.textContent = 'Completing authorization...';
|
|
718
|
+
|
|
719
|
+
// Post the code + state to the server to exchange for tokens.
|
|
720
|
+
// The state is validated server-side to prevent CSRF.
|
|
721
|
+
fetch('/__sunpeak/oauth/complete', {
|
|
722
|
+
method: 'POST',
|
|
723
|
+
headers: { 'Content-Type': 'application/json' },
|
|
724
|
+
body: JSON.stringify({ code: code, state: state })
|
|
725
|
+
})
|
|
726
|
+
.then(function(res) { return res.json(); })
|
|
727
|
+
.then(function(data) {
|
|
728
|
+
if (data.error) {
|
|
729
|
+
notify({ type: 'sunpeak-oauth-callback', error: data.error });
|
|
730
|
+
document.body.textContent = 'Authorization failed: ' + data.error;
|
|
731
|
+
} else {
|
|
732
|
+
notify({ type: 'sunpeak-oauth-callback', success: true, simulations: data.simulations });
|
|
733
|
+
document.body.textContent = 'Authorized! You can close this window.';
|
|
734
|
+
}
|
|
735
|
+
setTimeout(function() { window.close(); }, 1000);
|
|
736
|
+
})
|
|
737
|
+
.catch(function(err) {
|
|
738
|
+
notify({ type: 'sunpeak-oauth-callback', error: err.message });
|
|
739
|
+
document.body.textContent = 'Error: ' + err.message;
|
|
740
|
+
});
|
|
741
|
+
})();
|
|
742
|
+
</script>
|
|
743
|
+
</body></html>`;
|
|
744
|
+
|
|
745
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
746
|
+
res.end(html);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Complete OAuth: exchange authorization code for tokens and connect
|
|
750
|
+
server.middlewares.use('/__sunpeak/oauth/complete', async (req, res) => {
|
|
751
|
+
if (req.method !== 'POST') {
|
|
752
|
+
res.writeHead(405);
|
|
753
|
+
res.end('Method not allowed');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const body = await readRequestBody(req);
|
|
758
|
+
let parsed;
|
|
759
|
+
try { parsed = JSON.parse(body); } catch {
|
|
760
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
761
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const { code, state } = parsed;
|
|
766
|
+
if (!code) {
|
|
767
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
768
|
+
res.end(JSON.stringify({ error: 'Missing authorization code' }));
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (!state) {
|
|
772
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
773
|
+
res.end(JSON.stringify({ error: 'Missing state parameter' }));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Look up the provider via the state parameter (CSRF protection).
|
|
778
|
+
// Uses the direct provider reference from the pending flow, not the
|
|
779
|
+
// oauthProviders map, so concurrent flows for the same server URL
|
|
780
|
+
// don't clobber each other's codeVerifier/clientInformation.
|
|
781
|
+
const pending = pendingOAuthFlows.get(state);
|
|
782
|
+
pendingOAuthFlows.delete(state); // Consume — single-use
|
|
783
|
+
|
|
784
|
+
if (!pending) {
|
|
785
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
786
|
+
res.end(JSON.stringify({ error: 'Invalid or expired OAuth state. Start the flow again.' }));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const { serverUrl, oauthState } = pending;
|
|
791
|
+
|
|
792
|
+
try {
|
|
793
|
+
// Exchange the code for tokens
|
|
794
|
+
const { auth } = await import('@modelcontextprotocol/sdk/client/auth.js');
|
|
795
|
+
const result = await auth(oauthState.provider, {
|
|
796
|
+
serverUrl,
|
|
797
|
+
authorizationCode: code,
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
if (result !== 'AUTHORIZED') {
|
|
801
|
+
throw new Error('Token exchange did not result in authorization');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Store the now-authorized provider so reconnects can reuse tokens.
|
|
805
|
+
oauthProviders.set(serverUrl, oauthState);
|
|
806
|
+
|
|
807
|
+
// Create MCP connection with the authorized provider
|
|
808
|
+
try { await getClient().close(); } catch { /* ignore */ }
|
|
809
|
+
const newConnection = await createMcpConnection(serverUrl, {
|
|
810
|
+
type: 'oauth',
|
|
811
|
+
authProvider: oauthState.provider,
|
|
812
|
+
});
|
|
813
|
+
setClient(newConnection.client);
|
|
814
|
+
|
|
815
|
+
const simulations = await discoverSimulations(newConnection.client);
|
|
816
|
+
if (pluginOpts.simulationsDir) {
|
|
817
|
+
mergeSimulationFixtures(pluginOpts.simulationsDir, simulations);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
821
|
+
res.end(JSON.stringify({ status: 'ok', simulations }));
|
|
822
|
+
} catch (err) {
|
|
823
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
824
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
442
828
|
// Read resource from connected server
|
|
443
829
|
server.middlewares.use('/__sunpeak/read-resource', async (req, res) => {
|
|
444
830
|
const url = new URL(req.url, 'http://localhost');
|
|
@@ -460,9 +846,23 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
460
846
|
}
|
|
461
847
|
|
|
462
848
|
const mimeType = content.mimeType || 'text/html';
|
|
463
|
-
res.writeHead(200, {
|
|
849
|
+
res.writeHead(200, {
|
|
850
|
+
'Content-Type': `${mimeType}; charset=utf-8`,
|
|
851
|
+
'X-Content-Type-Options': 'nosniff',
|
|
852
|
+
});
|
|
464
853
|
if (typeof content.text === 'string') {
|
|
465
|
-
|
|
854
|
+
const stripOverlay = url.searchParams.get('devOverlay') === 'false';
|
|
855
|
+
let text = content.text;
|
|
856
|
+
if (stripOverlay) {
|
|
857
|
+
// Strip dev overlay (e.g., for e2e tests)
|
|
858
|
+
text = text.replace(/<script>(?:(?!<\/script>)[\s\S])*?__sunpeak-dev-timing(?:(?!<\/script>)[\s\S])*?<\/script>/g, '');
|
|
859
|
+
} else if (process.env.SUNPEAK_DEV_OVERLAY !== 'false' && !text.includes('__sunpeak-dev-timing') && text.includes('</body>')) {
|
|
860
|
+
// Inject dev overlay into resources from non-sunpeak servers.
|
|
861
|
+
// The overlay shows resource served timestamp and tool timing (from
|
|
862
|
+
// _meta._sunpeak.requestTimeMs on the PostMessage tool-result notification).
|
|
863
|
+
text = text.replace('</body>', `${getDevOverlayScript(Date.now(), null)}\n</body>`);
|
|
864
|
+
}
|
|
865
|
+
res.end(text);
|
|
466
866
|
} else if (content.blob) {
|
|
467
867
|
res.end(Buffer.from(content.blob, 'base64'));
|
|
468
868
|
} else {
|
package/bin/commands/new.mjs
CHANGED
|
@@ -175,6 +175,11 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
175
175
|
return false;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// Skip framework-internal test files (dev overlay tests are for sunpeak development, not user projects)
|
|
179
|
+
if ((src.includes('/tests/e2e/') || src.includes('/tests/live/')) && name.startsWith('dev-')) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
178
183
|
// Skip deps.json files (build-time metadata, not needed in scaffolded projects)
|
|
179
184
|
if (name === 'deps.json' && src.includes('/resources/')) {
|
|
180
185
|
return false;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate an inline script that shows a dev overlay with resource served timestamp
|
|
3
|
+
* and tool call request timing.
|
|
4
|
+
*
|
|
5
|
+
* The resource timestamp is baked into the HTML at readResource time. Tool timing
|
|
6
|
+
* arrives two ways:
|
|
7
|
+
* 1. Baked-in `toolMs` from readResource time (works when the tool call precedes the
|
|
8
|
+
* resource read, which is the case for Claude and the inspector's initial render).
|
|
9
|
+
* 2. `_meta._sunpeak.requestTimeMs` on the tool result PostMessage (handles inspector
|
|
10
|
+
* Re-run and hosts that pass `_meta` through to the resource iframe).
|
|
11
|
+
*
|
|
12
|
+
* @param {number} servedAt - Unix timestamp (ms) when the resource HTML was generated/served.
|
|
13
|
+
* @param {number | null} toolMs - Most recent tool call duration (ms), or null if no call yet.
|
|
14
|
+
* @returns {string} HTML script tag with the dev overlay.
|
|
15
|
+
*/
|
|
16
|
+
export function getDevOverlayScript(servedAt, toolMs) {
|
|
17
|
+
return `<script>
|
|
18
|
+
(function(){
|
|
19
|
+
var servedAt=${servedAt};
|
|
20
|
+
var el=null,hidden=false,lastMs=${toolMs ?? 'null'};
|
|
21
|
+
function fmt(ts){var d=new Date(ts);var h=d.getHours(),m=d.getMinutes(),s=d.getSeconds();return (h<10?'0':'')+h+':'+(m<10?'0':'')+m+':'+(s<10?'0':'')+s}
|
|
22
|
+
function make(){
|
|
23
|
+
var existing=document.getElementById('__sunpeak-dev-timing');
|
|
24
|
+
if(existing)return existing;
|
|
25
|
+
var b=document.createElement('button');b.id='__sunpeak-dev-timing';
|
|
26
|
+
b.style.cssText='position:fixed;bottom:8px;right:8px;z-index:2147483647;display:grid;grid-template-columns:auto auto;gap:0 6px;align-items:baseline;padding:5px 8px;border-radius:6px;border:1px solid rgba(128,128,128,0.25);background:rgba(0,0,0,0.75);backdrop-filter:blur(8px);color:#e5e5e5;font-size:11px;font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;line-height:1.4;cursor:pointer;user-select:none;opacity:0.85;transition:opacity 150ms;';
|
|
27
|
+
b.onmouseenter=function(){b.style.opacity='1'};
|
|
28
|
+
b.onmouseleave=function(){b.style.opacity='0.85'};
|
|
29
|
+
b.onclick=function(){hidden=!hidden;upd()};
|
|
30
|
+
document.body.appendChild(b);return b;
|
|
31
|
+
}
|
|
32
|
+
function upd(){
|
|
33
|
+
if(!el)el=make();
|
|
34
|
+
if(hidden){el.title='Show dev info';el.innerHTML='<span style="grid-column:1/-1;font-size:9px;text-align:center">DEV</span>';return}
|
|
35
|
+
var h='';
|
|
36
|
+
h+='<span style="text-align:right;color:rgba(255,255,255,0.5);white-space:nowrap">Resource:</span><span style="white-space:nowrap">'+fmt(servedAt)+'</span>';
|
|
37
|
+
if(lastMs!=null)h+='<span style="text-align:right;color:rgba(255,255,255,0.5);white-space:nowrap">Tool:</span><span style="white-space:nowrap">'+(lastMs%1===0?lastMs:lastMs.toFixed(1))+'ms</span>';
|
|
38
|
+
el.title='Hide dev info';el.innerHTML=h;
|
|
39
|
+
}
|
|
40
|
+
upd();
|
|
41
|
+
window.addEventListener('message',function(e){
|
|
42
|
+
var d=e.data;if(!d||typeof d!=='object')return;
|
|
43
|
+
if(d.method!=='ui/notifications/tool-result')return;
|
|
44
|
+
var p=d.params;if(!p)return;
|
|
45
|
+
var ms=p._meta&&p._meta._sunpeak&&p._meta._sunpeak.requestTimeMs;
|
|
46
|
+
if(typeof ms==='number'){lastMs=ms;upd()}
|
|
47
|
+
});
|
|
48
|
+
})();
|
|
49
|
+
</script>`;
|
|
50
|
+
}
|
|
@@ -23,6 +23,9 @@ export interface LiveConfigOptions {
|
|
|
23
23
|
/** Browser permissions to grant (e.g., ['geolocation']). */
|
|
24
24
|
permissions?: string[];
|
|
25
25
|
|
|
26
|
+
/** Show the dev overlay (resource timestamp + tool timing) in resources. Default: true */
|
|
27
|
+
devOverlay?: boolean;
|
|
28
|
+
|
|
26
29
|
/** Additional Playwright `use` options, merged with defaults. */
|
|
27
30
|
use?: Record<string, unknown>;
|
|
28
31
|
}
|
|
@@ -32,6 +32,7 @@ const GLOBAL_SETUP_PATH = join(__dirname, 'global-setup.mjs');
|
|
|
32
32
|
* @param {string} [options.timezoneId] - Timezone (e.g., 'America/New_York')
|
|
33
33
|
* @param {{ latitude: number, longitude: number }} [options.geolocation] - Geolocation coordinates
|
|
34
34
|
* @param {string[]} [options.permissions] - Browser permissions to grant (e.g., ['geolocation'])
|
|
35
|
+
* @param {boolean} [options.devOverlay=true] - Show the dev overlay (resource timestamp + tool timing) in resources
|
|
35
36
|
* @param {Object} [options.use] - Additional Playwright `use` options (merged with defaults)
|
|
36
37
|
*/
|
|
37
38
|
export function createLiveConfig(hostOptions, options = {}) {
|
|
@@ -40,6 +41,7 @@ export function createLiveConfig(hostOptions, options = {}) {
|
|
|
40
41
|
testDir = '.',
|
|
41
42
|
authDir,
|
|
42
43
|
vitePort = getPortSync(3456),
|
|
44
|
+
devOverlay = true,
|
|
43
45
|
colorScheme,
|
|
44
46
|
viewport,
|
|
45
47
|
locale,
|
|
@@ -89,7 +91,7 @@ export function createLiveConfig(hostOptions, options = {}) {
|
|
|
89
91
|
},
|
|
90
92
|
],
|
|
91
93
|
webServer: {
|
|
92
|
-
command: `SUNPEAK_LIVE_TEST=1 SUNPEAK_SANDBOX_PORT=${getPortSync(24680)} pnpm dev -- --prod-resources --port ${vitePort}`,
|
|
94
|
+
command: `SUNPEAK_LIVE_TEST=1 SUNPEAK_SANDBOX_PORT=${getPortSync(24680)}${devOverlay ? '' : ' SUNPEAK_DEV_OVERLAY=false'} pnpm dev -- --prod-resources --port ${vitePort}`,
|
|
93
95
|
url: `http://localhost:${vitePort}/health`,
|
|
94
96
|
reuseExistingServer: !process.env.CI,
|
|
95
97
|
timeout: 60_000,
|
|
@@ -97,7 +97,7 @@ function generateProxyHtml(theme, platform) {
|
|
|
97
97
|
<head>
|
|
98
98
|
<meta name="color-scheme" content="${colorScheme}" />
|
|
99
99
|
<style>
|
|
100
|
-
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
|
|
100
|
+
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: transparent; }
|
|
101
101
|
iframe { border: none; width: 100%; height: 100%; display: block; }
|
|
102
102
|
</style>
|
|
103
103
|
</head>
|
|
@@ -168,6 +168,16 @@ iframe { border: none; width: 100%; height: 100%; display: block; }
|
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// Sync color-scheme on the inner iframe element when theme changes.
|
|
172
|
+
// This ensures prefers-color-scheme resolves correctly inside the app.
|
|
173
|
+
// Important: do NOT set color-scheme on the proxy's own document —
|
|
174
|
+
// changing it from the initial 'dark' causes Chrome to re-evaluate
|
|
175
|
+
// the CSS Canvas as opaque white, blocking the host's conversation
|
|
176
|
+
// background from showing through the transparent proxy.
|
|
177
|
+
if (data.method === 'ui/notifications/host-context-changed' && data.params && data.params.theme) {
|
|
178
|
+
if (innerFrame) innerFrame.style.colorScheme = data.params.theme;
|
|
179
|
+
}
|
|
180
|
+
|
|
171
181
|
// Forward all other messages to the inner iframe
|
|
172
182
|
if (innerWindow) {
|
|
173
183
|
try { innerWindow.postMessage(data, '*'); } catch(e) { /* detached */ }
|
package/dist/chatgpt/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../chunk-9hOWP6kD.cjs");
|
|
3
3
|
require("../protocol-jbxhzcnS.cjs");
|
|
4
|
-
const require_inspector = require("../inspector-
|
|
5
|
-
const require_inspector_url = require("../inspector-url-
|
|
4
|
+
const require_inspector = require("../inspector-CKc58UuI.cjs");
|
|
5
|
+
const require_inspector_url = require("../inspector-url-C3LTKgXt.cjs");
|
|
6
6
|
const require_discovery = require("../discovery-Clu4uHp1.cjs");
|
|
7
7
|
//#region src/chatgpt/index.ts
|
|
8
8
|
var chatgpt_exports = /* @__PURE__ */ require_chunk.__exportAll({
|
package/dist/chatgpt/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { r as __exportAll } from "../chunk-D6g4UhsZ.js";
|
|
2
2
|
import "../protocol-DJmRaBzO.js";
|
|
3
|
-
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-
|
|
4
|
-
import { t as createInspectorUrl } from "../inspector-url-
|
|
3
|
+
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-DZrN0kej.js";
|
|
4
|
+
import { t as createInspectorUrl } from "../inspector-url-CyQcuBI9.js";
|
|
5
5
|
import { c as toPascalCase, i as findResourceKey, n as extractSimulationKey, r as findResourceDirs, s as getComponentName, t as extractResourceKey } from "../discovery-Cgoegt62.js";
|
|
6
6
|
//#region src/chatgpt/index.ts
|
|
7
7
|
var chatgpt_exports = /* @__PURE__ */ __exportAll({
|
package/dist/claude/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
require("../chunk-9hOWP6kD.cjs");
|
|
3
3
|
require("../protocol-jbxhzcnS.cjs");
|
|
4
|
-
const require_inspector = require("../inspector-
|
|
4
|
+
const require_inspector = require("../inspector-CKc58UuI.cjs");
|
|
5
5
|
exports.Inspector = require_inspector.Inspector;
|
package/dist/claude/index.js
CHANGED
package/dist/hooks/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export type { OpenLinkParams } from './use-open-link';
|
|
|
36
36
|
export { useReadServerResource } from './use-read-server-resource';
|
|
37
37
|
export type { ReadServerResourceParams, ReadServerResourceResult, } from './use-read-server-resource';
|
|
38
38
|
export { useRequestDisplayMode } from './use-request-display-mode';
|
|
39
|
+
export { useRequestTeardown } from './use-request-teardown';
|
|
39
40
|
export type { AppDisplayMode } from './use-request-display-mode';
|
|
40
41
|
export { useSendLog } from './use-send-log';
|
|
41
42
|
export type { LogLevel, SendLogParams } from './use-send-log';
|