@@ -5,6 +5,7 @@ import { join } from 'node:path'
55import { defineDevframe } from 'devframe'
66import { createRpcClient } from 'devframe/rpc/client'
77import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client'
8+ import { createWsOriginRegistry } from 'devframe/rpc/transports/ws-server'
89import { open } from 'devframe/utils/open'
910import { getPort } from 'get-port-please'
1011import { describe , expect , it , vi } from 'vitest'
@@ -31,6 +32,19 @@ function makeTmpDist(): string {
3132 return dir
3233}
3334
35+ async function connectRaw ( url : string , origin ?: string ) : Promise < 'open' | 'closed' > {
36+ return await new Promise ( ( resolve ) => {
37+ const ws = new WebSocket ( url , origin ? { headers : { origin } } : undefined )
38+ ws . on ( 'open' , ( ) => {
39+ ws . close ( )
40+ resolve ( 'open' )
41+ } )
42+ ws . on ( 'error' , ( ) => resolve ( 'closed' ) )
43+ ws . on ( 'unexpected-response' , ( ) => resolve ( 'closed' ) )
44+ ws . on ( 'close' , ( ) => resolve ( 'closed' ) )
45+ } )
46+ }
47+
3448describe ( 'adapters/dev' , ( ) => {
3549 it ( 'createDevServer starts, exposes __connection.json, and closes' , async ( ) => {
3650 const distDir = makeTmpDist ( )
@@ -256,6 +270,99 @@ describe('adapters/dev', () => {
256270 }
257271 } )
258272
273+ it ( 'allowedOrigins: default stays loopback-only' , async ( ) => {
274+ const devframe = defineDevframe ( {
275+ id : 'devframe-origins-default' ,
276+ name : 'Origins Default' ,
277+ version : '0.0.0' ,
278+ packageName : 'devframe-test' ,
279+ homepage : 'https://example.test' ,
280+ description : 'Test devframe.' ,
281+ setup : ( ) => { } ,
282+ } )
283+ const host = '127.0.0.1'
284+ const port = await getPort ( { port : 19450 , host } )
285+ const handle = await createDevServer ( devframe , { host, port, openBrowser : false } )
286+
287+ try {
288+ await expect ( connectRaw ( `ws://${ host } :${ port } /__ws` ) ) . resolves . toBe ( 'open' )
289+ await expect ( connectRaw ( `ws://${ host } :${ port } /__ws` , `http://${ host } :12345` ) ) . resolves . toBe ( 'open' )
290+ await expect ( connectRaw ( `ws://${ host } :${ port } /__ws` , 'http://evil.example' ) ) . resolves . toBe ( 'closed' )
291+ }
292+ finally {
293+ await handle . close ( )
294+ }
295+ } )
296+
297+ it ( 'allowedOrigins: an array of extra origins is honored on top of the loopback default' , async ( ) => {
298+ const devframe = defineDevframe ( {
299+ id : 'devframe-origins-array' ,
300+ name : 'Origins Array' ,
301+ version : '0.0.0' ,
302+ packageName : 'devframe-test' ,
303+ homepage : 'https://example.test' ,
304+ description : 'Test devframe.' ,
305+ setup : ( ) => { } ,
306+ } )
307+ const host = '127.0.0.1'
308+ const port = await getPort ( { port : 19440 , host } )
309+ const handle = await createDevServer ( devframe , {
310+ host,
311+ port,
312+ openBrowser : false ,
313+ allowedOrigins : [ 'http://evil.example' ] ,
314+ } )
315+
316+ try {
317+ await expect ( connectRaw ( `ws://${ host } :${ port } /__ws` , 'http://evil.example' ) ) . resolves . toBe ( 'open' )
318+ // Still rejects an origin that's neither loopback nor allowlisted.
319+ await expect ( connectRaw ( `ws://${ host } :${ port } /__ws` , 'http://other.example' ) ) . resolves . toBe ( 'closed' )
320+ }
321+ finally {
322+ await handle . close ( )
323+ }
324+ } )
325+
326+ it ( 'allowedOrigins: a WsOriginRegistry gates the upgrade end-to-end' , async ( ) => {
327+ const devframe = defineDevframe ( {
328+ id : 'devframe-origins-registry' ,
329+ name : 'Origins Registry' ,
330+ version : '0.0.0' ,
331+ packageName : 'devframe-test' ,
332+ homepage : 'https://example.test' ,
333+ description : 'Test devframe.' ,
334+ setup : ( ) => { } ,
335+ } )
336+ const host = '127.0.0.1'
337+ const port = await getPort ( { port : 19430 , host } )
338+ const origin = 'chrome-extension://abcdefghijklmnop'
339+ const registry = createWsOriginRegistry ( {
340+ validateOrigin : value => value . startsWith ( 'chrome-extension://' ) ,
341+ } )
342+ const handle = await createDevServer ( devframe , {
343+ host,
344+ port,
345+ openBrowser : false ,
346+ allowedOrigins : registry ,
347+ } )
348+
349+ try {
350+ // Unregistered, the registry rejects the same origin it'll accept below.
351+ await expect ( connectRaw ( `ws://${ host } :${ port } /__ws` , origin ) ) . resolves . toBe ( 'closed' )
352+
353+ const params = new URLSearchParams ( {
354+ devframe_viewer_origin : origin ,
355+ devframe_viewer_origin_token : registry . token ,
356+ } )
357+ expect ( registry . registerFromUrl ( `/__connection.json?${ params } ` ) ) . toBe ( origin )
358+
359+ await expect ( connectRaw ( `ws://${ host } :${ port } /__ws` , origin ) ) . resolves . toBe ( 'open' )
360+ }
361+ finally {
362+ await handle . close ( )
363+ }
364+ } )
365+
259366 it ( 'createDevServer runs in bridge mode when no distDir is configured' , async ( ) => {
260367 const devframe = defineDevframe ( {
261368 id : 'devframe-test-nodist' ,
0 commit comments