1- import * as http from "node:http" ;
21import { randomBytes } from "node:crypto" ;
3- import { URL } from "node:url" ;
42import * as vscode from "vscode" ;
53
64import { type CoderApi } from "../../api/coderApi" ;
75import { type Logger } from "../../logging/logger" ;
86
97/**
10- * A local reverse proxy that forwards requests to the Coder server.
11- * This exists solely to work around VS Code's webview sandbox which
12- * blocks script execution in nested cross-origin iframes. By serving
13- * through a local proxy the iframe gets its own browsing context and
14- * scripts execute normally.
8+ * Provides a webview that embeds the Coder agent chat UI.
9+ * Authentication flows through postMessage:
1510 *
16- * The proxy does NOT inject auth headers — authentication is handled
17- * entirely via the postMessage bootstrap flow.
18- */
19- class EmbedProxy implements vscode . Disposable {
20- private server ?: http . Server ;
21- private _port = 0 ;
22-
23- constructor (
24- private readonly coderUrl : string ,
25- private readonly logger : Logger ,
26- ) { }
27-
28- get port ( ) : number {
29- return this . _port ;
30- }
31-
32- async start ( ) : Promise < number > {
33- const target = new URL ( this . coderUrl ) ;
34-
35- this . server = http . createServer ( ( req , res ) => {
36- const options : http . RequestOptions = {
37- hostname : target . hostname ,
38- port : target . port || 80 ,
39- path : req . url ,
40- method : req . method ,
41- headers : {
42- ...req . headers ,
43- host : target . host ,
44- } ,
45- } ;
46-
47- const proxyReq = http . request ( options , ( proxyRes ) => {
48- res . writeHead ( proxyRes . statusCode ?? 502 , proxyRes . headers ) ;
49- proxyRes . pipe ( res , { end : true } ) ;
50- } ) ;
51-
52- proxyReq . on ( "error" , ( err ) => {
53- this . logger . warn ( "Embed proxy request error" , err ) ;
54- res . writeHead ( 502 ) ;
55- res . end ( "Bad Gateway" ) ;
56- } ) ;
57-
58- req . pipe ( proxyReq , { end : true } ) ;
59- } ) ;
60-
61- return new Promise < number > ( ( resolve , reject ) => {
62- this . server ! . listen ( 0 , "127.0.0.1" , ( ) => {
63- const addr = this . server ! . address ( ) ;
64- if ( typeof addr === "object" && addr !== null ) {
65- this . _port = addr . port ;
66- this . logger . info (
67- `Embed proxy listening on 127.0.0.1:${ this . _port } ` ,
68- ) ;
69- resolve ( this . _port ) ;
70- } else {
71- reject ( new Error ( "Failed to bind embed proxy" ) ) ;
72- }
73- } ) ;
74- this . server ! . on ( "error" , reject ) ;
75- } ) ;
76- }
77-
78- dispose ( ) : void {
79- this . server ?. close ( ) ;
80- }
81- }
82-
83- /**
84- * Provides a webview that embeds the Coder agent chat UI via a local
85- * proxy. Authentication flows through postMessage:
86- *
87- * 1. The iframe loads /agents/{id}/embed through the proxy.
11+ * 1. The iframe loads /agents/{id}/embed on the Coder server.
8812 * 2. The embed page detects the user is signed out and sends
8913 * { type: "coder:vscode-ready" } to window.parent.
9014 * 3. Our webview relays this to the extension host.
@@ -101,7 +25,6 @@ export class ChatPanelProvider
10125
10226 private view ?: vscode . WebviewView ;
10327 private disposables : vscode . Disposable [ ] = [ ] ;
104- private proxy ?: EmbedProxy ;
10528 private agentId : string | undefined ;
10629
10730 constructor (
@@ -150,23 +73,8 @@ export class ChatPanelProvider
15073 } ) ,
15174 ) ;
15275
153- this . proxy = new EmbedProxy ( coderUrl , this . logger ) ;
154- this . disposables . push ( this . proxy ) ;
155-
156- try {
157- const port = await this . proxy . start ( ) ;
158- const proxyOrigin = `http://127.0.0.1:${ port } ` ;
159- const embedUrl = `${ proxyOrigin } /agents/${ this . agentId } /embed` ;
160- webviewView . webview . html = this . getIframeHtml (
161- embedUrl ,
162- proxyOrigin ,
163- ) ;
164- } catch ( err ) {
165- this . logger . error ( "Failed to start embed proxy" , err ) ;
166- webviewView . webview . html = this . getErrorHtml (
167- "Failed to start embed proxy." ,
168- ) ;
169- }
76+ const embedUrl = `${ coderUrl } /agents/${ this . agentId } /embed` ;
77+ webviewView . webview . html = this . getIframeHtml ( embedUrl , coderUrl ) ;
17078
17179 webviewView . onDidDispose ( ( ) => this . disposeInternals ( ) ) ;
17280 }
@@ -189,36 +97,15 @@ export class ChatPanelProvider
18997
19098 this . disposeInternals ( ) ;
19199
192- this . proxy = new EmbedProxy ( coderUrl , this . logger ) ;
193- this . disposables . push ( this . proxy ) ;
194-
195100 this . disposables . push (
196101 this . view . webview . onDidReceiveMessage ( ( msg : unknown ) => {
197102 this . handleMessage ( msg ) ;
198103 } ) ,
199104 ) ;
200105
201- this . proxy
202- . start ( )
203- . then ( ( port ) => {
204- const proxyOrigin = `http://127.0.0.1:${ port } ` ;
205- const embedUrl = `${ proxyOrigin } /agents/${ this . agentId } /embed` ;
206- if ( this . view ) {
207- this . view . webview . options = { enableScripts : true } ;
208- this . view . webview . html = this . getIframeHtml (
209- embedUrl ,
210- proxyOrigin ,
211- ) ;
212- }
213- } )
214- . catch ( ( err ) => {
215- this . logger . error ( "Failed to restart embed proxy" , err ) ;
216- if ( this . view ) {
217- this . view . webview . html = this . getErrorHtml (
218- "Failed to start embed proxy." ,
219- ) ;
220- }
221- } ) ;
106+ this . view . webview . options = { enableScripts : true } ;
107+ const embedUrl = `${ coderUrl } /agents/${ this . agentId } /embed` ;
108+ this . view . webview . html = this . getIframeHtml ( embedUrl , coderUrl ) ;
222109 }
223110
224111 private handleMessage ( message : unknown ) : void {
@@ -242,7 +129,7 @@ export class ChatPanelProvider
242129 }
243130 }
244131
245- private getIframeHtml ( embedUrl : string , proxyOrigin : string ) : string {
132+ private getIframeHtml ( embedUrl : string , allowedOrigin : string ) : string {
246133 const nonce = randomBytes ( 16 ) . toString ( "base64" ) ;
247134
248135 return /* html */ `<!DOCTYPE html>
@@ -251,7 +138,7 @@ export class ChatPanelProvider
251138 <meta charset="UTF-8">
252139 <meta http-equiv="Content-Security-Policy"
253140 content="default-src 'none';
254- frame-src ${ proxyOrigin } ;
141+ frame-src ${ allowedOrigin } ;
255142 script-src 'nonce-${ nonce } ';
256143 style-src 'unsafe-inline';">
257144 <meta name="viewport" content="width=device-width, initial-scale=1.0">
0 commit comments