@hanzo/shopify 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Hanzo AI for Shopify</title>
7
+ <!--
8
+ App Bridge is loaded from Shopify's CDN and MUST be the first script, with
9
+ the app's API key in a meta tag. Shopify requires this for an embedded admin
10
+ app so the iframe registers window.shopify (session tokens, navigation).
11
+ build.js stamps __SHOPIFY_API_KEY__ (public — the client id, never the secret)
12
+ and app.js / __BASE__.
13
+ -->
14
+ <meta name="shopify-api-key" content="__SHOPIFY_API_KEY__" />
15
+ <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
16
+ <meta name="hanzo:base" content="" />
17
+ </head>
18
+ <body>
19
+ <div id="root"></div>
20
+ <script type="module" src="app.js"></script>
21
+ </body>
22
+ </html>
package/dist/server.js ADDED
@@ -0,0 +1,56 @@
1
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
2
+ import{createServer as xt}from"node:http";import{randomUUID as Ot}from"node:crypto";var O="https://api.hanzo.ai",b="zen5";var V="2025-01",Z=["read_products","write_products","read_orders"],f=2e4;function l(t){return typeof t=="string"&&/^[a-z0-9][a-z0-9-]*\.myshopify\.com$/.test(t)}function v(t,r=V){return`https://${t}/admin/api/${r}/graphql.json`}function I(t){let r=t.SHOPIFY_API_KEY,e=t.SHOPIFY_API_SECRET,n=t.SHOPIFY_REDIRECT_URI,o=[];if(r||o.push("SHOPIFY_API_KEY"),e||o.push("SHOPIFY_API_SECRET"),n||o.push("SHOPIFY_REDIRECT_URI"),o.length>0)throw new Error(`Missing required environment: ${o.join(", ")}`);return{shopifyApiKey:r,shopifyApiSecret:e,shopifyRedirectUri:n,scopes:t.SHOPIFY_SCOPES||Z.join(","),hanzoApiKey:t.HANZO_API_KEY||"",port:Number(t.PORT)||8791}}import{createHmac as X,timingSafeEqual as tt}from"node:crypto";function R(t){let r=new URLSearchParams({client_id:t.apiKey,scope:t.scopes,redirect_uri:t.redirectUri,state:t.state});return`https://${t.shop}/admin/oauth/authorize?${r.toString()}`}function T(t){return{url:`https://${t.shop}/admin/oauth/access_token`,headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({client_id:t.apiKey,client_secret:t.apiSecret,code:t.code})}}function E(t){if(!t||typeof t!="object")throw new Error("empty token response");if(t.error||t.errors){let r=t.error_description||t.error||JSON.stringify(t.errors);throw new Error(`Shopify OAuth error: ${r}`)}if(typeof t.access_token!="string"||t.access_token.length===0)throw new Error("Shopify OAuth response missing access_token");return{access_token:t.access_token,scope:String(t.scope??"")}}function et(t){let r=new URLSearchParams;for(let e of Object.keys(t).filter(n=>n!=="hmac"&&n!=="signature").sort())r.append(e,t[e]);return r.toString()}function rt(t,r){return X("sha256",t).update(r,"utf8").digest("hex")}function nt(t,r){let e=Buffer.from(t,"hex"),n=Buffer.from(r,"hex");return e.length===0||e.length!==n.length?!1:tt(e,n)}function C(t,r){let e=r.hmac;if(!t||!e)return!1;let n=rt(t,et(r));return nt(e,n)}function _(t){let r=t.shop;if(!l(r))throw new Error("invalid or missing shop domain");let e=t.code;if(!e)throw new Error("missing code");return{shop:r,code:e,state:t.state??""}}function g(t,r,e,n){return{url:v(t),headers:{"Content-Type":"application/json","X-Shopify-Access-Token":r},body:JSON.stringify({query:e,variables:n})}}var ot=`
3
+ query ProductForAI($id: ID!) {
4
+ product(id: $id) {
5
+ id
6
+ title
7
+ handle
8
+ descriptionHtml
9
+ description
10
+ productType
11
+ vendor
12
+ tags
13
+ status
14
+ seo { title description }
15
+ options { name values }
16
+ variants(first: 10) {
17
+ nodes { title sku price }
18
+ }
19
+ }
20
+ }
21
+ `;function m(t,r,e){return g(t,r,ot,{id:e})}var it=`
22
+ mutation UpdateProduct($input: ProductInput!) {
23
+ productUpdate(input: $input) {
24
+ product { id title descriptionHtml seo { title description } }
25
+ userErrors { field message }
26
+ }
27
+ }
28
+ `;function st(t){if(!t.id)throw new Error("productUpdate requires a product id");let r={id:t.id};if(t.descriptionHtml!==void 0&&(r.descriptionHtml=t.descriptionHtml),t.seoTitle!==void 0||t.seoDescription!==void 0){let e={};t.seoTitle!==void 0&&(e.title=t.seoTitle),t.seoDescription!==void 0&&(e.description=t.seoDescription),r.seo=e}return r}function U(t,r,e){return g(t,r,it,{input:st(e)})}var at=`
29
+ query OrderForAI($id: ID!) {
30
+ order(id: $id) {
31
+ id
32
+ name
33
+ note
34
+ email
35
+ displayFinancialStatus
36
+ displayFulfillmentStatus
37
+ createdAt
38
+ totalPriceSet { shopMoney { amount currencyCode } }
39
+ customer { firstName lastName email }
40
+ shippingAddress { city province country }
41
+ lineItems(first: 50) {
42
+ nodes { title quantity sku }
43
+ }
44
+ }
45
+ }
46
+ `;function y(t,r,e){return g(t,r,at,{id:e})}function S(t){let r=t?.errors;return!Array.isArray(r)||r.length===0?"":r.map(e=>String(e?.message??e)).join("; ")}function A(t){let r=S(t);if(r)throw new Error(`Shopify GraphQL error: ${r}`);let e=t?.data?.product;if(!e)throw new Error("product not found");return{id:String(e.id??""),title:String(e.title??""),handle:String(e.handle??""),descriptionHtml:String(e.descriptionHtml??""),description:String(e.description??""),productType:String(e.productType??""),vendor:String(e.vendor??""),tags:Array.isArray(e.tags)?e.tags.map(String):[],status:String(e.status??""),seoTitle:String(e.seo?.title??""),seoDescription:String(e.seo?.description??""),options:Array.isArray(e.options)?e.options.map(n=>({name:String(n?.name??""),values:Array.isArray(n?.values)?n.values.map(String):[]})):[],variants:Array.isArray(e.variants?.nodes)?e.variants.nodes.map(n=>({title:String(n?.title??""),sku:String(n?.sku??""),price:String(n?.price??"")})):[]}}function k(t){let r=S(t);if(r)throw new Error(`Shopify GraphQL error: ${r}`);let e=t?.data?.order;if(!e)throw new Error("order not found");let n=e.totalPriceSet?.shopMoney??{},o=e.customer??{},i=e.shippingAddress??{},s=[o.firstName,o.lastName].filter(Boolean).map(String).join(" ").trim(),p=[i.city,i.province,i.country].filter(Boolean).map(String).join(", ");return{id:String(e.id??""),name:String(e.name??""),note:String(e.note??""),email:String(e.email??o.email??""),financialStatus:String(e.displayFinancialStatus??""),fulfillmentStatus:String(e.displayFulfillmentStatus??""),createdAt:String(e.createdAt??""),totalAmount:String(n.amount??""),currency:String(n.currencyCode??""),customerName:s,shipTo:p,lineItems:Array.isArray(e.lineItems?.nodes)?e.lineItems.nodes.map(d=>({title:String(d?.title??""),quantity:Number(d?.quantity??0)||0,sku:String(d?.sku??"")})):[]}}function $(t){let r=S(t);if(r)throw new Error(`Shopify GraphQL error: ${r}`);let e=t?.data?.productUpdate;if(!e)throw new Error("productUpdate returned no result");let n=Array.isArray(e.userErrors)?e.userErrors:[];if(n.length>0){let i=n.map(s=>`${Array.isArray(s?.field)?s.field.join("."):s?.field??""}: ${s?.message??""}`.trim()).join("; ");throw new Error(`productUpdate failed: ${i}`)}let o=e.product;if(!o?.id)throw new Error("productUpdate returned no product");return{id:String(o.id),title:String(o.title??"")}}import{createHmac as ct,timingSafeEqual as pt}from"node:crypto";function dt(t,r){return ct("sha256",t).update(r,"utf8").digest("base64")}function ut(t,r){let e=Buffer.from(t,"base64"),n=Buffer.from(r,"base64");return e.length===0||e.length!==n.length?!1:pt(e,n)}function D(t,r,e){return!t||!r?!1:ut(r,dt(t,e))}var lt=new Set(["customers/data_request","customers/redact","shop/redact"]);function ht(t){return String(t?.admin_graphql_api_id??t?.id??"")}function H(t,r,e){let n=t.toLowerCase();return n==="orders/create"?{kind:"order_created",shop:r,orderId:ht(e)}:n==="app/uninstalled"?{kind:"app_uninstalled",shop:r}:lt.has(n)?{kind:"gdpr",topic:n,shop:r}:{kind:"ignored",topic:n,shop:r}}import{createAiClient as ft}from"@hanzo/ai";var gt="You are Hanzo AI, an assistant for a Shopify merchant. You help write product content (descriptions, SEO titles and meta descriptions) and handle orders and support. Work ONLY from the product or order data provided below \u2014 never invent specifications, materials, prices, dates, shipping promises, or policies that are not supported by the data. Write in clear, natural, conversion-minded prose for product content, and a warm, professional tone for customer replies. Return only the requested content, ready to paste \u2014 no preamble, no meta commentary.";function mt(t){return t.replace(/<[^>]*>/g," ").replace(/&nbsp;/g," ").replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/\s+/g," ").trim()}function yt(t,r=f){let e=[];e.push(`Title: ${t.title}`),t.productType&&e.push(`Type: ${t.productType}`),t.vendor&&e.push(`Vendor: ${t.vendor}`),t.tags.length&&e.push(`Tags: ${t.tags.join(", ")}`);for(let i of t.options)i.name&&i.values.length&&e.push(`Option ${i.name}: ${i.values.join(", ")}`);if(t.variants.length){let i=t.variants.map(s=>[s.title,s.sku&&`SKU ${s.sku}`,s.price&&`$${s.price}`].filter(Boolean).join(" \u2014 ")).join("; ");e.push(`Variants: ${i}`)}t.seoTitle&&e.push(`Current SEO title: ${t.seoTitle}`),t.seoDescription&&e.push(`Current SEO description: ${t.seoDescription}`);let n=mt(t.descriptionHtml||t.description);n&&e.push(`Current description: ${n}`);let o=e.join(`
47
+ `);return o.length<=r?{text:o,truncated:!1}:{text:o.slice(0,r),truncated:!0}}function St(t,r=f){let e=[];if(e.push(`Order: ${t.name}`),t.createdAt&&e.push(`Placed: ${t.createdAt}`),t.customerName&&e.push(`Customer: ${t.customerName}`),t.email&&e.push(`Email: ${t.email}`),t.financialStatus&&e.push(`Payment: ${t.financialStatus}`),t.fulfillmentStatus&&e.push(`Fulfillment: ${t.fulfillmentStatus}`),t.totalAmount&&e.push(`Total: ${t.totalAmount} ${t.currency}`.trim()),t.shipTo&&e.push(`Ship to: ${t.shipTo}`),t.lineItems.length){let o=t.lineItems.map(i=>`${i.quantity}\xD7 ${i.title}${i.sku?` (SKU ${i.sku})`:""}`).join("; ");e.push(`Items: ${o}`)}t.note&&e.push(`Internal note: ${t.note}`);let n=e.join(`
48
+ `);return n.length<=r?{text:n,truncated:!1}:{text:n.slice(0,r),truncated:!0}}function N(t,r){return t?`Note: the ${r} data below was truncated to fit \u2014 answer only from what is shown.`:""}function j(t,r,e,n){let o={role:"system",content:gt},s={role:"user",content:`${e?`${e}
49
+
50
+ `:""}---- ${n} ----
51
+ ${r}
52
+ ---- end ${n} ----
53
+
54
+ ---- task ----
55
+ ${t}`};return[o,s]}function At(t,r){let e=yt(r);return j(t,e.text,N(e.truncated,"product"),"product")}function kt(t,r){let e=St(r);return j(t,e.text,N(e.truncated,"order"),"order")}function L(t){return t.client??ft({token:t.token,baseUrl:t.baseURL??O})}async function M(t,r={}){let n=(await L(r).chat.completions.create({model:r.model??b,messages:t,temperature:r.temperature,stream:!1},{signal:r.signal})).choices?.[0]?.message?.content;if(typeof n!="string"||n==="")throw new Error("Hanzo API returned no content");return n}async function W(t,r,e={}){return M(At(t,r),e)}async function z(t,r,e={}){return M(kt(t,r),e)}async function G(t={}){return(await L(t).models.list({signal:t.signal})).map(e=>e.id)}var K={writeDescription:{label:"Write description",prompt:"Write a compelling product description for this product. 2\u20134 short paragraphs (or a short intro plus a bullet list of key features/benefits). Lead with the benefit, ground every claim in the product data above, and match a confident, on-brand retail tone. Return only the description text, ready to paste."},rewriteDescription:{label:"Rewrite description",prompt:"Rewrite the current product description above to be clearer, more persuasive, and better structured, WITHOUT adding any fact not present in the product data. Keep it roughly the same length. Return only the rewritten description, ready to paste."},seoTitle:{label:"SEO title",prompt:"Write an SEO page title for this product: at most 60 characters, front-loading the most important keyword, natural (not keyword-stuffed), and accurate to the product. Return only the title text on a single line."},seoDescription:{label:"SEO meta description",prompt:"Write an SEO meta description for this product: 140\u2013160 characters, one or two sentences, benefit-led, and accurate to the product data. Return only the meta description text on a single line."}},F={summarize:{label:"Summarize order",prompt:"Summarize this order for a support agent at a glance: who ordered, what and how much, the payment and fulfillment status, where it ships, and anything notable in the note. Short bullets. No preamble."},draftReply:{label:"Draft customer reply",prompt:"Draft a warm, professional reply to the customer about this order. Address them by name if known, reference the order by its name/number, and speak only to what the order data supports (status, items, shipping). Do not promise dates, refunds, or policies that are not in the data. Return only the email body, ready to send."},extractIssues:{label:"Extract issues",prompt:"Read this order (especially the internal note) and extract any customer issues, requests, or risks as a short prioritized list. For each: a one-line description and a suggested next action. If nothing stands out, say so. Do not invent issues that are not supported by the data."}};function P(t){return Object.prototype.hasOwnProperty.call(K,t)}function w(t){return Object.prototype.hasOwnProperty.call(F,t)}function Pt(t){if(!P(t))throw new Error(`Unknown product action: ${t}`);return K[t].prompt}function wt(t){if(!w(t))throw new Error(`Unknown order action: ${t}`);return F[t].prompt}async function q(t,r,e={}){return W(Pt(t),r,e)}async function B(t,r,e={}){return z(wt(t),r,e)}var x=new Map,Y=new Set;async function J(t){let r=[];for await(let e of t)r.push(e);return Buffer.concat(r).toString("utf8")}function a(t,r,e){t.writeHead(r,{"Content-Type":"application/json"}),t.end(JSON.stringify(e))}function c(t){console.log(JSON.stringify(t))}async function h(t){let r=await fetch(t.url,{method:"POST",headers:t.headers,body:t.body}),e=await r.text(),n;try{n=JSON.parse(e)}catch{throw new Error(`Shopify Admin API ${r.status}: ${e.slice(0,200)}`)}return n}function u(t){if(!l(t))throw new Error("invalid or missing shop");let r=x.get(t);if(!r)throw new Error("shop not installed");return{shop:t,accessToken:r.accessToken}}function bt(t,r,e){let n=r.searchParams.get("shop")??void 0;if(!l(n))return a(e,400,{error:"invalid or missing shop"});let o=Ot();Y.add(o);let i=R({shop:n,apiKey:t.shopifyApiKey,scopes:t.scopes,redirectUri:t.shopifyRedirectUri,state:o});e.writeHead(302,{Location:i}),e.end()}async function vt(t,r,e){let n={};if(r.searchParams.forEach((s,p)=>n[p]=s),!C(t.shopifyApiSecret,n))return c({msg:"oauth callback: HMAC verification failed"}),a(e,401,{error:"invalid hmac"});let o;try{o=_(n)}catch(s){return a(e,400,{error:s?.message||"invalid callback"})}if(!Y.delete(o.state))return c({msg:"oauth callback: unknown state (possible CSRF)",shop:o.shop}),a(e,401,{error:"invalid state"});let i=T({shop:o.shop,apiKey:t.shopifyApiKey,apiSecret:t.shopifyApiSecret,code:o.code});try{let s=await fetch(i.url,{method:"POST",headers:i.headers,body:i.body}),p=E(await s.json().catch(()=>({})));return x.set(o.shop,{accessToken:p.access_token,scope:p.scope}),c({msg:"oauth: shop installed",shop:o.shop,scope:p.scope}),a(e,200,{ok:!0,installed:!0,shop:o.shop})}catch(s){return a(e,400,{error:s?.message||"token exchange failed"})}}async function It(t,r){let{shop:e,accessToken:n}=u(t.searchParams.get("shop")??void 0),o=t.searchParams.get("id");if(!o)return a(r,400,{error:"missing product id"});let i=await h(m(e,n,o));return a(r,200,A(i))}async function Rt(t,r){let{shop:e,accessToken:n}=u(t.searchParams.get("shop")??void 0),o=t.searchParams.get("id");if(!o)return a(r,400,{error:"missing order id"});let i=await h(y(e,n,o));return a(r,200,k(i))}async function Tt(t,r,e){u(r.searchParams.get("shop")??void 0);let n=await G({token:t.hanzoApiKey});return a(e,200,{models:n})}function Q(t,r){return{token:t.hanzoApiKey,model:typeof r=="string"&&r?r:void 0}}async function Et(t,r,e){let{shop:n,accessToken:o}=u(r?.shop);if(!P(String(r?.action)))return a(e,400,{error:"unknown product action"});if(!r?.id)return a(e,400,{error:"missing product id"});let i=A(await h(m(n,o,String(r.id)))),s=await q(String(r.action),i,Q(t,r?.model));return a(e,200,{content:s})}async function Ct(t,r,e){let{shop:n,accessToken:o}=u(r?.shop);if(!w(String(r?.action)))return a(e,400,{error:"unknown order action"});if(!r?.id)return a(e,400,{error:"missing order id"});let i=k(await h(y(n,o,String(r.id)))),s=await B(String(r.action),i,Q(t,r?.model));return a(e,200,{content:s})}async function _t(t,r){let{shop:e,accessToken:n}=u(t?.shop);if(!t?.id)return a(r,400,{error:"missing product id"});let o={id:String(t.id),descriptionHtml:typeof t.descriptionHtml=="string"?t.descriptionHtml:void 0,seoTitle:typeof t.seoTitle=="string"?t.seoTitle:void 0,seoDescription:typeof t.seoDescription=="string"?t.seoDescription:void 0},i=$(await h(U(e,n,o)));return c({msg:"product updated",shop:e,productId:i.id}),a(r,200,{ok:!0,product:i})}async function Ut(t,r){switch(r.kind){case"order_created":c({msg:"webhook: order created",shop:r.shop,orderId:r.orderId,canDraft:!!t.hanzoApiKey});return;case"app_uninstalled":x.delete(r.shop),c({msg:"webhook: app uninstalled, token dropped",shop:r.shop});return;case"gdpr":c({msg:"webhook: gdpr topic acknowledged",topic:r.topic,shop:r.shop});return;case"ignored":c({msg:"webhook: ignored",topic:r.topic,shop:r.shop});return}}async function $t(t,r,e){let n=await J(r),o=r.headers["x-shopify-hmac-sha256"],i=Array.isArray(o)?o[0]:o;if(!D(t.shopifyApiSecret,i,n))return c({msg:"webhook: signature verification failed"}),a(e,401,{error:"invalid signature"});let s=String(r.headers["x-shopify-topic"]??""),p=String(r.headers["x-shopify-shop-domain"]??""),d;try{d=JSON.parse(n)}catch{return a(e,400,{error:"invalid json"})}a(e,200,{ok:!0}),Ut(t,H(s,p,d))}function Dt(t){return async(r,e)=>{let n=new URL(r.url||"/",`http://localhost:${t.port}`);try{if(r.method==="GET"&&n.pathname==="/oauth/install")return bt(t,n,e);if(r.method==="GET"&&n.pathname==="/oauth/callback")return await vt(t,n,e);if(r.method==="GET"&&n.pathname==="/v1/product")return await It(n,e);if(r.method==="GET"&&n.pathname==="/v1/order")return await Rt(n,e);if(r.method==="GET"&&n.pathname==="/v1/models")return await Tt(t,n,e);if(r.method==="POST"&&n.pathname==="/webhooks")return await $t(t,r,e);if(r.method==="GET"&&n.pathname==="/healthz")return a(e,200,{ok:!0});if(r.method==="POST"){let o=await J(r),i={};if(o)try{i=JSON.parse(o)}catch{return a(e,400,{error:"invalid json"})}if(n.pathname==="/v1/product/action")return await Et(t,i,e);if(n.pathname==="/v1/order/action")return await Ct(t,i,e);if(n.pathname==="/v1/product/update")return await _t(i,e)}return a(e,404,{error:"not found"})}catch(o){let i=o?.message||"error",s=/invalid|missing|not installed|unknown|not found/i.test(i)?400:500;return c({msg:"request error",path:n.pathname,error:i,status:s}),a(e,s,{error:i})}}}function Ht(){let t=I(process.env);xt(Dt(t)).listen(t.port,()=>{c({msg:"hanzo shopify service up",port:t.port,ai:!!t.hanzoApiKey})})}process.argv[1]&&process.argv[1].endsWith("server.js")&&Ht();export{Dt as createHandler,Ht as main};
56
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/server/server.ts", "../src/config.ts", "../src/server/oauth.ts", "../src/server/shopify-api.ts", "../src/server/webhooks.ts", "../src/server/hanzo.ts", "../src/server/actions.ts"],
4
+ "sourcesContent": ["// The Hanzo AI for Shopify service. This is the ONLY place the Shopify API secret\n// and the Hanzo API key exist \u2014 both read from the environment (never bundled,\n// never sent to the browser). It does OAuth, proxies the Admin GraphQL API + the\n// @hanzo/ai model gateway, and verifies every Shopify HMAC. The embedded Polaris\n// frontend calls the /v1/* proxy endpoints with its shop domain; the offline\n// access token stays here.\n//\n// GET /oauth/install?shop=\u2026 \u2192 redirect the merchant to Shopify consent\n// GET /oauth/callback?\u2026 \u2192 verify HMAC + state, exchange code, store\n// the offline token per shop\n// GET /v1/product?shop=&id= \u2192 read a product (Admin GraphQL)\n// GET /v1/order?shop=&id= \u2192 read an order (Admin GraphQL)\n// GET /v1/models?shop= \u2192 list model ids the shop may route to\n// POST /v1/product/action \u2192 run an AI product action, return content\n// POST /v1/order/action \u2192 run an AI order action, return content\n// POST /v1/product/update \u2192 write AI content back (productUpdate)\n// POST /webhooks \u2192 verify the Shopify webhook HMAC, route\n// GET /healthz \u2192 readiness\n//\n// Dependency-free Node http handler over the pure modules (config, oauth,\n// shopify-api, webhooks, hanzo, actions) so it is deployable behind\n// hanzoai/ingress as a small service at shopify.hanzo.ai.\n//\n// node dist/server.js (after build.js bundles it)\n\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { randomUUID } from 'node:crypto';\nimport { isShopDomain, readServerConfig, type ServerConfig } from '../config.js';\nimport {\n authorizeUrl,\n tokenExchange,\n parseTokenResponse,\n verifyCallbackHmac,\n parseCallback,\n} from './oauth.js';\nimport {\n getProduct,\n getOrder,\n updateProduct,\n parseProduct,\n parseOrder,\n parseProductUpdate,\n type PreparedGraphql,\n type ProductWriteback,\n} from './shopify-api.js';\nimport { verifyWebhook, routeWebhook, type WebhookAction } from './webhooks.js';\nimport { runProductAction, runOrderAction, isProductActionId, isOrderActionId } from './actions.js';\nimport { listModels, type AskOptions } from './hanzo.js';\n\n// A stored per-shop authorization: the offline access token to call the Admin\n// API. In-memory here keyed by shop domain; a production deployment persists\n// this to KMS/Valkey (keyed by shop) so the webhook and the panel can act after\n// a restart. The install flow populates it; every other endpoint reads it.\nconst shops = new Map<string, { accessToken: string; scope: string }>();\n\n// Fresh OAuth `state` nonces awaiting their callback. A production deployment\n// persists these (Valkey, TTL) so state survives a restart and cannot be\n// replayed; in-process is enough for a single instance and keeps the trust gate\n// real (an unknown state is rejected).\nconst pendingStates = new Set<string>();\n\nasync function readRawBody(req: IncomingMessage): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const c of req) chunks.push(c as Buffer);\n return Buffer.concat(chunks).toString('utf8');\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n}\n\nfunction log(fields: Record<string, unknown>): void {\n console.log(JSON.stringify(fields));\n}\n\n// send performs a PreparedGraphql request and returns the parsed JSON. The token\n// header + body are already shaped by shopify-api; this is the one fetch.\nasync function send(req: PreparedGraphql): Promise<any> {\n const resp = await fetch(req.url, { method: 'POST', headers: req.headers, body: req.body });\n const text = await resp.text();\n let data: any;\n try {\n data = JSON.parse(text);\n } catch {\n throw new Error(`Shopify Admin API ${resp.status}: ${text.slice(0, 200)}`);\n }\n return data;\n}\n\n// requireShopAuth resolves the shop from a query/body value and its stored token,\n// or throws a boundary error (turned into a 400/401 by the caller). Every proxy\n// endpoint runs input through this before touching Shopify \u2014 a request for an\n// unknown or un-installed shop never reaches the Admin API.\nfunction requireShopAuth(shop: string | undefined): { shop: string; accessToken: string } {\n if (!isShopDomain(shop)) throw new Error('invalid or missing shop');\n const auth = shops.get(shop);\n if (!auth) throw new Error('shop not installed');\n return { shop, accessToken: auth.accessToken };\n}\n\n// GET /oauth/install?shop=\u2026 \u2192 302 to Shopify's consent screen. The shop is\n// validated (never build the authorize URL against an unverified host); `state`\n// is a fresh CSRF nonce we remember for the callback.\nfunction handleInstall(cfg: ServerConfig, url: URL, res: ServerResponse): void {\n const shop = url.searchParams.get('shop') ?? undefined;\n if (!isShopDomain(shop)) return json(res, 400, { error: 'invalid or missing shop' });\n const state = randomUUID();\n pendingStates.add(state);\n const dest = authorizeUrl({\n shop,\n apiKey: cfg.shopifyApiKey,\n scopes: cfg.scopes,\n redirectUri: cfg.shopifyRedirectUri,\n state,\n });\n res.writeHead(302, { Location: dest });\n res.end();\n}\n\n// GET /oauth/callback \u2192 the trust gate + token exchange. Order matters:\n// 1. verify the HMAC signature (authenticity) BEFORE trusting any param\n// 2. check state (CSRF) against what we issued\n// 3. exchange the code for the offline token (secret stays in the body)\nasync function handleOAuthCallback(cfg: ServerConfig, url: URL, res: ServerResponse): Promise<void> {\n const params: Record<string, string> = {};\n url.searchParams.forEach((v, k) => (params[k] = v));\n\n if (!verifyCallbackHmac(cfg.shopifyApiSecret, params)) {\n log({ msg: 'oauth callback: HMAC verification failed' });\n return json(res, 401, { error: 'invalid hmac' });\n }\n\n let cb;\n try {\n cb = parseCallback(params);\n } catch (e: any) {\n return json(res, 400, { error: e?.message || 'invalid callback' });\n }\n\n if (!pendingStates.delete(cb.state)) {\n log({ msg: 'oauth callback: unknown state (possible CSRF)', shop: cb.shop });\n return json(res, 401, { error: 'invalid state' });\n }\n\n const ex = tokenExchange({\n shop: cb.shop,\n apiKey: cfg.shopifyApiKey,\n apiSecret: cfg.shopifyApiSecret,\n code: cb.code,\n });\n try {\n const resp = await fetch(ex.url, { method: 'POST', headers: ex.headers, body: ex.body });\n const tokenSet = parseTokenResponse(await resp.json().catch(() => ({})));\n shops.set(cb.shop, { accessToken: tokenSet.access_token, scope: tokenSet.scope });\n log({ msg: 'oauth: shop installed', shop: cb.shop, scope: tokenSet.scope });\n return json(res, 200, { ok: true, installed: true, shop: cb.shop });\n } catch (e: any) {\n return json(res, 400, { error: e?.message || 'token exchange failed' });\n }\n}\n\n// GET /v1/product?shop=&id= \u2192 read a product for the panel.\nasync function handleGetProduct(url: URL, res: ServerResponse): Promise<void> {\n const { shop, accessToken } = requireShopAuth(url.searchParams.get('shop') ?? undefined);\n const id = url.searchParams.get('id');\n if (!id) return json(res, 400, { error: 'missing product id' });\n const data = await send(getProduct(shop, accessToken, id));\n return json(res, 200, parseProduct(data));\n}\n\n// GET /v1/order?shop=&id= \u2192 read an order for the panel.\nasync function handleGetOrder(url: URL, res: ServerResponse): Promise<void> {\n const { shop, accessToken } = requireShopAuth(url.searchParams.get('shop') ?? undefined);\n const id = url.searchParams.get('id');\n if (!id) return json(res, 400, { error: 'missing order id' });\n const data = await send(getOrder(shop, accessToken, id));\n return json(res, 200, parseOrder(data));\n}\n\n// GET /v1/models?shop= \u2192 the model ids this shop may route to. Uses the server's\n// Hanzo key (the shop's org context) so the picker matches what a run will use.\nasync function handleModels(cfg: ServerConfig, url: URL, res: ServerResponse): Promise<void> {\n requireShopAuth(url.searchParams.get('shop') ?? undefined);\n const models = await listModels({ token: cfg.hanzoApiKey });\n return json(res, 200, { models });\n}\n\n// askOpts builds the @hanzo/ai options for a proxied run: the server's Hanzo key\n// is the bearer (the merchant authenticates to Shopify via App Bridge, not to\n// Hanzo directly), and the request may name a model.\nfunction askOpts(cfg: ServerConfig, model: unknown): AskOptions {\n return { token: cfg.hanzoApiKey, model: typeof model === 'string' && model ? model : undefined };\n}\n\n// POST /v1/product/action { shop, id, action, model? } \u2192 read the product, run\n// the AI action, return the generated content. The panel then previews it and,\n// on confirm, calls /v1/product/update.\nasync function handleProductAction(cfg: ServerConfig, body: any, res: ServerResponse): Promise<void> {\n const { shop, accessToken } = requireShopAuth(body?.shop);\n if (!isProductActionId(String(body?.action))) return json(res, 400, { error: 'unknown product action' });\n if (!body?.id) return json(res, 400, { error: 'missing product id' });\n const product = parseProduct(await send(getProduct(shop, accessToken, String(body.id))));\n const content = await runProductAction(String(body.action), product, askOpts(cfg, body?.model));\n return json(res, 200, { content });\n}\n\n// POST /v1/order/action { shop, id, action, model? } \u2192 read the order, run the\n// AI order/support action, return the content.\nasync function handleOrderAction(cfg: ServerConfig, body: any, res: ServerResponse): Promise<void> {\n const { shop, accessToken } = requireShopAuth(body?.shop);\n if (!isOrderActionId(String(body?.action))) return json(res, 400, { error: 'unknown order action' });\n if (!body?.id) return json(res, 400, { error: 'missing order id' });\n const order = parseOrder(await send(getOrder(shop, accessToken, String(body.id))));\n const content = await runOrderAction(String(body.action), order, askOpts(cfg, body?.model));\n return json(res, 200, { content });\n}\n\n// POST /v1/product/update { shop, id, descriptionHtml?, seoTitle?, seoDescription? }\n// \u2192 write AI content back via productUpdate. This is the write-back flow: the\n// panel generated + previewed content, the merchant confirmed, and only the\n// fields present in the body are written (a description rewrite never clobbers\n// the SEO title). userErrors surface as a thrown 400.\nasync function handleProductUpdate(body: any, res: ServerResponse): Promise<void> {\n const { shop, accessToken } = requireShopAuth(body?.shop);\n if (!body?.id) return json(res, 400, { error: 'missing product id' });\n const w: ProductWriteback = {\n id: String(body.id),\n descriptionHtml: typeof body.descriptionHtml === 'string' ? body.descriptionHtml : undefined,\n seoTitle: typeof body.seoTitle === 'string' ? body.seoTitle : undefined,\n seoDescription: typeof body.seoDescription === 'string' ? body.seoDescription : undefined,\n };\n const updated = parseProductUpdate(await send(updateProduct(shop, accessToken, w)));\n log({ msg: 'product updated', shop, productId: updated.id });\n return json(res, 200, { ok: true, product: updated });\n}\n\n// handleWebhookAction dispatches a verified webhook. Order creation could\n// auto-draft a support summary (needs the shop's token + Hanzo key); app\n// uninstall drops the stored token; GDPR topics are acknowledged (a real\n// deployment fulfills the data request/redaction against its own store here).\n// Never throws into the http handler \u2014 logs and returns.\nasync function handleWebhookAction(cfg: ServerConfig, action: WebhookAction): Promise<void> {\n switch (action.kind) {\n case 'order_created':\n log({ msg: 'webhook: order created', shop: action.shop, orderId: action.orderId, canDraft: !!cfg.hanzoApiKey });\n // A deployment that wants an auto-drafted summary reads the order via the\n // shop's stored token and runs runOrderAction('summarize', \u2026), then stores\n // the result (hanzoai/docdb) or notifies the merchant. Delivery target is a\n // per-deployment choice (documented in the README).\n return;\n case 'app_uninstalled':\n shops.delete(action.shop);\n log({ msg: 'webhook: app uninstalled, token dropped', shop: action.shop });\n return;\n case 'gdpr':\n log({ msg: 'webhook: gdpr topic acknowledged', topic: action.topic, shop: action.shop });\n return;\n case 'ignored':\n log({ msg: 'webhook: ignored', topic: action.topic, shop: action.shop });\n return;\n }\n}\n\n// POST /webhooks \u2192 verify, then dispatch. Verification is the trust gate: a\n// webhook with a bad/absent HMAC is a 401 before any action. Shopify sends the\n// signature in X-Shopify-Hmac-Sha256, the shop in X-Shopify-Shop-Domain, and the\n// topic in X-Shopify-Topic.\nasync function handleWebhook(cfg: ServerConfig, req: IncomingMessage, res: ServerResponse): Promise<void> {\n const raw = await readRawBody(req);\n const headerHmac = req.headers['x-shopify-hmac-sha256'];\n const hmac = Array.isArray(headerHmac) ? headerHmac[0] : headerHmac;\n\n if (!verifyWebhook(cfg.shopifyApiSecret, hmac, raw)) {\n log({ msg: 'webhook: signature verification failed' });\n return json(res, 401, { error: 'invalid signature' });\n }\n\n const topic = String(req.headers['x-shopify-topic'] ?? '');\n const shop = String(req.headers['x-shopify-shop-domain'] ?? '');\n let body: any;\n try {\n body = JSON.parse(raw);\n } catch {\n return json(res, 400, { error: 'invalid json' });\n }\n\n // Acknowledge immediately; do the work after (Shopify retries slow endpoints).\n json(res, 200, { ok: true });\n void handleWebhookAction(cfg, routeWebhook(topic, shop, body));\n}\n\n// createHandler is the request router. Every route is a small handler over the\n// pure modules. Exported so a test can drive it with a mock req/res if desired.\nexport function createHandler(cfg: ServerConfig) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const url = new URL(req.url || '/', `http://localhost:${cfg.port}`);\n try {\n if (req.method === 'GET' && url.pathname === '/oauth/install') return handleInstall(cfg, url, res);\n if (req.method === 'GET' && url.pathname === '/oauth/callback') return await handleOAuthCallback(cfg, url, res);\n if (req.method === 'GET' && url.pathname === '/v1/product') return await handleGetProduct(url, res);\n if (req.method === 'GET' && url.pathname === '/v1/order') return await handleGetOrder(url, res);\n if (req.method === 'GET' && url.pathname === '/v1/models') return await handleModels(cfg, url, res);\n if (req.method === 'POST' && url.pathname === '/webhooks') return await handleWebhook(cfg, req, res);\n if (req.method === 'GET' && url.pathname === '/healthz') return json(res, 200, { ok: true });\n\n if (req.method === 'POST') {\n const raw = await readRawBody(req);\n let body: any = {};\n if (raw) {\n try {\n body = JSON.parse(raw);\n } catch {\n return json(res, 400, { error: 'invalid json' });\n }\n }\n if (url.pathname === '/v1/product/action') return await handleProductAction(cfg, body, res);\n if (url.pathname === '/v1/order/action') return await handleOrderAction(cfg, body, res);\n if (url.pathname === '/v1/product/update') return await handleProductUpdate(body, res);\n }\n\n return json(res, 404, { error: 'not found' });\n } catch (e: any) {\n const msg = e?.message || 'error';\n // Boundary errors (bad input, un-installed shop) are 400; the rest 500.\n const status = /invalid|missing|not installed|unknown|not found/i.test(msg) ? 400 : 500;\n log({ msg: 'request error', path: url.pathname, error: msg, status });\n return json(res, status, { error: msg });\n }\n };\n}\n\n// main boots the server when run directly. Import-safe: only the direct entry\n// listens, so tests import the handlers without opening a port.\nexport function main(): void {\n const cfg = readServerConfig(process.env);\n const server = createServer(createHandler(cfg));\n server.listen(cfg.port, () => {\n log({ msg: 'hanzo shopify service up', port: cfg.port, ai: !!cfg.hanzoApiKey });\n });\n}\n\nif (process.argv[1] && process.argv[1].endsWith('server.js')) {\n main();\n}\n", "// Shopify config \u2014 the api.hanzo.ai model gateway (default model + endpoints),\n// the Shopify Admin API version + OAuth scopes, and the server-side secret set\n// (Shopify API key/secret + webhook HMAC). Endpoints and the bearer choice\n// mirror @hanzo/docusign and @hanzo/notion so the productivity suite stays DRY;\n// the commerce-specific pieces (product-context assembly, the AI actions) live\n// in hanzo.ts / actions.ts, not here.\n\n// ---- Hanzo model gateway --------------------------------------------------\n\n// Where the Hanzo model gateway lives. `@hanzo/ai` (createAiClient) defaults\n// here too. /v1 only, never an /api/ prefix (api.hanzo.ai IS the api host).\nexport const HANZO_API_BASE_URL = 'https://api.hanzo.ai';\n\n// Default model. A Zen model (qwen3+). Overridable per-request via the picker;\n// the gateway routes it.\nexport const DEFAULT_MODEL = 'zen5';\n\n// Public IAM origin that mints Hanzo user tokens, and the OAuth client id an\n// inbound Hanzo token is audienced to (owner-scoping validation via @hanzo/iam).\nexport const DEFAULT_IAM_SERVER_URL = 'https://hanzo.id';\nexport const DEFAULT_IAM_CLIENT_ID = 'hanzo-shopify';\n\n// chatCompletionsURL / modelsURL \u2014 the model gateway endpoints. The client\n// (createAiClient) builds these itself; these exist for tests + honest docs.\nexport function chatCompletionsURL(): string {\n return `${HANZO_API_BASE_URL}/v1/chat/completions`;\n}\nexport function modelsURL(): string {\n return `${HANZO_API_BASE_URL}/v1/models`;\n}\n\n// ---- Shopify Admin API ----------------------------------------------------\n//\n// Shopify Admin API is versioned by calendar quarter (YYYY-MM). We pin ONE\n// version and move it forward deliberately \u2014 never a speculative future one.\n// The GraphQL Admin API is the primary surface (productUpdate, product/order\n// reads); REST is legacy. Every call is against the per-shop host\n// `https://{shop}/admin/api/{version}/graphql.json`.\nexport const ADMIN_API_VERSION = '2025-01';\n\n// The OAuth scopes the app requests. read/write products for content write-back,\n// read_orders for order insight + support drafting. No scope we do not use.\nexport const OAUTH_SCOPES = ['read_products', 'write_products', 'read_orders'] as const;\n\n// Product description text budget. A product body_html plus its metafields and\n// variants can run long; this caps the characters of product context we attach\n// to any one request so it fits comfortably in a model window alongside the\n// reply. Honest truncation, never silent drop.\nexport const PRODUCT_CHAR_BUDGET = 20_000;\n\n// A Shopify shop domain looks like `my-store.myshopify.com`. isShopDomain is the\n// boundary guard: OAuth install/callback and webhook handlers validate the shop\n// parameter against this before it is ever placed in a URL, so an attacker\n// cannot point the OAuth dance or an API call at an arbitrary host. Shopify's\n// rule: hostname ending in `.myshopify.com`, only [a-z0-9-] in the store slug.\nexport function isShopDomain(shop: string | undefined | null): shop is string {\n return typeof shop === 'string' && /^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$/.test(shop);\n}\n\n// adminGraphqlUrl builds the GraphQL Admin API endpoint for a shop. Pure \u2014 the\n// request wrappers in shopify-api.ts take this string. Callers MUST have passed\n// `shop` through isShopDomain first (server.ts does).\nexport function adminGraphqlUrl(shop: string, version: string = ADMIN_API_VERSION): string {\n return `https://${shop}/admin/api/${version}/graphql.json`;\n}\n\n// ---- Server-side configuration (Shopify OAuth + webhook HMAC) --------------\n//\n// These are read from the environment by src/server/server.ts. They NEVER reach\n// the browser bundle: the API key (client id) is public, but the API secret is\n// server-only \u2014 it signs the OAuth exchange AND is the HMAC key Shopify uses to\n// sign both the OAuth callback and every webhook. readServerConfig throws on a\n// missing secret so a server that can neither complete OAuth nor verify a\n// webhook refuses to start.\n\nexport interface ServerConfig {\n /** Shopify API key / OAuth client id (public \u2014 appears in the authorize URL). */\n shopifyApiKey: string;\n /**\n * Shopify API secret (SERVER ONLY). Two jobs: the OAuth token-exchange\n * `client_secret`, AND the HMAC-SHA256 key for the OAuth callback signature\n * and every webhook signature. One secret, both trust gates.\n */\n shopifyApiSecret: string;\n /** OAuth redirect registered on the app (e.g. https://shopify.hanzo.ai/oauth/callback). */\n shopifyRedirectUri: string;\n /** The scopes string sent to /admin/oauth/authorize (space is not used \u2014 comma). */\n scopes: string;\n /**\n * Hanzo API key the server uses to run AI on behalf of a shop when no user\n * bearer is in the loop (the webhook path). Optional: without it the server\n * still does OAuth + Admin API + verifies webhooks, but the webhook cannot\n * run a model call (it logs and skips), which readServerConfig reports.\n */\n hanzoApiKey: string;\n /** Listen port. */\n port: number;\n}\n\n// readServerConfig fails fast (throws) if a required Shopify secret is missing.\n// hanzoApiKey is the one optional field (see above). Pure given an env map, so\n// it is unit-tested without touching process.env.\nexport function readServerConfig(env: Record<string, string | undefined>): ServerConfig {\n const shopifyApiKey = env.SHOPIFY_API_KEY;\n const shopifyApiSecret = env.SHOPIFY_API_SECRET;\n const shopifyRedirectUri = env.SHOPIFY_REDIRECT_URI;\n const missing: string[] = [];\n if (!shopifyApiKey) missing.push('SHOPIFY_API_KEY');\n if (!shopifyApiSecret) missing.push('SHOPIFY_API_SECRET');\n if (!shopifyRedirectUri) missing.push('SHOPIFY_REDIRECT_URI');\n if (missing.length > 0) {\n throw new Error(`Missing required environment: ${missing.join(', ')}`);\n }\n return {\n shopifyApiKey: shopifyApiKey!,\n shopifyApiSecret: shopifyApiSecret!,\n shopifyRedirectUri: shopifyRedirectUri!,\n scopes: env.SHOPIFY_SCOPES || OAUTH_SCOPES.join(','),\n hanzoApiKey: env.HANZO_API_KEY || '',\n port: Number(env.PORT) || 8791,\n };\n}\n", "// Shopify OAuth (Authorization Code Grant) \u2014 pure request shaping + the OAuth\n// callback HMAC verification. The API secret is held by the server (server.ts)\n// and never reaches the browser; these functions build the exact URL/body of\n// each OAuth call and verify Shopify's signature on the redirect, so the wire\n// shape and the trust gate are unit-testable without a network round-trip.\n//\n// Shopify's flow (shopify.dev/docs/apps/auth/oauth):\n// 1. redirect the merchant to https://{shop}/admin/oauth/authorize?\u2026\n// 2. Shopify redirects back to our redirect_uri with ?code&hmac&shop&state&\u2026\n// \u2014 we VERIFY the `hmac` (HMAC-SHA256 over the sorted remaining params,\n// hex) before trusting anything, then check `state` (CSRF).\n// 3. POST https://{shop}/admin/oauth/access_token with the code + secret to\n// get the offline access token, stored server-side per shop.\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport { isShopDomain } from '../config.js';\n\n// authorizeUrl is where the install flow sends the merchant's browser to grant\n// the app. `scope` is comma-joined (Shopify's convention, NOT space), `state`\n// is the CSRF nonce the server generates and re-checks on callback. Offline\n// access mode is the default (no `grant_options[]=per-user`), which is what a\n// background webhook needs \u2014 a long-lived token, not a per-user online one.\nexport function authorizeUrl(args: {\n shop: string;\n apiKey: string;\n scopes: string;\n redirectUri: string;\n state: string;\n}): string {\n const q = new URLSearchParams({\n client_id: args.apiKey,\n scope: args.scopes,\n redirect_uri: args.redirectUri,\n state: args.state,\n });\n return `https://${args.shop}/admin/oauth/authorize?${q.toString()}`;\n}\n\n// A prepared token-exchange request: the pieces a single fetch needs. Pure\n// output so a test asserts the URL + JSON body without opening a socket. The\n// secret rides ONLY in the body, over TLS to Shopify's own host.\nexport interface PreparedRequest {\n url: string;\n headers: Record<string, string>;\n body: string;\n}\n\n// tokenExchange builds the code\u2192token request against the shop's\n// /admin/oauth/access_token. Shopify takes a JSON body of client_id +\n// client_secret + code and returns { access_token, scope }.\nexport function tokenExchange(args: {\n shop: string;\n apiKey: string;\n apiSecret: string;\n code: string;\n}): PreparedRequest {\n return {\n url: `https://${args.shop}/admin/oauth/access_token`,\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify({\n client_id: args.apiKey,\n client_secret: args.apiSecret,\n code: args.code,\n }),\n };\n}\n\n// The token response we care about. Shopify returns access_token (the offline\n// token) + the granted scope. parseTokenResponse validates the one field we\n// must have and surfaces Shopify's own error otherwise.\nexport interface ShopifyTokenSet {\n access_token: string;\n scope: string;\n}\n\nexport function parseTokenResponse(data: any): ShopifyTokenSet {\n if (!data || typeof data !== 'object') throw new Error('empty token response');\n if (data.error || data.errors) {\n const reason = data.error_description || data.error || JSON.stringify(data.errors);\n throw new Error(`Shopify OAuth error: ${reason}`);\n }\n if (typeof data.access_token !== 'string' || data.access_token.length === 0) {\n throw new Error('Shopify OAuth response missing access_token');\n }\n return { access_token: data.access_token, scope: String(data.scope ?? '') };\n}\n\n// ---- OAuth callback HMAC verification (the trust gate) --------------------\n//\n// Shopify signs the OAuth redirect: the `hmac` query param is\n// hex(HMAC-SHA256(apiSecret, message)), where `message` is the OTHER query\n// params (everything except `hmac` and the legacy `signature`) sorted by key\n// and joined `key=value&key=value` \u2014 with the ORIGINAL, still-URL-encoded\n// values. We must reconstruct that exact string. A callback that fails here is\n// discarded before any token exchange. Docs:\n// shopify.dev/docs/apps/auth/oauth/getting-started#step-4-confirm-installation\n\n// callbackMessage builds the signed message from a param map: drop hmac +\n// signature, sort the rest by key, then re-encode as an\n// application/x-www-form-urlencoded query string. This EXACTLY matches Shopify's\n// canonical form (stringifyQueryForAdmin \u2192 ProcessedQuery.stringify, which is a\n// URLSearchParams.toString()): keys/values are percent-encoded, spaces become\n// `+`. Building the raw `key=value` join instead would fail to validate any\n// callback whose values contain encodable characters (e.g. `host`, `state`), so\n// we mirror URLSearchParams here. `params` are the decoded values (as\n// URLSearchParams.forEach yields them); URLSearchParams re-encodes them the same\n// way Shopify did when it signed. Pure.\nexport function callbackMessage(params: Record<string, string>): string {\n const sorted = new URLSearchParams();\n for (const key of Object.keys(params).filter((k) => k !== 'hmac' && k !== 'signature').sort()) {\n sorted.append(key, params[key]);\n }\n return sorted.toString();\n}\n\n// computeCallbackHmac is hex(HMAC-SHA256(apiSecret, message)). Both verification\n// and any test fixture are built from it, so the value we compare is produced\n// exactly as Shopify produces it.\nexport function computeCallbackHmac(apiSecret: string, message: string): string {\n return createHmac('sha256', apiSecret).update(message, 'utf8').digest('hex');\n}\n\n// constantTimeEqualHex compares two hex digests without leaking length/position\n// via timing. Different-length inputs are unequal and short-circuit safely (we\n// never feed mismatched buffers to timingSafeEqual, which would throw).\nexport function constantTimeEqualHex(a: string, b: string): boolean {\n const ba = Buffer.from(a, 'hex');\n const bb = Buffer.from(b, 'hex');\n if (ba.length === 0 || ba.length !== bb.length) return false;\n return timingSafeEqual(ba, bb);\n}\n\n// verifyCallbackHmac is the OAuth trust gate: reconstruct the signed message\n// from the callback params, recompute the hex HMAC with the app secret, and\n// constant-time compare against the provided `hmac` param. Returns false on any\n// missing input rather than throwing, so a malformed callback is a clean reject.\nexport function verifyCallbackHmac(\n apiSecret: string,\n params: Record<string, string>,\n): boolean {\n const provided = params.hmac;\n if (!apiSecret || !provided) return false;\n const expected = computeCallbackHmac(apiSecret, callbackMessage(params));\n return constantTimeEqualHex(provided, expected);\n}\n\n// ---- Callback parameter validation ----------------------------------------\n\n// A validated OAuth callback: the shop, the code to exchange, and the state to\n// re-check. parseCallback pulls these from the query map AFTER the HMAC passed;\n// it re-validates the shop domain (defense in depth \u2014 never build a URL against\n// an unverified host) and throws on a missing code.\nexport interface OAuthCallback {\n shop: string;\n code: string;\n state: string;\n}\n\nexport function parseCallback(params: Record<string, string>): OAuthCallback {\n const shop = params.shop;\n if (!isShopDomain(shop)) throw new Error('invalid or missing shop domain');\n const code = params.code;\n if (!code) throw new Error('missing code');\n return { shop, code, state: params.state ?? '' };\n}\n", "// Shopify Admin GraphQL API \u2014 pure request shaping + response parsing. Every\n// function returns a PreparedGraphql (url + headers + JSON body) or parses a\n// response body; none opens a socket, so the whole API surface is unit-testable\n// without a network. server.ts is the thin glue that fetches these shapes with a\n// shop's offline access token.\n//\n// The endpoint is `https://{shop}/admin/api/{version}/graphql.json` (see\n// config.adminGraphqlUrl). Authorization is the per-shop token in the\n// X-Shopify-Access-Token header (NOT a Bearer). Docs:\n// shopify.dev/docs/api/admin-graphql\n\nimport { adminGraphqlUrl } from '../config.js';\n\n// A prepared GraphQL POST: the pieces a single fetch needs. The access token\n// rides in the X-Shopify-Access-Token header, per Shopify's Admin API.\nexport interface PreparedGraphql {\n url: string;\n headers: Record<string, string>;\n body: string;\n}\n\n// graphql builds a PreparedGraphql from a query + variables against a shop.\n// One place assembles the token header + JSON body so every call is identical.\nexport function graphql(\n shop: string,\n accessToken: string,\n query: string,\n variables: Record<string, unknown>,\n): PreparedGraphql {\n return {\n url: adminGraphqlUrl(shop),\n headers: {\n 'Content-Type': 'application/json',\n 'X-Shopify-Access-Token': accessToken,\n },\n body: JSON.stringify({ query, variables }),\n };\n}\n\n// ---- Queries / mutations --------------------------------------------------\n\n// PRODUCT_QUERY reads the fields the AI needs to write content: title, current\n// description (as body HTML + plain text), product type, vendor, tags, the SEO\n// title/description, and the first handful of variants for price/option context.\n// We ask for exactly what the prompt uses \u2014 no over-fetching.\nexport const PRODUCT_QUERY = /* GraphQL */ `\n query ProductForAI($id: ID!) {\n product(id: $id) {\n id\n title\n handle\n descriptionHtml\n description\n productType\n vendor\n tags\n status\n seo { title description }\n options { name values }\n variants(first: 10) {\n nodes { title sku price }\n }\n }\n }\n`;\n\n// getProduct \u2014 fetch one product by its GID (gid://shopify/Product/123).\nexport function getProduct(shop: string, accessToken: string, id: string): PreparedGraphql {\n return graphql(shop, accessToken, PRODUCT_QUERY, { id });\n}\n\n// PRODUCT_UPDATE_MUTATION writes AI-generated content back: the description HTML\n// and/or the SEO title/description. `input` is Shopify's ProductInput; we only\n// ever send the fields we changed. userErrors is requested so a validation\n// failure surfaces as data, not a thrown 200.\nexport const PRODUCT_UPDATE_MUTATION = /* GraphQL */ `\n mutation UpdateProduct($input: ProductInput!) {\n productUpdate(input: $input) {\n product { id title descriptionHtml seo { title description } }\n userErrors { field message }\n }\n }\n`;\n\n// The write-back fields the AI produces. Any subset may be present \u2014 we send\n// only what is set, so a \"rewrite description\" never clobbers the SEO title.\nexport interface ProductWriteback {\n id: string;\n descriptionHtml?: string;\n seoTitle?: string;\n seoDescription?: string;\n}\n\n// buildProductInput turns a ProductWriteback into Shopify's ProductInput shape,\n// including `seo` only when a title or description is set. Pure so the exact\n// mutation variables are asserted in a test. Throws on a missing id (the mutation\n// cannot target a product without it) \u2014 a boundary error surfaced to the caller.\nexport function buildProductInput(w: ProductWriteback): Record<string, unknown> {\n if (!w.id) throw new Error('productUpdate requires a product id');\n const input: Record<string, unknown> = { id: w.id };\n if (w.descriptionHtml !== undefined) input.descriptionHtml = w.descriptionHtml;\n if (w.seoTitle !== undefined || w.seoDescription !== undefined) {\n const seo: Record<string, unknown> = {};\n if (w.seoTitle !== undefined) seo.title = w.seoTitle;\n if (w.seoDescription !== undefined) seo.description = w.seoDescription;\n input.seo = seo;\n }\n return input;\n}\n\n// updateProduct \u2014 the productUpdate mutation with the built input.\nexport function updateProduct(shop: string, accessToken: string, w: ProductWriteback): PreparedGraphql {\n return graphql(shop, accessToken, PRODUCT_UPDATE_MUTATION, { input: buildProductInput(w) });\n}\n\n// ORDER_QUERY reads the fields the AI needs to summarize an order and draft a\n// customer reply: name/number, financial + fulfillment status, totals, the\n// customer, line items, the shipping address, and the internal note. Enough to\n// answer \"what is this order\" and \"draft a reply\" without a second round trip.\nexport const ORDER_QUERY = /* GraphQL */ `\n query OrderForAI($id: ID!) {\n order(id: $id) {\n id\n name\n note\n email\n displayFinancialStatus\n displayFulfillmentStatus\n createdAt\n totalPriceSet { shopMoney { amount currencyCode } }\n customer { firstName lastName email }\n shippingAddress { city province country }\n lineItems(first: 50) {\n nodes { title quantity sku }\n }\n }\n }\n`;\n\n// getOrder \u2014 fetch one order by its GID (gid://shopify/Order/123).\nexport function getOrder(shop: string, accessToken: string, id: string): PreparedGraphql {\n return graphql(shop, accessToken, ORDER_QUERY, { id });\n}\n\n// ---- Response parsing -----------------------------------------------------\n\n// The normalized product our prompt assembly consumes. Snake/camel from the wire\n// is normalized here so nothing downstream touches the GraphQL shape.\nexport interface ProductInfo {\n id: string;\n title: string;\n handle: string;\n descriptionHtml: string;\n description: string;\n productType: string;\n vendor: string;\n tags: string[];\n status: string;\n seoTitle: string;\n seoDescription: string;\n options: Array<{ name: string; values: string[] }>;\n variants: Array<{ title: string; sku: string; price: string }>;\n}\n\n// graphqlErrors extracts the top-level `errors` array a GraphQL endpoint returns\n// on a bad query (distinct from userErrors on a mutation). Returns the joined\n// messages, or '' when there are none. Pure.\nexport function graphqlErrors(data: any): string {\n const errs = data?.errors;\n if (!Array.isArray(errs) || errs.length === 0) return '';\n return errs.map((e: any) => String(e?.message ?? e)).join('; ');\n}\n\n// parseProduct reads a getProduct response into a ProductInfo. Throws on a\n// GraphQL error or a null product (a bad id) so the caller learns immediately.\nexport function parseProduct(data: any): ProductInfo {\n const err = graphqlErrors(data);\n if (err) throw new Error(`Shopify GraphQL error: ${err}`);\n const p = data?.data?.product;\n if (!p) throw new Error('product not found');\n return {\n id: String(p.id ?? ''),\n title: String(p.title ?? ''),\n handle: String(p.handle ?? ''),\n descriptionHtml: String(p.descriptionHtml ?? ''),\n description: String(p.description ?? ''),\n productType: String(p.productType ?? ''),\n vendor: String(p.vendor ?? ''),\n tags: Array.isArray(p.tags) ? p.tags.map(String) : [],\n status: String(p.status ?? ''),\n seoTitle: String(p.seo?.title ?? ''),\n seoDescription: String(p.seo?.description ?? ''),\n options: Array.isArray(p.options)\n ? p.options.map((o: any) => ({\n name: String(o?.name ?? ''),\n values: Array.isArray(o?.values) ? o.values.map(String) : [],\n }))\n : [],\n variants: Array.isArray(p.variants?.nodes)\n ? p.variants.nodes.map((v: any) => ({\n title: String(v?.title ?? ''),\n sku: String(v?.sku ?? ''),\n price: String(v?.price ?? ''),\n }))\n : [],\n };\n}\n\n// The normalized order our prompt assembly + summary consume.\nexport interface OrderInfo {\n id: string;\n name: string;\n note: string;\n email: string;\n financialStatus: string;\n fulfillmentStatus: string;\n createdAt: string;\n totalAmount: string;\n currency: string;\n customerName: string;\n shipTo: string;\n lineItems: Array<{ title: string; quantity: number; sku: string }>;\n}\n\n// parseOrder reads a getOrder response into an OrderInfo. Throws on a GraphQL\n// error or a null order.\nexport function parseOrder(data: any): OrderInfo {\n const err = graphqlErrors(data);\n if (err) throw new Error(`Shopify GraphQL error: ${err}`);\n const o = data?.data?.order;\n if (!o) throw new Error('order not found');\n const money = o.totalPriceSet?.shopMoney ?? {};\n const cust = o.customer ?? {};\n const addr = o.shippingAddress ?? {};\n const customerName = [cust.firstName, cust.lastName].filter(Boolean).map(String).join(' ').trim();\n const shipTo = [addr.city, addr.province, addr.country].filter(Boolean).map(String).join(', ');\n return {\n id: String(o.id ?? ''),\n name: String(o.name ?? ''),\n note: String(o.note ?? ''),\n email: String(o.email ?? cust.email ?? ''),\n financialStatus: String(o.displayFinancialStatus ?? ''),\n fulfillmentStatus: String(o.displayFulfillmentStatus ?? ''),\n createdAt: String(o.createdAt ?? ''),\n totalAmount: String(money.amount ?? ''),\n currency: String(money.currencyCode ?? ''),\n customerName,\n shipTo,\n lineItems: Array.isArray(o.lineItems?.nodes)\n ? o.lineItems.nodes.map((li: any) => ({\n title: String(li?.title ?? ''),\n quantity: Number(li?.quantity ?? 0) || 0,\n sku: String(li?.sku ?? ''),\n }))\n : [],\n };\n}\n\n// parseProductUpdate reads a productUpdate response, surfacing userErrors (a\n// validation failure Shopify returns inside a 200) as a thrown error so the\n// write-back path has ONE way to fail: reject. Returns the updated product's id\n// + title on success. Pure.\nexport function parseProductUpdate(data: any): { id: string; title: string } {\n const err = graphqlErrors(data);\n if (err) throw new Error(`Shopify GraphQL error: ${err}`);\n const result = data?.data?.productUpdate;\n if (!result) throw new Error('productUpdate returned no result');\n const userErrors = Array.isArray(result.userErrors) ? result.userErrors : [];\n if (userErrors.length > 0) {\n const msg = userErrors\n .map((e: any) => `${Array.isArray(e?.field) ? e.field.join('.') : e?.field ?? ''}: ${e?.message ?? ''}`.trim())\n .join('; ');\n throw new Error(`productUpdate failed: ${msg}`);\n }\n const product = result.product;\n if (!product?.id) throw new Error('productUpdate returned no product');\n return { id: String(product.id), title: String(product.title ?? '') };\n}\n", "// Shopify webhooks: HMAC signature verification and topic routing \u2014 all pure\n// over their inputs so they are unit-testable without an http server. server.ts\n// is the thin http glue that reads the raw body + headers and calls these.\n// Crypto is Node's stdlib (node:crypto), never a dependency.\n//\n// Shopify signs every webhook with the app's API secret. Unlike the OAuth\n// callback (hex over sorted query params), a webhook signature is:\n// X-Shopify-Hmac-Sha256: base64(HMAC-SHA256(apiSecret, RAW_REQUEST_BODY))\n// computed over the RAW body bytes. We must NOT JSON.parse-and-reserialize\n// before verifying (key order/whitespace would differ and every webhook would\n// fail). This is the whole trust boundary: a webhook that fails here is\n// discarded before it is acted on. The shop + topic ride in headers\n// (X-Shopify-Shop-Domain, X-Shopify-Topic). Docs:\n// shopify.dev/docs/apps/build/webhooks/subscribe/verify-webhooks\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n// computeWebhookHmac is base64(HMAC-SHA256(apiSecret, rawBody)) \u2014 the value\n// Shopify puts in X-Shopify-Hmac-Sha256. Both verification and any test fixture\n// are built from it, so the value we compare is produced exactly as Shopify\n// produces it.\nexport function computeWebhookHmac(apiSecret: string, rawBody: string): string {\n return createHmac('sha256', apiSecret).update(rawBody, 'utf8').digest('base64');\n}\n\n// constantTimeEqualB64 compares two base64 digests without leaking\n// length/position via timing. Different-length inputs are unequal and\n// short-circuit safely (never feeding mismatched buffers to timingSafeEqual,\n// which would throw).\nexport function constantTimeEqualB64(a: string, b: string): boolean {\n const ba = Buffer.from(a, 'base64');\n const bb = Buffer.from(b, 'base64');\n if (ba.length === 0 || ba.length !== bb.length) return false;\n return timingSafeEqual(ba, bb);\n}\n\n// verifyWebhook is the trust gate: recompute base64(HMAC-SHA256(apiSecret,\n// rawBody)) and constant-time compare against the X-Shopify-Hmac-Sha256 header.\n// Returns false on any missing input rather than throwing, so a malformed\n// request is a clean reject.\nexport function verifyWebhook(apiSecret: string, headerHmac: string | undefined, rawBody: string): boolean {\n if (!apiSecret || !headerHmac) return false;\n return constantTimeEqualB64(headerHmac, computeWebhookHmac(apiSecret, rawBody));\n}\n\n// ---- Topic routing --------------------------------------------------------\n//\n// After a webhook is verified, we route on the X-Shopify-Topic header into a\n// discriminated action so the server can dispatch. We act on order creation\n// (auto-draft a support summary), app uninstall (drop the stored token), and\n// the three GDPR \"mandatory\" topics every public app MUST handle to pass App\n// Store review. Everything unknown is acknowledged and ignored \u2014 a clean 200.\n\nexport type WebhookAction =\n | { kind: 'order_created'; shop: string; orderId: string }\n | { kind: 'app_uninstalled'; shop: string }\n | { kind: 'gdpr'; topic: string; shop: string }\n | { kind: 'ignored'; topic: string; shop: string };\n\n// The three GDPR/privacy topics Shopify requires a public app to subscribe to\n// and respond to. shopify.dev/docs/apps/build/privacy-law-compliance\nconst GDPR_TOPICS = new Set([\n 'customers/data_request',\n 'customers/redact',\n 'shop/redact',\n]);\n\n// gid://shopify/Order/12345 \u2192 \"gid://shopify/Order/12345\". We keep the full GID\n// (the GraphQL Admin API takes GIDs); the numeric REST `id` is a fallback. Pure.\nfunction orderIdOf(body: any): string {\n return String(body?.admin_graphql_api_id ?? body?.id ?? '');\n}\n\n// routeWebhook turns a topic + PARSED, ALREADY-VERIFIED body into a\n// WebhookAction. Verification is the caller's job (verifyWebhook) \u2014 this is pure\n// dispatch, unit-testable with plain objects. `shop` is the\n// X-Shopify-Shop-Domain header value.\nexport function routeWebhook(topic: string, shop: string, body: any): WebhookAction {\n const t = topic.toLowerCase();\n if (t === 'orders/create') {\n return { kind: 'order_created', shop, orderId: orderIdOf(body) };\n }\n if (t === 'app/uninstalled') {\n return { kind: 'app_uninstalled', shop };\n }\n if (GDPR_TOPICS.has(t)) {\n return { kind: 'gdpr', topic: t, shop };\n }\n return { kind: 'ignored', topic: t, shop };\n}\n", "// The Hanzo call and its request/response shaping over a PRODUCT or ORDER \u2014\n// pure, host-agnostic, and fully unit-testable (no Shopify SDK, no DOM). The\n// Polaris panel and the webhook server are the thin glue that read a product/\n// order and hand it here.\n//\n// This is a THIN wrapper over the PUBLISHED headless client `@hanzo/ai`\n// (createAiClient) \u2014 we do NOT reimplement the transport. This module owns only\n// the commerce-aware layer: product/order context assembly + truncation + prompt\n// building, kept pure so it is tested and reused by both the panel backend and\n// the (browser-less) webhook. The AI actions (their prompt templates) live in\n// actions.ts, layered on `ask`.\n\nimport { createAiClient, type AiClient } from '@hanzo/ai';\nimport { DEFAULT_MODEL, HANZO_API_BASE_URL, PRODUCT_CHAR_BUDGET } from '../config.js';\nimport type { ProductInfo, OrderInfo } from './shopify-api.js';\n\n// A message in the OpenAI-compatible schema \u2014 the shape @hanzo/ai's\n// chat.completions.create takes. Kept as a local alias so the prompt-assembly\n// functions (buildProductMessages/buildOrderMessages) have a precise, testable\n// return type independent of the SDK's broader union.\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\n// SYSTEM_PROMPT grounds every answer in the store data provided. It forbids\n// inventing facts about the product or order (the failure mode that makes a\n// commerce assistant dangerous \u2014 fake specs, fake shipping promises), and keeps\n// output ready to paste into a Shopify field. Merchant-facing, brand-neutral.\nexport const SYSTEM_PROMPT =\n 'You are Hanzo AI, an assistant for a Shopify merchant. You help write product ' +\n 'content (descriptions, SEO titles and meta descriptions) and handle orders and ' +\n 'support. Work ONLY from the product or order data provided below \u2014 never invent ' +\n 'specifications, materials, prices, dates, shipping promises, or policies that ' +\n 'are not supported by the data. Write in clear, natural, conversion-minded prose ' +\n 'for product content, and a warm, professional tone for customer replies. Return ' +\n 'only the requested content, ready to paste \u2014 no preamble, no meta commentary.';\n\n// ---- Product context assembly ---------------------------------------------\n//\n// A product's attributes are assembled into a compact, labeled block the model\n// reads as data (never as instructions). We include the existing description so\n// \"rewrite\" has the current copy to improve, and cap the whole block at the\n// budget \u2014 HTML descriptions can be long. Honest truncation, never silent drop.\n\n// The result of rendering a product down to what we attach: the text and whether\n// anything was truncated (so the prompt and UI can say so honestly).\nexport interface ProductContext {\n text: string;\n truncated: boolean;\n}\n\n// stripHtml reduces an HTML description to readable text for the context block\n// (the model does not need the markup to understand the copy, and tags waste\n// budget). Minimal + total: drop tags, collapse whitespace. Pure.\nexport function stripHtml(html: string): string {\n return html\n .replace(/<[^>]*>/g, ' ')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n// buildProductContext renders the product attributes into a labeled block,\n// capped at `budget` characters. The current description is the last (and\n// longest) field, so truncation trims it rather than the structured attributes\n// the model most needs. Pure and total. `truncated` is true whenever the render\n// was cut to fit.\nexport function buildProductContext(p: ProductInfo, budget: number = PRODUCT_CHAR_BUDGET): ProductContext {\n const lines: string[] = [];\n lines.push(`Title: ${p.title}`);\n if (p.productType) lines.push(`Type: ${p.productType}`);\n if (p.vendor) lines.push(`Vendor: ${p.vendor}`);\n if (p.tags.length) lines.push(`Tags: ${p.tags.join(', ')}`);\n for (const o of p.options) {\n if (o.name && o.values.length) lines.push(`Option ${o.name}: ${o.values.join(', ')}`);\n }\n if (p.variants.length) {\n const vs = p.variants\n .map((v) => [v.title, v.sku && `SKU ${v.sku}`, v.price && `$${v.price}`].filter(Boolean).join(' \u2014 '))\n .join('; ');\n lines.push(`Variants: ${vs}`);\n }\n if (p.seoTitle) lines.push(`Current SEO title: ${p.seoTitle}`);\n if (p.seoDescription) lines.push(`Current SEO description: ${p.seoDescription}`);\n const currentDesc = stripHtml(p.descriptionHtml || p.description);\n if (currentDesc) lines.push(`Current description: ${currentDesc}`);\n\n const full = lines.join('\\n');\n if (full.length <= budget) return { text: full, truncated: false };\n return { text: full.slice(0, budget), truncated: true };\n}\n\n// ---- Order context assembly -----------------------------------------------\n\nexport interface OrderContext {\n text: string;\n truncated: boolean;\n}\n\n// buildOrderContext renders an order into a labeled block. Orders are small; the\n// budget only bites on a huge line-item list or a long note, in which case we\n// truncate the tail honestly. Pure and total.\nexport function buildOrderContext(o: OrderInfo, budget: number = PRODUCT_CHAR_BUDGET): OrderContext {\n const lines: string[] = [];\n lines.push(`Order: ${o.name}`);\n if (o.createdAt) lines.push(`Placed: ${o.createdAt}`);\n if (o.customerName) lines.push(`Customer: ${o.customerName}`);\n if (o.email) lines.push(`Email: ${o.email}`);\n if (o.financialStatus) lines.push(`Payment: ${o.financialStatus}`);\n if (o.fulfillmentStatus) lines.push(`Fulfillment: ${o.fulfillmentStatus}`);\n if (o.totalAmount) lines.push(`Total: ${o.totalAmount} ${o.currency}`.trim());\n if (o.shipTo) lines.push(`Ship to: ${o.shipTo}`);\n if (o.lineItems.length) {\n const items = o.lineItems\n .map((li) => `${li.quantity}\u00D7 ${li.title}${li.sku ? ` (SKU ${li.sku})` : ''}`)\n .join('; ');\n lines.push(`Items: ${items}`);\n }\n if (o.note) lines.push(`Internal note: ${o.note}`);\n\n const full = lines.join('\\n');\n if (full.length <= budget) return { text: full, truncated: false };\n return { text: full.slice(0, budget), truncated: true };\n}\n\n// ---- Prompt assembly ------------------------------------------------------\n\n// contextNote is the one honest sentence prepended when the render was cut, so\n// the model does not answer as though it saw everything.\nfunction contextNote(truncated: boolean, kind: 'product' | 'order'): string {\n return truncated\n ? `Note: the ${kind} data below was truncated to fit \u2014 answer only from what is shown.`\n : '';\n}\n\n// buildMessages fences the store data as DATA (never instructions) and rides the\n// honest note inside the user turn so it is never lost. Shared by product and\n// order paths \u2014 one assembly, one shape. Pure.\nexport function buildMessages(task: string, contextText: string, note: string, fence: string): ChatMessage[] {\n const system: ChatMessage = { role: 'system', content: SYSTEM_PROMPT };\n const notePrefix = note ? `${note}\\n\\n` : '';\n const user: ChatMessage = {\n role: 'user',\n content:\n `${notePrefix}---- ${fence} ----\\n${contextText}\\n---- end ${fence} ----\\n\\n` +\n `---- task ----\\n${task}`,\n };\n return [system, user];\n}\n\n// buildProductMessages / buildOrderMessages are the two public assemblers the\n// actions + freeform ask use. Each windows its context and builds the message\n// list. Pure \u2014 the whole prompt is asserted in a test.\nexport function buildProductMessages(task: string, p: ProductInfo): ChatMessage[] {\n const ctx = buildProductContext(p);\n return buildMessages(task, ctx.text, contextNote(ctx.truncated, 'product'), 'product');\n}\n\nexport function buildOrderMessages(task: string, o: OrderInfo): ChatMessage[] {\n const ctx = buildOrderContext(o);\n return buildMessages(task, ctx.text, contextNote(ctx.truncated, 'order'), 'order');\n}\n\n// ---- The single call path -------------------------------------------------\n\nexport interface AskOptions {\n model?: string;\n temperature?: number;\n token?: string;\n baseURL?: string;\n /** Injected client (tests). Defaults to a real createAiClient. */\n client?: AiClient;\n signal?: AbortSignal;\n}\n\n// client resolves an AiClient: the injected one (tests) or a real published\n// @hanzo/ai client pointed at the gateway with the caller's bearer. One place\n// constructs it so the token/baseURL wiring is identical for every surface.\nfunction client(opts: AskOptions): AiClient {\n return opts.client ?? createAiClient({ token: opts.token, baseUrl: opts.baseURL ?? HANZO_API_BASE_URL });\n}\n\n// runChat is the single path from a built message list to the model. Every\n// Shopify surface (the actions, a freeform question, the webhook auto-draft)\n// funnels here. Non-streaming: the panel renders the final text and the webhook\n// stores a finished draft. token may be empty (the gateway serves anonymous/\n// limited models).\nexport async function runChat(messages: ChatMessage[], opts: AskOptions = {}): Promise<string> {\n const res = await client(opts).chat.completions.create({\n model: opts.model ?? DEFAULT_MODEL,\n messages,\n temperature: opts.temperature,\n stream: false,\n }, { signal: opts.signal });\n const content = res.choices?.[0]?.message?.content;\n if (typeof content !== 'string' || content === '') {\n throw new Error('Hanzo API returned no content');\n }\n return content;\n}\n\n// askProduct / askOrder run ONE completion over a product/order with a freeform\n// task. The actions in actions.ts resolve their prompt and call these.\nexport async function askProduct(task: string, p: ProductInfo, opts: AskOptions = {}): Promise<string> {\n return runChat(buildProductMessages(task, p), opts);\n}\n\nexport async function askOrder(task: string, o: OrderInfo, opts: AskOptions = {}): Promise<string> {\n return runChat(buildOrderMessages(task, o), opts);\n}\n\n// listModels returns the model ids the caller may route to, from /v1/models via\n// the headless client. Org-scoped by the bearer; an empty token lists public\n// models.\nexport async function listModels(opts: AskOptions = {}): Promise<string[]> {\n const models = await client(opts).models.list({ signal: opts.signal });\n return models.map((m) => m.id);\n}\n", "// The AI actions over a product or an order \u2014 each a prompt template applied to\n// the assembled context via the single `askProduct` / `askOrder` primitives in\n// hanzo.ts. There is exactly ONE code path to the model per subject: an action\n// is (id \u2192 prompt + subject), and the panel, the picker, and the webhook all\n// resolve an id here and call the matching ask. No action speaks to the gateway\n// directly.\n\nimport { askOrder, askProduct, type AskOptions } from './hanzo.js';\nimport type { ProductInfo, OrderInfo } from './shopify-api.js';\n\n// A product action's prompt is written to produce paste-ready content (a\n// description, an SEO title, a meta description). Prompts are specific and\n// output-shaped so results drop straight into the Shopify field.\nexport const PRODUCT_ACTIONS = {\n writeDescription: {\n label: 'Write description',\n prompt:\n 'Write a compelling product description for this product. 2\u20134 short ' +\n 'paragraphs (or a short intro plus a bullet list of key features/benefits). ' +\n 'Lead with the benefit, ground every claim in the product data above, and ' +\n 'match a confident, on-brand retail tone. Return only the description text, ' +\n 'ready to paste.',\n },\n rewriteDescription: {\n label: 'Rewrite description',\n prompt:\n 'Rewrite the current product description above to be clearer, more ' +\n 'persuasive, and better structured, WITHOUT adding any fact not present in ' +\n 'the product data. Keep it roughly the same length. Return only the rewritten ' +\n 'description, ready to paste.',\n },\n seoTitle: {\n label: 'SEO title',\n prompt:\n 'Write an SEO page title for this product: at most 60 characters, ' +\n 'front-loading the most important keyword, natural (not keyword-stuffed), and ' +\n 'accurate to the product. Return only the title text on a single line.',\n },\n seoDescription: {\n label: 'SEO meta description',\n prompt:\n 'Write an SEO meta description for this product: 140\u2013160 characters, one or ' +\n 'two sentences, benefit-led, and accurate to the product data. Return only ' +\n 'the meta description text on a single line.',\n },\n} as const;\n\n// An order action's prompt produces a summary, a customer reply, or an issue\n// extraction \u2014 the support/order-insight surface.\nexport const ORDER_ACTIONS = {\n summarize: {\n label: 'Summarize order',\n prompt:\n 'Summarize this order for a support agent at a glance: who ordered, what and ' +\n 'how much, the payment and fulfillment status, where it ships, and anything ' +\n 'notable in the note. Short bullets. No preamble.',\n },\n draftReply: {\n label: 'Draft customer reply',\n prompt:\n 'Draft a warm, professional reply to the customer about this order. Address ' +\n 'them by name if known, reference the order by its name/number, and speak ' +\n 'only to what the order data supports (status, items, shipping). Do not ' +\n 'promise dates, refunds, or policies that are not in the data. Return only ' +\n 'the email body, ready to send.',\n },\n extractIssues: {\n label: 'Extract issues',\n prompt:\n 'Read this order (especially the internal note) and extract any customer ' +\n 'issues, requests, or risks as a short prioritized list. For each: a one-line ' +\n 'description and a suggested next action. If nothing stands out, say so. Do ' +\n 'not invent issues that are not supported by the data.',\n },\n} as const;\n\n// A product / order action id from the respective catalog.\nexport type ProductActionId = keyof typeof PRODUCT_ACTIONS;\nexport type OrderActionId = keyof typeof ORDER_ACTIONS;\n\n// isProductActionId / isOrderActionId narrow an arbitrary string to a known id.\n// Boundary guards \u2014 the panel and the server validate an inbound id here before\n// running it.\nexport function isProductActionId(id: string): id is ProductActionId {\n return Object.prototype.hasOwnProperty.call(PRODUCT_ACTIONS, id);\n}\nexport function isOrderActionId(id: string): id is OrderActionId {\n return Object.prototype.hasOwnProperty.call(ORDER_ACTIONS, id);\n}\n\n// productActionPrompt / orderActionPrompt resolve an id to its prompt. Each\n// throws on an unknown id (a boundary error surfaced to the caller) rather than\n// silently running a default.\nexport function productActionPrompt(id: string): string {\n if (!isProductActionId(id)) throw new Error(`Unknown product action: ${id}`);\n return PRODUCT_ACTIONS[id].prompt;\n}\nexport function orderActionPrompt(id: string): string {\n if (!isOrderActionId(id)) throw new Error(`Unknown order action: ${id}`);\n return ORDER_ACTIONS[id].prompt;\n}\n\n// productActionList / orderActionList are the ordered catalogs for building the\n// UI, derived from the action maps so the panel and the catalog can never drift.\nexport function productActionList(): Array<{ id: ProductActionId; label: string }> {\n return (Object.keys(PRODUCT_ACTIONS) as ProductActionId[]).map((id) => ({ id, label: PRODUCT_ACTIONS[id].label }));\n}\nexport function orderActionList(): Array<{ id: OrderActionId; label: string }> {\n return (Object.keys(ORDER_ACTIONS) as OrderActionId[]).map((id) => ({ id, label: ORDER_ACTIONS[id].label }));\n}\n\n// runProductAction / runOrderAction are the single entry points each surface\n// calls: resolve the action's prompt and run it over the assembled context via\n// the matching ask. One code path from an id to the model per subject. Async so\n// an unknown id surfaces as a rejected promise (not a synchronous throw), giving\n// callers ONE way to handle failure: await/.catch.\nexport async function runProductAction(id: string, p: ProductInfo, opts: AskOptions = {}): Promise<string> {\n return askProduct(productActionPrompt(id), p, opts);\n}\nexport async function runOrderAction(id: string, o: OrderInfo, opts: AskOptions = {}): Promise<string> {\n return askOrder(orderActionPrompt(id), o, opts);\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["createServer", "randomUUID", "HANZO_API_BASE_URL", "DEFAULT_MODEL", "ADMIN_API_VERSION", "OAUTH_SCOPES", "PRODUCT_CHAR_BUDGET", "isShopDomain", "shop", "adminGraphqlUrl", "version", "readServerConfig", "env", "shopifyApiKey", "shopifyApiSecret", "shopifyRedirectUri", "missing", "createHmac", "timingSafeEqual", "authorizeUrl", "args", "q", "tokenExchange", "parseTokenResponse", "data", "reason", "callbackMessage", "params", "sorted", "key", "k", "computeCallbackHmac", "apiSecret", "message", "createHmac", "constantTimeEqualHex", "a", "b", "ba", "bb", "timingSafeEqual", "verifyCallbackHmac", "provided", "expected", "parseCallback", "shop", "isShopDomain", "code", "graphql", "shop", "accessToken", "query", "variables", "adminGraphqlUrl", "PRODUCT_QUERY", "getProduct", "id", "PRODUCT_UPDATE_MUTATION", "buildProductInput", "w", "input", "seo", "updateProduct", "ORDER_QUERY", "getOrder", "graphqlErrors", "data", "errs", "parseProduct", "err", "p", "o", "v", "parseOrder", "money", "cust", "addr", "customerName", "shipTo", "li", "parseProductUpdate", "result", "userErrors", "msg", "e", "product", "createHmac", "timingSafeEqual", "computeWebhookHmac", "apiSecret", "rawBody", "constantTimeEqualB64", "a", "b", "ba", "bb", "verifyWebhook", "headerHmac", "GDPR_TOPICS", "orderIdOf", "body", "routeWebhook", "topic", "shop", "t", "createAiClient", "SYSTEM_PROMPT", "stripHtml", "html", "buildProductContext", "p", "budget", "PRODUCT_CHAR_BUDGET", "lines", "o", "vs", "v", "currentDesc", "full", "buildOrderContext", "items", "li", "contextNote", "truncated", "kind", "buildMessages", "task", "contextText", "note", "fence", "system", "user", "buildProductMessages", "ctx", "buildOrderMessages", "client", "opts", "createAiClient", "HANZO_API_BASE_URL", "runChat", "messages", "content", "DEFAULT_MODEL", "askProduct", "askOrder", "listModels", "m", "PRODUCT_ACTIONS", "ORDER_ACTIONS", "isProductActionId", "id", "isOrderActionId", "productActionPrompt", "orderActionPrompt", "runProductAction", "id", "p", "opts", "askProduct", "productActionPrompt", "runOrderAction", "o", "askOrder", "orderActionPrompt", "shops", "pendingStates", "readRawBody", "req", "chunks", "c", "json", "res", "status", "body", "log", "fields", "send", "resp", "text", "data", "requireShopAuth", "shop", "isShopDomain", "auth", "handleInstall", "cfg", "url", "state", "randomUUID", "dest", "authorizeUrl", "handleOAuthCallback", "params", "v", "k", "verifyCallbackHmac", "cb", "parseCallback", "e", "ex", "tokenExchange", "tokenSet", "parseTokenResponse", "handleGetProduct", "accessToken", "id", "getProduct", "parseProduct", "handleGetOrder", "getOrder", "parseOrder", "handleModels", "models", "listModels", "askOpts", "model", "handleProductAction", "isProductActionId", "product", "content", "runProductAction", "handleOrderAction", "isOrderActionId", "order", "runOrderAction", "handleProductUpdate", "w", "updated", "parseProductUpdate", "updateProduct", "handleWebhookAction", "action", "handleWebhook", "raw", "headerHmac", "hmac", "verifyWebhook", "topic", "routeWebhook", "createHandler", "msg", "main", "readServerConfig", "createServer"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@hanzo/shopify",
3
+ "version": "1.0.0",
4
+ "description": "Hanzo AI for Shopify — an embedded admin app: generate/rewrite product descriptions + SEO, summarize orders and draft customer replies, and ask about a product or order. A Polaris + App Bridge React panel over a Node backend that does Shopify OAuth, proxies the Admin GraphQL API, verifies every Shopify HMAC (OAuth callback + webhooks), and runs the model gateway through the published @hanzo/ai over api.hanzo.ai.",
5
+ "type": "module",
6
+ "dependencies": {
7
+ "@hanzo/ai": "^0.2.0",
8
+ "@hanzo/iam": "^0.13.2",
9
+ "react": "^18.3.1",
10
+ "react-dom": "^18.3.1"
11
+ },
12
+ "peerDependencies": {
13
+ "@shopify/app-bridge-react": "^4.1.6",
14
+ "@shopify/polaris": "^13.9.0",
15
+ "@shopify/shopify-api": "^11.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@shopify/app-bridge-react": "^4.1.6",
19
+ "@shopify/polaris": "^13.9.0",
20
+ "@shopify/shopify-api": "^11.0.0",
21
+ "@types/node": "^20.14.0",
22
+ "@types/react": "^18.3.3",
23
+ "@types/react-dom": "^18.3.0",
24
+ "esbuild": "^0.25.8",
25
+ "typescript": "^5.8.3",
26
+ "vitest": "^3.2.6"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "keywords": [
32
+ "hanzo",
33
+ "shopify",
34
+ "ecommerce",
35
+ "product-content",
36
+ "seo",
37
+ "orders",
38
+ "ai",
39
+ "embedded-app",
40
+ "polaris",
41
+ "app-bridge"
42
+ ],
43
+ "author": "Hanzo AI",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/hanzoai/extension.git",
48
+ "directory": "packages/shopify"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "files": [
54
+ "dist",
55
+ "src",
56
+ "README.md"
57
+ ],
58
+ "main": "dist/server.js",
59
+ "scripts": {
60
+ "build": "node build.js",
61
+ "watch": "node build.js --watch",
62
+ "start": "node dist/server.js",
63
+ "test": "vitest run",
64
+ "typecheck": "tsc --noEmit"
65
+ }
66
+ }
@@ -0,0 +1,37 @@
1
+ // The app root: Polaris AppProvider wrapping the assistant. Polaris needs its
2
+ // AppProvider (theme + i18n) at the root; App Bridge is loaded by the CDN script
3
+ // in index.html (it registers the global `window.shopify`), so there is no React
4
+ // provider for it — components read the session token through the authenticated
5
+ // fetch built in main.tsx. This file is deliberately thin: it wires the provider
6
+ // and hands the built Api + launch context to the Assistant.
7
+
8
+ import { AppProvider, Page, Text, BlockStack, Banner } from '@shopify/polaris';
9
+ import enTranslations from '@shopify/polaris/locales/en.json';
10
+ import { Assistant } from './Assistant';
11
+ import type { Api } from './api';
12
+ import type { LaunchContext } from './context';
13
+
14
+ export interface AppProps {
15
+ api: Api;
16
+ ctx: LaunchContext;
17
+ }
18
+
19
+ export function App({ api, ctx }: AppProps): JSX.Element {
20
+ return (
21
+ <AppProvider i18n={enTranslations}>
22
+ <Page title="Hanzo AI">
23
+ <BlockStack gap="400">
24
+ <Text as="p" variant="bodyMd" tone="subdued">
25
+ Generate product content, handle orders, and ask about your store — powered by the Hanzo model gateway.
26
+ </Text>
27
+ {!ctx.shop && (
28
+ <Banner tone="warning">
29
+ No shop context. Open this app from your Shopify admin so it can act on your store.
30
+ </Banner>
31
+ )}
32
+ <Assistant api={api} ctx={ctx} />
33
+ </BlockStack>
34
+ </Page>
35
+ </AppProvider>
36
+ );
37
+ }