[object Object]

← back to Shopify Oauth Handler

initial scaffold: one-shot Shopify OAuth callback handler for FA 5-11-26

21cc3ff31aabc0cc270fc4358768881df2231ea5 · 2026-05-11 09:26:33 -0700 · SteveStudio2

Files touched

Diff

commit 21cc3ff31aabc0cc270fc4358768881df2231ea5
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 11 09:26:33 2026 -0700

    initial scaffold: one-shot Shopify OAuth callback handler for FA 5-11-26
---
 .gitignore |  6 +++++
 server.js  | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..07c2c14
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+.env*
+token.txt
+scopes.txt
+*.log
+.DS_Store
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..11af344
--- /dev/null
+++ b/server.js
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+/**
+ * One-shot Shopify OAuth callback handler for the FA 5-11-26 Partner app.
+ * Listens on :9895, receives the install redirect, exchanges code for an
+ * admin API access token, writes it to ./token.txt, then exits.
+ *
+ * Run:   CLIENT_ID=... CLIENT_SECRET=... node server.js
+ */
+const http = require('http');
+const fs = require('fs');
+const { URL } = require('url');
+
+const PORT = 9895;
+const CLIENT_ID = process.env.CLIENT_ID;
+const CLIENT_SECRET = process.env.CLIENT_SECRET;
+const EXPECT_SHOP = 'designer-laboratory-sandbox.myshopify.com';
+
+if (!CLIENT_ID || !CLIENT_SECRET) {
+  console.error('FATAL: CLIENT_ID and CLIENT_SECRET env vars required');
+  process.exit(1);
+}
+
+const server = http.createServer(async (req, res) => {
+  const u = new URL(req.url, `http://localhost:${PORT}`);
+  if (u.pathname !== '/oauth/callback') {
+    res.writeHead(404).end('not found');
+    return;
+  }
+  const code = u.searchParams.get('code');
+  const shop = u.searchParams.get('shop');
+  console.log(`[oauth] callback hit: shop=${shop} code=${code ? code.slice(0,8)+'...' : '(none)'}`);
+  if (!code || !shop) {
+    res.writeHead(400).end('missing code or shop param');
+    return;
+  }
+  if (shop !== EXPECT_SHOP) {
+    res.writeHead(400).end(`unexpected shop: ${shop}`);
+    return;
+  }
+  try {
+    const r = await fetch(`https://${shop}/admin/oauth/access_token`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        client_id: CLIENT_ID,
+        client_secret: CLIENT_SECRET,
+        code
+      })
+    });
+    const j = await r.json();
+    if (!j.access_token) {
+      console.error('[oauth] no access_token in response:', JSON.stringify(j));
+      res.writeHead(500).end('token exchange failed: ' + JSON.stringify(j));
+      return;
+    }
+    fs.writeFileSync(__dirname + '/token.txt', j.access_token + '\n', { mode: 0o600 });
+    fs.writeFileSync(__dirname + '/scopes.txt', (j.scope || '') + '\n', { mode: 0o600 });
+    console.log(`[oauth] SUCCESS — token written (prefix ${j.access_token.slice(0,14)}..., scopes=${j.scope})`);
+    res.writeHead(200, { 'Content-Type': 'text/html' }).end(
+      `<html><body style="font:18px system-ui;padding:40px;background:#0d1117;color:#7ee787">
+       <h1>✓ Token received</h1>
+       <p>Scopes: <code>${j.scope}</code></p>
+       <p>Token written to <code>token.txt</code> (last 6: <code>...${j.access_token.slice(-6)}</code>).</p>
+       <p>You can close this tab. Claude will pick it up.</p>
+       </body></html>`
+    );
+    setTimeout(() => process.exit(0), 500);
+  } catch (e) {
+    console.error('[oauth] exchange error:', e);
+    res.writeHead(500).end('exchange error: ' + e.message);
+  }
+});
+
+server.listen(PORT, '127.0.0.1', () => {
+  console.log(`[oauth] listening on http://localhost:${PORT}/oauth/callback`);
+  console.log(`[oauth] expected shop: ${EXPECT_SHOP}`);
+  console.log(`[oauth] client_id: ${CLIENT_ID.slice(0,8)}...`);
+});

(oldest)  ·  back to Shopify Oauth Handler  ·  gitignore: broaden to standing-rules set (dist/build/.next/t d7165da →