docs(#5): Woo auth is WordPress Application Passwords

Replace consumer-key auth with dedicated low-priv WP user + application
password over HTTPS Basic Auth. AES-GCM at rest, server-only calls, rotation
via profile revoke. CONTENT-WOO, OPENAPI, ARCHITECTURE, DEPLOYMENT, DATA-MODEL,
AGENTS, IMPL-PLAN, README.
This commit is contained in:
2026-09-02 00:27:46 +00:00
parent fcad50d86e
commit 7bfba9f39f
8 changed files with 81 additions and 29 deletions
+7 -3
View File
@@ -121,11 +121,15 @@ Depth: `docs/TECH-SPEC-PIPELINE.md`.
**Never in browser storage / JS bundles:**
- Woo consumer key + secret
- Woo WordPress username + application password (not consumer keys)
- Inference base URL + API key
- AES master key (`SLAB_AES_KEY` env only; never in DB or repo)
**Server:** AES-GCM at rest in SQLite for Woo + inference secrets. Settings UI POSTs to FastAPI; server encrypts. All Woo and inference calls are server-side.
**Server:** AES-GCM at rest in SQLite for Woo + inference secrets. Settings UI
POSTs to FastAPI; server encrypts. All Woo and inference calls are server-side.
Woo auth is a dedicated low-privilege WP user's application password over HTTPS
Basic Auth to `/wp-json/wc/v3/`. Do not implement WooCommerce consumer keys
(`ck_`/`cs_`) or the Woo → Settings → Advanced → REST API key UI.
**Browser may persist (non-secret):** sheet mode, sensitivity, edge offset, feather (localStorage OK; reset-to-default required).
@@ -178,7 +182,7 @@ Prompt file mechanics: `docs/PROMPTS.md`.
- Deterministic core unit-tested before UI consumes it.
- One phase = reviewable, shippable increment; Ty exit-gates phases.
- Phase naming: Phase 0–6 stay delivery chapters. **Phase 0 = Gates A+B.** **Gate C = early Phase 1** claimable publish slice.
- Do not write secrets, real Woo keys, or AES keys into the repo.
- Do not write secrets, real Woo application passwords, or AES keys into the repo.
- Prefer fixing docs when code and locked decisions diverge — do not silently reintroduce `/12`, offline, ruler, or server happy-path pipeline.
---
+1 -1
View File
@@ -37,7 +37,7 @@ Hard locks live in `AGENTS.md`. Short map:
- **Hybrid:** phone owns mask/sliders/sqft/bdft/3:4 PNG; server stores, Woo, inference proxy, on-demand U2Net.
- **bdft** = sqft × thickness_in (no `/12`). TV-1 through TV-10 on the client.
- **Secrets** never in browser; AES-GCM on server; `SLAB_AES_KEY` env only.
- **Secrets** never in browser; AES-GCM on server; `SLAB_AES_KEY` env only. Woo auth = WP application password (dedicated low-priv user), not consumer keys.
- **Woo UAT:** draft default; `SLAB-UAT-*` force draft (AGENTS §5).
- **Inference OFF** until Gate C; Call 1/2 after, still inside POC.
- **Online-only for POC** (offline PWA deferred).
+21 -11
View File
@@ -89,8 +89,10 @@ flowchart TB
Call 1 results (or manually-entered taxonomy if Call 1 failed) + deterministic
numbers + brand/GEO for prose generation; assembles title/description/short
description from deterministic templates with the LLM prose injected.
6. **Woo** is only FastAPI. Browser never sees the App Password. Duplicate SKU → stop, show error, no edit-same-SKU in MVP.
7. **Settings** (species $/bdft, prompts, endpoint, Woo, publish status) live on the server; the phone is just the form.
6. **Woo** is only FastAPI. Browser never sees the WordPress application
password. Duplicate SKU → stop, show error, no edit-same-SKU in MVP.
7. **Settings** (species $/bdft, prompts, inference endpoint, Woo credentials,
publish status) live on the server; the phone is just the form.
## 4. What runs where
@@ -110,9 +112,13 @@ flowchart TB
## 5. Secrets and settings
**Server-side only (never in browser JS):**
1. WooCommerce consumer key + consumer secret (App Password). Server uses HTTP Basic Auth to call `/wp-json/wc/v3/`. Stored encrypted at rest in SQLite (AES-GCM).
2. Inference endpoint base URL + API key. Stored encrypted. Server proxies all inference calls.
3. AES master key. Env var on server, never in the DB.
1. WordPress username + **application password** for a dedicated low-privilege
WP user. Server uses HTTP Basic Auth (`username:application_password`) against
`/wp-json/wc/v3/`. Stored encrypted at rest in SQLite (AES-GCM). Not Woo
consumer keys. Not the account login password.
2. Inference endpoint base URL + API key. Stored encrypted. Server proxies all
inference calls.
3. AES master key. Env var on server (`SLAB_AES_KEY`), never in the DB.
**Browser-side (no secrets):**
1. User preferences: sheet mode, sensitivity, edge offset, feather. Persisted in localStorage, reset-to-default available.
@@ -120,13 +126,17 @@ flowchart TB
3. Taxonomy snapshot (categories, attributes, tags). Read from server. No auth needed.
**Flow:**
1. User enters Woo credentials in Settings UI on the phone.
2. Browser POSTs them to `/api/settings` on FastAPI. Server stores encrypted.
3. Server syncs taxonomy from Woo, caches in SQLite.
4. Client reads taxonomy from `/api/admin/taxonomy` — no auth needed.
5. All Woo calls go server-to-woo. Browser never calls Woo REST directly.
1. Operator creates a low-privilege WP user on the store (HTTPS required for
Application Passwords UI). Generates an application password under
Users → Profile.
2. User enters that username + application password in Settings UI on the phone.
3. Browser POSTs them to `/api/v1/settings` on FastAPI. Server encrypts with
AES-GCM and stores ciphertext only.
4. Server syncs taxonomy from Woo, caches in SQLite.
5. Client reads taxonomy from `/api/v1/admin/taxonomy` — no auth needed.
6. All Woo calls go server-to-Woo. Browser never calls Woo REST directly.
This is the canonical WooCommerce App Password integration pattern. Browser-to-Woo REST requires CORS configuration on the Woo site, which is not under your control.
Browser-to-Woo REST would need CORS on the store, which this design does not use.
## 6. Status machine
+15 -3
View File
@@ -60,9 +60,21 @@ and the final stored content.
## 2. WooCommerce integration (FR24–28)
### 2.1 Auth & client
- REST v3: `GET/POST/PATCH/DELETE {woo_base_url}/wp-json/wc/v3/…`
- Basic auth: `woo_consumer_key:woo_consumer_secret` (Decision 10; the "username" is
the consumer key, the "app password" is the consumer secret).
- REST v3 surface unchanged: `GET/POST/PATCH/DELETE {woo_base_url}/wp-json/wc/v3/…`
- Auth is **WordPress Application Passwords**, not WooCommerce consumer keys
(`ck_`/`cs_`) and not the user's login password.
- Create a **dedicated low-privilege WordPress user** (shop manager, or a custom
role with product create/edit + media upload). Do not reuse an admin account.
- Generate the application password under **Users → Profile → Application
Passwords**. WordPress requires **HTTPS** for that UI; the store must already
be on TLS (Caddy enforces HTTPS on the app side too).
- Basic auth shape: `woo_wp_username:woo_app_password` (username + application
password). Spaces in the generated password are ignored by WordPress; store
the value as entered.
- FastAPI is the sole Woo caller. Credentials live encrypted at rest in SQLite
(AES-GCM, `SLAB_AES_KEY` env). The browser never holds them.
- Rotation: revoke the application password in the WP user profile, enter a new
one in Settings UI, then `POST /api/v1/settings/test-woo`.
- Client: `httpx` with timeouts (connect 10s, read 60s for image-heavy create).
- `woo_base_url` default: `https://www.whidbeywoodstore.com`.
+3 -1
View File
@@ -175,7 +175,9 @@ authoritative source for tag definitions; this table caches synced Woo tags.
| `updated_at` | TEXT | |
Keys (server settings; secrets encrypted at rest):
- `woo_base_url`, `woo_consumer_key`, `woo_consumer_secret`
- `woo_base_url`, `woo_wp_username`, `woo_app_password`
(username + WordPress application password for a dedicated low-priv WP user;
not WooCommerce consumer keys)
- `inference_base_url`, `inference_api_key`, `inference_model`
- `inference_enabled`, `content_llm_enabled`
- `brand_voice`, `geo_context`
+16 -3
View File
@@ -122,10 +122,23 @@ API must win over the frontend catch-all. Put `handle /api/*` **before** the fro
| Secret | Where | Notes |
|---|---|---|
| `SLAB_AES_KEY` | `.env` on host (mode 600) → compose env | 32-byte base64; NEVER in the repo |
| Woo consumer key/secret | SQLite `settings` (AES-GCM) via Settings UI | rotate in Woo → re-enter in UI |
| Woo WP username + application password | SQLite `settings` (AES-GCM) via Settings UI | dedicated low-priv WP user; not consumer keys |
| Inference API key | SQLite `settings` (AES-GCM) via Settings UI | usually empty for local llama.cpp |
| `.env.example` | repo | placeholders only, committed |
**Woo credential setup (once per store):**
1. Store must already be on **HTTPS**. WordPress will not offer Application
Passwords over plain HTTP.
2. Create a dedicated low-privilege WordPress user (shop manager, or custom role
with product create/edit + media upload). Do not reuse an admin login.
3. Sign in as that user → **Users → Profile → Application Passwords** → generate
a password for "SlabUploader".
4. Enter the username + application password in the app Settings UI. FastAPI
stores them AES-GCM encrypted. Browser never keeps them.
5. Rotate by revoking the old application password in the WP profile and
re-entering a new one in Settings, then `test-woo`.
Key rotation (AES): `SLAB_AES_KEY` change requires re-encrypting settings —
provide `python -m app.security.rekey NEW_KEY` (reads old key from env
`SLAB_AES_KEY_OLD`, decrypts, re-encrypts, updates). Documented in playbook §7.
@@ -156,7 +169,7 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env
| Logs | `docker compose logs -f fastapi` / `frontend` |
| Health | `curl -k https://${APP_HOSTNAME}/api/health` |
| Rekey AES | §5 |
| Rotate Woo creds | Woo UI → new consumer key/secret → Settings UI → `POST /api/settings/test-woo` |
| Rotate Woo creds | WP user profile → revoke old application password → new one → Settings UI → `POST /api/v1/settings/test-woo` |
| Rotate inference key | Settings UI (if external) |
| Edit a prompt | Edit the text file on host, no restart needed (read fresh each time) |
| Rollback image | `git checkout <tag>` → rebuild → up -d; DB down-migrations provided for schema changes |
@@ -165,7 +178,7 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env
## 8. Smoke test (after every deploy)
1. `GET https://${APP_HOSTNAME}/api/health` → ok (from phone over HTTPS once CA trust is proven)
2. Settings: Woo creds + `test-woo` ok; `woo_create_status=draft`; inference OFF for early gates
2. Settings: WP application password + `test-woo` ok; `woo_create_status=draft`; inference OFF for early gates
3. Create a slab → upload test photos (fixture set) → poll to `ready`
4. Verify bdft/price match the fixture's expected values (golden test; bdft = sqft × thickness)
5. Publish test slab with SKU `SLAB-UAT-…` → slab may show local `published`; Woo product must be **draft**
+7 -5
View File
@@ -165,9 +165,10 @@ Deliverables:
(configurable). "Generate text" triggers Call 2; never auto.
- Publish button (online-only), poll `publishing` → `published` | `failed`, surface
error detail from sync_log.
- Settings UI: Woo creds + test, inference endpoint + test (vision check), pricing
rules editor, brand voice/GEO, `woo_create_status`. Sheet/slider prefs stay in
client localStorage. Prompts are server files, not UI settings.
- Settings UI: WP username + application password + test-woo, inference endpoint +
test (vision check), pricing rules editor, brand voice/GEO, `woo_create_status`.
Sheet/slider prefs stay in client localStorage. Prompts are server files, not UI
settings.
- Secrets never appear in GET settings responses (masked `*_configured` only).
Acceptance:
@@ -190,8 +191,9 @@ Media bytes are client-processed **PNGs**. Safety policy from AGENTS §5.
Deliverables:
- Woo client (httpx, Basic auth, timeouts), taxonomy sync into cache tables, auto-assign
per CONTENT-WOO §2.3 (categories + five attributes + fig-*/feat-* tags only).
- Woo client (httpx, Basic auth with WP username + application password, timeouts),
taxonomy sync into cache tables, auto-assign per CONTENT-WOO §2.3 (categories +
five attributes + fig-*/feat-* tags only). No WooCommerce consumer keys.
- Media upload of processed PNG inventory photos; product create via `woo_create_status`
(default **draft**). If SKU matches `SLAB-UAT-*`, force Woo **draft** even when
Settings say publish.
+11 -2
View File
@@ -121,6 +121,7 @@ the call returns. WooCommerce is the authoritative source for `feat-*` definitio
### SettingsView (read; secrets masked)
```jsonc
{ "woo_base_url": "https://www.whidbeywoodstore.com",
"woo_wp_username": "slab-uploader", // non-secret identity; password never returned
"woo_credentials_configured": true,
"inference_base_url": "http://192.168.1.202:8080/v1",
"inference_model": "<model>", "inference_enabled": true,
@@ -130,7 +131,9 @@ the call returns. WooCommerce is the authoritative source for `feat-*` definitio
"aspect_ratio": "3:4", "output_px": 1600, "output_px_min": 1600,
"fill_target": 0.80 }
```
Secrets (keys) are never returned. Only a `*_configured` boolean.
Secrets (application password, inference API key) are never returned. Only a
`*_configured` boolean. Woo auth is WordPress Application Passwords
(`woo_wp_username` + `woo_app_password` write-only), not consumer keys.
Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
---
@@ -206,8 +209,14 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
### Settings
- `GET /api/v1/settings` → `200` `SettingsView`
- `PUT /api/v1/settings` — Update settings. Body: partial `{ key: value }`.
- Secrets re-encrypted server-side. `200` `SettingsView` returned.
- Woo credential keys: `woo_wp_username` (plain) and `woo_app_password`
(write-only; AES-GCM at rest). Reject any payload that still sends
`woo_consumer_key` / `woo_consumer_secret`.
- Secrets re-encrypted server-side. `200` `SettingsView` returned (password
never echoed).
- `POST /api/v1/settings/test-woo` → `200` `{ "ok": bool, "detail": "..." }`
- Uses stored username + application password over HTTPS Basic Auth against
the store REST v3 base. Fails closed if credentials missing or store HTTP.
- `POST /api/v1/settings/test-inference` → `200` `{ "ok": bool, "vision_capable": bool, "detail": "..." }`
---