@@ -648,6 +648,39 @@ test("renderPlanInBrowser rejects plan when security policy fails", async () =>
648648 ) ;
649649} ) ;
650650
651+ test ( "renderPlanInBrowser binds default runtime network policy to security policy" , async ( ) => {
652+ const plan : RuntimePlan = {
653+ specVersion : DEFAULT_RUNTIME_PLAN_SPEC_VERSION ,
654+ id : "embed_runtime_security_network_policy" ,
655+ version : 1 ,
656+ root : createElementNode ( "section" , undefined , [ createTextNode ( "ok" ) ] ) ,
657+ capabilities : {
658+ domWrite : true ,
659+ } ,
660+ } ;
661+
662+ const result = await renderPlanInBrowser ( plan , {
663+ runtimeOptions : {
664+ browserSourceSandboxMode : "none" ,
665+ remoteFallbackCdnBases : [ "https://esm.sh" ] ,
666+ } ,
667+ securityInitialization : {
668+ profile : "strict" ,
669+ } ,
670+ } ) ;
671+
672+ const runtime = result . runtime as unknown as {
673+ allowArbitraryNetwork ?: boolean ;
674+ allowedNetworkHosts ?: Set < string > ;
675+ } ;
676+
677+ assert . equal ( runtime . allowArbitraryNetwork , false ) ;
678+ assert . deepEqual (
679+ [ ...( runtime . allowedNetworkHosts ?? new Set < string > ( ) ) ] . sort ( ) ,
680+ [ "cdn.jspm.io" , "ga.jspm.io" ] ,
681+ ) ;
682+ } ) ;
683+
651684test ( "renderPlanInBrowser serializes concurrent renders for the same target" , async ( ) => {
652685 const planA : RuntimePlan = {
653686 specVersion : DEFAULT_RUNTIME_PLAN_SPEC_VERSION ,
@@ -719,6 +752,10 @@ test("renderPlanInBrowser serializes concurrent renders for the same target", as
719752
720753 const security = {
721754 initialize : ( ) => { } ,
755+ getPolicy : ( ) => ( {
756+ allowArbitraryNetwork : true ,
757+ allowedNetworkHosts : [ ] ,
758+ } ) ,
722759 checkPlan : ( ) => ( {
723760 safe : true ,
724761 issues : [ ] ,
@@ -1651,6 +1688,80 @@ test("runtime source loader hedges fallback CDN requests", async () => {
16511688 }
16521689} ) ;
16531690
1691+ test ( "runtime source loader skips fallback URLs blocked by network policy" , async ( ) => {
1692+ const runtime = new DefaultRuntimeManager ( {
1693+ remoteFallbackCdnBases : [ "https://esm.sh" ] ,
1694+ remoteFetchRetries : 0 ,
1695+ remoteFetchBackoffMs : 10 ,
1696+ remoteFetchTimeoutMs : 1200 ,
1697+ allowArbitraryNetwork : false ,
1698+ allowedNetworkHosts : [ "ga.jspm.io" , "cdn.jspm.io" ] ,
1699+ } ) ;
1700+
1701+ const diagnostics : Array < { code ?: string ; message ?: string } > = [ ] ;
1702+ const loader = (
1703+ runtime as unknown as {
1704+ createSourceModuleLoader : (
1705+ moduleManifest : RuntimeModuleManifest | undefined ,
1706+ diagnostics : Array < { code ?: string ; message ?: string } > ,
1707+ ) => {
1708+ fetchRemoteModuleCodeWithFallback (
1709+ url : string ,
1710+ ) : Promise < { requestUrl : string } > ;
1711+ } ;
1712+ }
1713+ ) . createSourceModuleLoader ( undefined , diagnostics ) ;
1714+
1715+ const requestedUrls : string [ ] = [ ] ;
1716+ const originalFetch = globalThis . fetch ;
1717+ globalThis . fetch = ( async ( input : RequestInfo | URL ) => {
1718+ const requestUrl = String ( input ) ;
1719+ requestedUrls . push ( requestUrl ) ;
1720+
1721+ if ( requestUrl . startsWith ( "https://ga.jspm.io/" ) ) {
1722+ return new Response ( "slow-failure" , { status : 503 } ) ;
1723+ }
1724+
1725+ if ( requestUrl . startsWith ( "https://esm.sh/" ) ) {
1726+ return new Response ( "export default 1;" , {
1727+ status : 200 ,
1728+ headers : {
1729+ "content-type" : "text/javascript; charset=utf-8" ,
1730+ } ,
1731+ } ) ;
1732+ }
1733+
1734+ return new Response ( "not-found" , { status : 404 } ) ;
1735+ } ) as typeof fetch ;
1736+
1737+ try {
1738+ await assert . rejects (
1739+ loader . fetchRemoteModuleCodeWithFallback (
1740+ "https://ga.jspm.io/npm:lit@3.3.0/index.js" ,
1741+ ) ,
1742+ ) ;
1743+
1744+ assert . ok (
1745+ requestedUrls . some ( ( url ) => url . startsWith ( "https://ga.jspm.io/" ) ) ,
1746+ "expected primary JSPM URL to be requested" ,
1747+ ) ;
1748+ assert . equal (
1749+ requestedUrls . some ( ( url ) => url . startsWith ( "https://esm.sh/" ) ) ,
1750+ false ,
1751+ "did not expect blocked fallback host to be requested" ,
1752+ ) ;
1753+ assert . ok (
1754+ diagnostics . some (
1755+ ( item ) =>
1756+ item . code === "RUNTIME_SOURCE_IMPORT_BLOCKED" &&
1757+ item . message ?. includes ( "https://esm.sh/" ) ,
1758+ ) ,
1759+ ) ;
1760+ } finally {
1761+ globalThis . fetch = originalFetch ;
1762+ }
1763+ } ) ;
1764+
16541765test ( "runtime source loader supports disabling fallback cdn attempts" , async ( ) => {
16551766 const runtime = new DefaultRuntimeManager ( {
16561767 remoteFallbackCdnBases : [ ] ,
0 commit comments