Skip to content

Proto: shopify store execute#7122

Draft
dmerand wants to merge 1 commit intomainfrom
store-execute
Draft

Proto: shopify store execute#7122
dmerand wants to merge 1 commit intomainfrom
store-execute

Conversation

@dmerand
Copy link
Copy Markdown
Contributor

@dmerand dmerand commented Mar 27, 2026

What

Add shopify store auth and shopify store execute so Shopify CLI can authenticate an app against a store and then run Admin GraphQL without a local app project. Keep the store execute flags aligned with shopify app execute, require --allow-mutations for writes, keep hidden --mock support for demos, and add a project-local Claude skill that routes merchant-style store tasks into this flow.

Why

This demo is meant to show a clear app-authenticated store workflow in Shopify CLI: authenticate once for a store, verify access, and then run task-specific Admin API operations. That makes it easier to demo merchant-style tasks like adding products or updating inventory through the CLI.

How

Add shopify store auth to run a standard Shopify app OAuth flow against a store, request required scopes, open a localhost callback, exchange the code using a client secret file, and store an online access token locally.

Update shopify store execute to use the stored app session and tell the user to run shopify store auth when the session is missing, expired, or lacks scopes.

Keep mutations blocked unless --allow-mutations is passed.

Keep --mock so the command can still be demoed without auth or network access.

Add .claude/skills/manage-store-with-cli so Claude can steer merchant-style requests into store auth, a safe verification query, and then the task-specific store execute command.

For the final version, replace the current demo-oriented client-secret-file exchange with PKCE.

Refresh generated docs, README output, and the oclif manifest for the new command set.

Testing

  • Allow http://localhost:3458/auth/callback in the demo app redirect settings
  • printf '%s' "$SHOPIFY_STORE_CLIENT_SECRET" > /tmp/shopify-store-client-secret.txt
  • pnpm run shopify store auth --store shop.myshopify.com --scopes read_products,write_products --client-secret-file /tmp/shopify-store-client-secret.txt
  • pnpm run shopify store execute --store shop.myshopify.com --query 'query { shop { name id } }'
  • pnpm run shopify store execute --store shop.myshopify.com --query 'mutation { shop { id } }' should fail until --allow-mutations is added
  • pnpm run shopify store execute --store shop.myshopify.com --query 'query { shop { name } }' --mock
  • Open Claude in this repo and ask merchant-style prompts like:
    • I want to add a draft product to my store with Shopify CLI
    • How do I update inventory with Shopify CLI?
    • How do I tag products in my store from Shopify CLI?
  • Verify the manage-store-with-cli skill loads
  • Verify Claude routes to pnpm run shopify store auth first, then a safe pnpm run shopify store execute --query 'query { shop { name id } }', and only then the task-specific command
  • Verify Claude asks for or uses the store domain, required scopes, and /tmp/shopify-store-client-secret.txt, and does not route to pnpm run shopify app execute

Copy link
Copy Markdown
Contributor Author

dmerand commented Mar 27, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 82.35% 15352/18642
🟡 Branches 74.75% 7530/10074
🟢 Functions 81.48% 3853/4729
🟢 Lines 82.75% 14517/17543

Test suite run success

4032 tests passing in 1541 suites.

Report generated by 🧪jest coverage report action from 7aae5d3

@dmerand
Copy link
Copy Markdown
Contributor Author

dmerand commented Mar 27, 2026

/snapit

@github-actions
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @dmerand! Your snapshot has been published to npm.

Test the snapshot by installing your package globally:

npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260327162821

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

@dmerand dmerand force-pushed the store-execute branch 2 times, most recently from e15c53b to 893af23 Compare March 27, 2026 16:59
Copy link
Copy Markdown
Contributor Author

dmerand commented Mar 27, 2026

/snapit

@github-actions
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @dmerand! Your snapshot has been published to npm.

Test the snapshot by installing your package globally:

npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260327203223

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

const fail = (message: string) => {
res.statusCode = 400
res.setHeader('Content-Type', 'text/html')
res.end(`<html><body><h1>Authentication failed</h1><p>${message}</p></body></html>`)

Check failure

Code scanning / CodeQL

Reflected cross-site scripting

Cross-site scripting vulnerability due to a [user-provided value](1).

Copilot Autofix

AI about 19 hours ago

In general, to fix reflected XSS when writing user input into HTML, you must apply appropriate HTML-encoding/escaping to any untrusted data before interpolating it into the response. This converts characters like <, >, ", ', and & into their HTML entity equivalents so they are rendered as text instead of being interpreted as HTML/JavaScript.

The minimal, best fix here is to ensure that the message passed to fail is HTML-escaped before interpolation into the response body. Since most fail call sites use fixed strings, we only need to sanitize the one value coming from searchParams.get('error'). The safest change that preserves existing behavior is:

  • Introduce a small HTML-escaping helper inside this file (since we don’t see any existing HTML-escape utility imported).
  • Use this helper only when building the message that includes the error parameter: change fail(\Shopify returned an OAuth error: ${error}`)tofail(`Shopify returned an OAuth error: ${escapeHtml(error)}`). This keeps the general structure of the code intact and only encodes potentially unsafe characters coming from user input. All other failmessages remain unchanged. No external dependencies are strictly required; we can define a simple localescapeHtml` function that handles standard dangerous characters.

Concretely:

  • In packages/cli/src/cli/services/store/auth.ts, inside the shown region, add a small escapeHtml function (for example just above the createServer callback or near the top of the file code you control).
  • Modify the if (error) { ... } block so that error is escaped before being included in the HTML string. This ensures that even if error contains script tags or HTML, the browser will display them as text, eliminating the XSS vector.
Suggested changeset 1
packages/cli/src/cli/services/store/auth.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/cli/src/cli/services/store/auth.ts b/packages/cli/src/cli/services/store/auth.ts
--- a/packages/cli/src/cli/services/store/auth.ts
+++ b/packages/cli/src/cli/services/store/auth.ts
@@ -102,6 +102,14 @@
       settleWithError(new AbortError('Timed out waiting for OAuth callback.'))
     }, timeoutMs)
 
+    const escapeHtml = (value: string): string =>
+      value
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;')
+        .replace(/'/g, '&#39;')
+
     const server = createServer((req, res) => {
       const requestUrl = new URL(req.url ?? '/', `http://localhost:${port}`)
 
@@ -138,7 +146,7 @@
 
       const error = searchParams.get('error')
       if (error) {
-        fail(`Shopify returned an OAuth error: ${error}`)
+        fail(`Shopify returned an OAuth error: ${escapeHtml(error)}`)
         return
       }
 
EOF
@@ -102,6 +102,14 @@
settleWithError(new AbortError('Timed out waiting for OAuth callback.'))
}, timeoutMs)

const escapeHtml = (value: string): string =>
value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')

const server = createServer((req, res) => {
const requestUrl = new URL(req.url ?? '/', `http://localhost:${port}`)

@@ -138,7 +146,7 @@

const error = searchParams.get('error')
if (error) {
fail(`Shopify returned an OAuth error: ${error}`)
fail(`Shopify returned an OAuth error: ${escapeHtml(error)}`)
return
}

Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants