1- import { defaultQuery , graphiqlTemplate } from './templates/graphiql.js'
21import { unauthorizedTemplate } from './templates/unauthorized.js'
32import express from 'express'
43import bodyParser from 'body-parser'
@@ -9,6 +8,8 @@ import {adminUrl, supportedApiVersions} from '@shopify/cli-kit/node/api/admin'
98import { fetch } from '@shopify/cli-kit/node/http'
109import { renderLiquidTemplate } from '@shopify/cli-kit/node/liquid'
1110import { outputDebug } from '@shopify/cli-kit/node/output'
11+ import { readFile , findPathUp } from '@shopify/cli-kit/node/fs'
12+ import { joinPath , moduleDirectory } from '@shopify/cli-kit/node/path'
1213import { Server } from 'http'
1314import { Writable } from 'stream'
1415import { createRequire } from 'module'
@@ -98,15 +99,14 @@ export function setupGraphiQLServer({
9899 res . send ( 'pong' )
99100 } )
100101
101- const faviconPath = require . resolve ( '@shopify/app/assets/graphiql/favicon.ico' )
102- app . get ( '/graphiql/favicon.ico' , ( _req , res ) => {
103- res . sendFile ( faviconPath )
104- } )
105-
106- const stylePath = require . resolve ( '@shopify/app/assets/graphiql/style.css' )
107- app . get ( '/graphiql/simple.css' , ( _req , res ) => {
108- res . sendFile ( stylePath )
109- } )
102+ // Serve static assets for the React app (JS, CSS, workers)
103+ const graphiqlIndexPath = require . resolve ( '@shopify/app/assets/graphiql/index.html' )
104+ const graphiqlAssetsDir = graphiqlIndexPath . replace ( '/index.html' , '' )
105+ app . use (
106+ '/extensions/graphiql/assets' ,
107+ express . static ( joinPath ( graphiqlAssetsDir , 'extensions' , 'graphiql' , 'assets' ) ) ,
108+ )
109+ app . use ( '/monacoeditorwork' , express . static ( joinPath ( graphiqlAssetsDir , 'monacoeditorwork' ) ) )
110110
111111 async function fetchApiVersionsWithTokenRefresh ( ) : Promise < string [ ] > {
112112 return performActionWithRetryAfterRecovery (
@@ -117,7 +117,14 @@ export function setupGraphiQLServer({
117117
118118 app . get ( '/graphiql/status' , ( _req , res ) => {
119119 fetchApiVersionsWithTokenRefresh ( )
120- . then ( ( ) => res . send ( { status : 'OK' , storeFqdn, appName, appUrl} ) )
120+ . then ( ( ) => {
121+ res . send ( {
122+ status : 'OK' ,
123+ storeFqdn,
124+ appName,
125+ appUrl,
126+ } )
127+ } )
121128 . catch ( ( ) => res . send ( { status : 'UNAUTHENTICATED' } ) )
122129 } )
123130
@@ -127,7 +134,7 @@ export function setupGraphiQLServer({
127134 if ( failIfUnmatchedKey ( req . query . key as string , res ) ) return
128135
129136 const usesHttps = req . protocol === 'https' || req . headers [ 'x-forwarded-proto' ] === 'https'
130- const url = `http${ usesHttps ? 's' : '' } ://${ req . get ( 'host' ) } `
137+ const baseUrl = `http${ usesHttps ? 's' : '' } ://${ req . get ( 'host' ) } `
131138
132139 let apiVersions : string [ ]
133140 try {
@@ -137,41 +144,57 @@ export function setupGraphiQLServer({
137144 return res . send (
138145 await renderLiquidTemplate ( unauthorizedTemplate , {
139146 previewUrl : appUrl ,
140- url,
147+ url : baseUrl ,
141148 } ) ,
142149 )
143150 }
144151 throw err
145152 }
146153
154+ const sortedVersions = apiVersions . sort ( ) . reverse ( )
147155 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148- const apiVersion = apiVersions . sort ( ) . reverse ( ) [ 0 ] !
156+ const apiVersion = sortedVersions [ 0 ] !
149157
150158 function decodeQueryString ( input : string | undefined ) {
151- return input ? decodeURIComponent ( input ) . replace ( / \n / g , '\\n' ) : undefined
159+ return input ? decodeURIComponent ( input ) : undefined
152160 }
153161
154162 const query = decodeQueryString ( req . query . query as string )
155- const variables = decodeQueryString ( req . query . variables as string )
156163
157- res . send (
158- await renderLiquidTemplate (
159- graphiqlTemplate ( {
160- apiVersion,
161- apiVersions : [ ...apiVersions , 'unstable' ] ,
162- appName,
163- appUrl,
164- key,
165- storeFqdn,
166- } ) ,
167- {
168- url,
169- defaultQueries : [ { query : defaultQuery } ] ,
170- query,
171- variables,
172- } ,
173- ) ,
174- )
164+ // Read the built React index.html
165+ const graphiqlAssetsDir = await findPathUp ( joinPath ( 'assets' , 'graphiql' ) , {
166+ type : 'directory' ,
167+ cwd : moduleDirectory ( import . meta. url ) ,
168+ } )
169+
170+ if ( ! graphiqlAssetsDir ) {
171+ return res . status ( 404 ) . send ( 'GraphiQL assets not found' )
172+ }
173+
174+ const indexHtmlPath = joinPath ( graphiqlAssetsDir , 'index.html' )
175+ let indexHtml = await readFile ( indexHtmlPath )
176+
177+ // Build config object to inject (never include apiSecret or tokens)
178+ const config = {
179+ apiVersion,
180+ apiVersions : [ ...apiVersions , 'unstable' ] ,
181+ appName,
182+ appUrl,
183+ storeFqdn,
184+ baseUrl,
185+ key : key ?? undefined ,
186+ query : query ?? undefined ,
187+ }
188+
189+ // Inject config script before </head>
190+ // Escape < > & in JSON to prevent XSS when embedding in HTML script tags
191+ // Use Unicode escapes so JavaScript correctly decodes them (HTML entities would break the query)
192+ const safeJson = JSON . stringify ( config ) . replace ( / < / g, '\\u003c' ) . replace ( / > / g, '\\u003e' ) . replace ( / & / g, '\\u0026' )
193+ const configScript = `<script>window.__GRAPHIQL_CONFIG__ = ${ safeJson } ;</script>`
194+ indexHtml = indexHtml . replace ( '</head>' , `${ configScript } \n </head>` )
195+
196+ res . setHeader ( 'Content-Type' , 'text/html' )
197+ res . send ( indexHtml )
175198 } )
176199
177200 app . use ( bodyParser . json ( ) )
0 commit comments