Skip to content

Commit 9b31961

Browse files
committed
feat(graphiql): integrate console with app server
Updates the app server to serve the new GraphiQL console with secure config injection. Removes old template-based implementation. Server changes: - Serve built React app from packages/app/assets/graphiql - Inject runtime config with Unicode escaping for XSS prevention - Use \u003c, \u003e, \u0026 to prevent script tag breakout - Support query parameter passing Cleanup: - Remove old template implementation (365 lines) - Remove old CSS styles (58 lines) Server integration complete with security built-in
1 parent de0599e commit 9b31961

File tree

3 files changed

+58
-400
lines changed

3 files changed

+58
-400
lines changed

packages/app/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"sourceRoot": "packages/app/src",
55
"projectType": "library",
66
"tags": ["scope:feature"],
7-
"implicitDependencies": ["ui-extensions-dev-console"],
7+
"implicitDependencies": ["ui-extensions-dev-console", "graphiql-console"],
88
"targets": {
99
"clean": {
1010
"executor": "nx:run-commands",

packages/app/src/cli/services/dev/graphiql/server.ts

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {defaultQuery, graphiqlTemplate} from './templates/graphiql.js'
21
import {unauthorizedTemplate} from './templates/unauthorized.js'
32
import express from 'express'
43
import bodyParser from 'body-parser'
@@ -9,6 +8,8 @@ import {adminUrl, supportedApiVersions} from '@shopify/cli-kit/node/api/admin'
98
import {fetch} from '@shopify/cli-kit/node/http'
109
import {renderLiquidTemplate} from '@shopify/cli-kit/node/liquid'
1110
import {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'
1213
import {Server} from 'http'
1314
import {Writable} from 'stream'
1415
import {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

Comments
 (0)