diff --git a/AGENTS.md b/AGENTS.md index 9851a6f..215c12c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. --- diff --git a/README.md b/README.md index 0771213..8014e2a 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9a86fc0..518a5d7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/CONTENT-WOO.md b/docs/CONTENT-WOO.md index dc19b8a..d89bf6a 100644 --- a/docs/CONTENT-WOO.md +++ b/docs/CONTENT-WOO.md @@ -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`. diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index c71e2ab..cf3ac6a 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -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` diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 6449ac4..86e3540 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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 ` → 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** diff --git a/docs/IMPL-PLAN.md b/docs/IMPL-PLAN.md index 103a995..2fb143e 100644 --- a/docs/IMPL-PLAN.md +++ b/docs/IMPL-PLAN.md @@ -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. diff --git a/docs/OPENAPI.md b/docs/OPENAPI.md index 0b30347..f244c24 100644 --- a/docs/OPENAPI.md +++ b/docs/OPENAPI.md @@ -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": "", "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": "..." }` ---