@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.
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright 2025 Hanzo Industries Inc
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
8
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
9
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
10
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
11
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
12
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # Hanzo AI for Shopify
2
+
3
+ AI for your Shopify store, where the store is the system of record. An embedded
4
+ admin app (Polaris + App Bridge) over a small Node backend:
5
+
6
+ - **Product content** — generate or rewrite product **descriptions**, and write
7
+ **SEO titles / meta descriptions**, from the product's own attributes, then
8
+ **write the result back** to the product (`productUpdate`).
9
+ - **Orders & support** — summarize an order, draft a customer reply, and extract
10
+ issues from an order's note.
11
+ - **Ask** — a freeform question about a product or an order, grounded in its data.
12
+
13
+ Every model call goes through the **published `@hanzo/ai`** headless client to the
14
+ Hanzo gateway (`https://api.hanzo.ai/v1`, a Zen model by default). Shopify data
15
+ goes through the **Admin GraphQL API**. The backend holds the Shopify API secret
16
+ and the Hanzo key; the React frontend never sees a secret.
17
+
18
+ ## Architecture
19
+
20
+ ```
21
+ Shopify Admin ──(iframe, App Bridge)──► Polaris React app (src/app/)
22
+ │ session-token fetch
23
+
24
+ Node backend (src/server/)
25
+ ┌───────────────┼────────────────────┐
26
+ │ │ │
27
+ Shopify OAuth Admin GraphQL API @hanzo/ai ──► api.hanzo.ai/v1
28
+ + webhook HMAC (product / order / (createAiClient)
29
+ verification productUpdate)
30
+ ```
31
+
32
+ - `src/server/oauth.ts` — OAuth request shaping + **OAuth-callback HMAC** verify
33
+ (hex over sorted query params) + state (CSRF) check.
34
+ - `src/server/shopify-api.ts` — Admin GraphQL wrappers: `getProduct`, `getOrder`,
35
+ `updateProduct` (the `productUpdate` mutation) — pure request shaping + parsing.
36
+ - `src/server/hanzo.ts` — thin over **`@hanzo/ai`** (`createAiClient`); owns
37
+ product/order context assembly + prompt building. Never reimplements transport.
38
+ - `src/server/actions.ts` — the AI action catalog (product content + order/support),
39
+ one prompt each over the single ask primitive.
40
+ - `src/server/webhooks.ts` — **webhook HMAC** verify (base64 over the RAW body) +
41
+ topic routing (`orders/create`, `app/uninstalled`, the three GDPR topics).
42
+ - `src/server/server.ts` — the HTTP service; the ONLY place the secret + Hanzo key
43
+ live. OAuth, the `/v1/*` proxy the frontend calls, and `/webhooks`.
44
+ - `src/app/` — the Polaris + App Bridge admin panel (`api.ts`, `context.ts`,
45
+ `session-fetch.ts`, `App.tsx`, `Assistant.tsx`, `main.tsx`).
46
+
47
+ All logic-heavy code is **pure and unit-tested**; the HTTP server and the React
48
+ DOM glue are thin over it.
49
+
50
+ ## Create the Shopify app (Partner Dashboard)
51
+
52
+ 1. **Partners → Apps → Create app → Create app manually.** Note the **API key**
53
+ (public — the OAuth `client_id`) and the **API secret key** (server-only — it
54
+ signs the OAuth exchange AND is the HMAC key for the callback and every
55
+ webhook).
56
+ 2. **App setup → App URL:** `https://shopify.hanzo.ai/` (where the embedded app
57
+ is served — see Deploy).
58
+ 3. **App setup → Allowed redirection URL(s):** add
59
+ `https://shopify.hanzo.ai/oauth/callback` (the exact `SHOPIFY_REDIRECT_URI`).
60
+ 4. **Embedded app:** leave "Embed app in Shopify admin" **on** (`embedded = true`).
61
+
62
+ Or manage it all from `shopify.app.toml`:
63
+
64
+ ```bash
65
+ npm i -g @shopify/cli
66
+ shopify app config link # bind shopify.app.toml to your Partner app
67
+ shopify app deploy # push scopes, URLs, webhooks, and the admin-link extension
68
+ ```
69
+
70
+ Put your Partner **API key** into `shopify.app.toml`'s `client_id` (it is public).
71
+ The **secret** never goes in the toml — only in the server environment.
72
+
73
+ ### OAuth scopes
74
+
75
+ `read_products, write_products, read_orders` — declared in `shopify.app.toml`
76
+ (`[access_scopes]`) and in `config.ts` (`OAUTH_SCOPES`). Read/write products for
77
+ content write-back; read orders for support + order insight. Nothing else.
78
+
79
+ ## OAuth flow + HMAC (the trust gates)
80
+
81
+ 1. **Install:** `GET /oauth/install?shop=<store>.myshopify.com` → the server
82
+ validates the shop domain (`isShopDomain`), mints a `state` nonce, and 302s to
83
+ `https://{shop}/admin/oauth/authorize?client_id&scope&redirect_uri&state`
84
+ (offline access — a long-lived token the webhook can use).
85
+ 2. **Callback:** Shopify redirects to `/oauth/callback?code&hmac&shop&state&…`.
86
+ The server, **in order**:
87
+ - **verifies the `hmac`** — `hex(HMAC-SHA256(apiSecret, sortedParams))`,
88
+ constant-time compare (`verifyCallbackHmac`). A bad signature is a `401`
89
+ before anything else.
90
+ - checks `state` against the nonce it issued (CSRF).
91
+ - exchanges the `code` at `POST https://{shop}/admin/oauth/access_token`
92
+ (secret in the JSON body, over TLS) for the **offline access token**, stored
93
+ server-side keyed by shop.
94
+ 3. **Webhooks:** `POST /webhooks` — the server **verifies the
95
+ `X-Shopify-Hmac-Sha256` header** — `base64(HMAC-SHA256(apiSecret, RAW_BODY))`,
96
+ computed over the raw bytes, constant-time compare (`verifyWebhook`). A bad
97
+ signature is a `401` before any action. Then it routes on `X-Shopify-Topic`.
98
+
99
+ > The OAuth callback HMAC (hex, sorted query params) and the webhook HMAC (base64,
100
+ > raw body) are **different** signatures — both keyed by the same API secret. Both
101
+ > are implemented and tested here.
102
+
103
+ ## Product write-back flow
104
+
105
+ 1. The panel loads a product: `GET /v1/product?shop=&id=` → Admin GraphQL
106
+ `product(id:)`.
107
+ 2. The merchant clicks e.g. **Rewrite description**:
108
+ `POST /v1/product/action {shop,id,action,model}` → the server reads the product,
109
+ assembles a grounded prompt (the product's attributes, fenced as data), runs
110
+ `@hanzo/ai`, and returns the generated content.
111
+ 3. The panel previews the content in an editable box. On **Save to product**:
112
+ `POST /v1/product/update {shop,id,descriptionHtml | seoTitle | seoDescription}`
113
+ → the server sends the **`productUpdate`** mutation with **only the changed
114
+ field** (a description rewrite never clobbers the SEO title), surfacing any
115
+ `userErrors` as a real failure.
116
+
117
+ ## Run it
118
+
119
+ ```bash
120
+ pnpm install
121
+ pnpm build # → dist/index.html + dist/app.js + dist/app.css (app), dist/server.js (service)
122
+ pnpm test # vitest — the pure modules
123
+ pnpm typecheck # tsc --noEmit
124
+
125
+ # The service (holds the secret + Hanzo key):
126
+ SHOPIFY_API_KEY=... # public client id
127
+ SHOPIFY_API_SECRET=... # SERVER ONLY — OAuth + all HMAC
128
+ SHOPIFY_REDIRECT_URI=https://shopify.hanzo.ai/oauth/callback
129
+ HANZO_API_KEY=hk-... # optional — lets the webhook run a model call
130
+ PORT=8791
131
+ node dist/server.js
132
+ ```
133
+
134
+ The app bundle is built with the **public** API key stamped into its
135
+ `shopify-api-key` meta tag (App Bridge needs it); the **secret is never bundled**
136
+ — `SHOPIFY_API_SECRET` is read from the server's environment only.
137
+
138
+ ```bash
139
+ SHOPIFY_API_KEY=<public-key> HANZO_SHOPIFY_BASE=https://shopify.hanzo.ai pnpm build
140
+ ```
141
+
142
+ ## Deploy (embedded app + App Store path)
143
+
144
+ - **App** (`dist/index.html` + `app.js` + `app.css`) is served as static assets
145
+ over `hanzoai/static` behind `hanzoai/ingress` at the **App URL**
146
+ (`https://shopify.hanzo.ai/`). Shopify loads it in the admin iframe; App Bridge
147
+ (loaded from Shopify's CDN) provides the session token the frontend sends to the
148
+ service.
149
+ - **Service** (`dist/server.js`) runs behind the same ingress: `/oauth/*`,
150
+ `/v1/*`, `/webhooks`, `/healthz`. In production, persist the per-shop offline
151
+ token and the OAuth `state` nonces to KMS/Valkey (the in-memory maps in
152
+ `server.ts` are single-instance).
153
+ - **Extension:** `extensions/product-assistant` is an admin **link** that adds
154
+ "Hanzo AI" to the product and order detail pages and deep-links the embedded app
155
+ with the resource in context. `shopify app deploy` ships it.
156
+ - **App Store:** an embedded app that (a) uses **session-token auth** (App Bridge),
157
+ (b) verifies **OAuth + webhook HMAC**, and (c) subscribes to the three
158
+ **GDPR/privacy webhooks** (`customers/data_request`, `customers/redact`,
159
+ `shop/redact` — all routed and acknowledged here) meets the core App Store
160
+ review requirements. Submit from the Partner Dashboard after `shopify app deploy`.
161
+
162
+ ## Security notes
163
+
164
+ - The Shopify **API secret** and the **Hanzo key** live only in the server
165
+ environment — never in the app bundle, never in `shopify.app.toml`.
166
+ - Every OAuth callback and every webhook is **HMAC-verified** (constant-time)
167
+ before it is trusted.
168
+ - The `shop` parameter is validated against `*.myshopify.com` (`isShopDomain`)
169
+ before it is ever placed in a URL — no request can be pointed at an arbitrary
170
+ host.
171
+ - Model calls use `@hanzo/ai` against `api.hanzo.ai/v1` — no `/api/` prefix, one
172
+ gateway, the same wire shape as every other Hanzo surface.