@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 +12 -0
- package/README.md +172 -0
- package/dist/app.css +2 -0
- package/dist/app.css.map +7 -0
- package/dist/app.js +56 -0
- package/dist/app.js.map +7 -0
- package/dist/index.html +22 -0
- package/dist/server.js +56 -0
- package/dist/server.js.map +7 -0
- package/package.json +66 -0
- package/src/app/App.tsx +37 -0
- package/src/app/Assistant.tsx +330 -0
- package/src/app/api.ts +126 -0
- package/src/app/context.ts +54 -0
- package/src/app/index.html +21 -0
- package/src/app/main.tsx +51 -0
- package/src/app/session-fetch.ts +31 -0
- package/src/config.ts +122 -0
- package/src/server/actions.ts +122 -0
- package/src/server/hanzo.ts +222 -0
- package/src/server/oauth.ts +165 -0
- package/src/server/server.ts +345 -0
- package/src/server/shopify-api.ts +278 -0
- package/src/server/webhooks.ts +90 -0
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.
|