# App Proxy Worker

The single backend for the wine club. Deployed as a Cloudflare Worker, exposed to the
storefront through a Shopify **App Proxy** so all requests are same-origin
(`/apps/wineclub/*`) and signed by Shopify.

## Why this exists (and stays tiny)

Three operations can't run in the browser safely:

| Operation | Why not client-side |
|-----------|--------------------|
| Save selection | Writing a customer metafield needs the Admin API token |
| Charge for extras | Draft-order creation needs the Admin API token |
| Tier upgrade | Appstle API key must stay secret |

Everything else (rendering, swap UI, live pricing) stays in the theme.

## Security model

1. **HMAC verify** every request — Shopify signs App Proxy requests with the app secret.
2. **Trust `logged_in_customer_id`** from Shopify's signed query, never the request body,
   for *who* the customer is.
3. **Re-validate + re-price server-side** — the Worker refetches the tier pool and reads
   live variant prices from Shopify. A tampered client payload can't inject an off-pool
   wine or a fake price; `reprice()` rejects anything not in the pool.
4. Enforce `bottleCount` and `cycleStatus === 'open'` server-side.

## Setup

```bash
cd app-proxy
npm i -g wrangler
wrangler secret put SHOPIFY_APP_PROXY_SECRET
wrangler secret put SHOPIFY_ADMIN_TOKEN
wrangler secret put SHOPIFY_SHOP          # maison-fayard.myshopify.com
wrangler secret put APPSTLE_API_KEY
wrangler deploy
```

Then in the Shopify custom app → **App proxy**:
- Subpath prefix: `apps`
- Subpath: `wineclub`
- Proxy URL: your `*.workers.dev` URL

Admin API scopes needed: `read_customers`, `write_customers`, `read_products`,
`write_draft_orders`.

## Routes

| Route | Body | Returns |
|-------|------|---------|
| `POST /save` | `{cycle, tier, bottles[], addOnBottles[], ...}` | `{ok, total, chargeUrl}` |
| `POST /charge` | `{cycle}` | `{ok, chargeUrl, chargeId}` |
| `POST /upgrade` | `{cycle, targetTier}` | `{ok}` |

## TODO before production

- Confirm the exact **Appstle API** endpoint + payload for a plan swap (the `/upgrade`
  handler is stubbed against a placeholder URL — needs the real contract-update call).
- Decide extras billing: draft-order invoice (current) vs. Appstle one-time charge API.
  Draft order is simpler and gives a hosted invoice URL; Appstle keeps it on the sub.
- Rate-limit / abuse guard on `/save` (Cloudflare rules).
