Commit Graph
8 Commits
Author SHA1 Message Date
twooeyandClaude Sonnet 5 8b149a672d Restrict production wp-admin/wp-login to an IP allowlist (opt-in)
New WP_ADMIN_ALLOWED_IPS env var (space-separated CIDR ranges, matching
Caddy's remote_ip syntax directly). Defaults to 0.0.0.0/0 ::/0 (today's
open behavior) when unset, resolved at the Compose level rather than in
the Caddyfile so an unset var can never leave `remote_ip` with zero
arguments and fail Caddy's startup. admin-ajax.php stays exempt since
WooCommerce routes logged-out storefront AJAX through it. Production
only — staging is already LAN-only.

Verified live: a scratch Caddy instance on a Docker network, with a
sibling container's actual observed source IP used to prove both the
blocked (403 on wp-admin/wp-login, everything else unaffected) and
allowed (matching CIDR lets it through) cases, plus `docker compose
config` to confirm the unset-var default resolves correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MqhgnCRjjVLFnZ3Jpn93Vg
2026-08-28 11:15:55 -04:00
twooeyandClaude Sonnet 5 a733ba4bfd Week 4 (scoped): BooksRun live price/stock check + daily FTP sync stub
Adds a live per-ISBN BooksRun API check to the checkout-time price
recheck (Order\PriceRecheck) so a booksrun-sourced offer is verified
fresh, not trusted from a potentially day-stale local cache -- fails
closed on any problem (API error, no stock), matching Appendix B's
"stale supplier cost must never be trusted as final." Live values are
written back to the same offer_id row so SupplierOrderJob's later
fulfillment-time read sees them too.

Also wires a real daily Action Scheduler job (scheduling/dedup/batch-write
infrastructure) for BooksRun's FTP inventory feed, but leaves the actual
connect+parse step (Import\BooksRunOfferSync::connect_and_parse_feed())
as a deliberate stub -- no FTP access or sample feed exists yet, and
guessing at a file format would silently write wrong data once real
access arrives. The stub fails loud on the manual CLI trigger, quiet on
the scheduled job (no point alarming on an expected, known state).

Ingram, BooksRun AutoBuy, production email, and tax/legal/monitoring are
explicitly out of scope for this pass -- narrowed by user request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2gmgaDVXcRGCwLfdXQajD
2026-08-27 21:29:37 -04:00
twooey e4ee584e86 Fix six low-severity issues from the full-session code review
- PricingEngine::round_to_99(): ceil($price) - 0.01 undershoots whenever
  $price's cents are already .99 or higher — an exact integer (ceil()
  equals floor(), landing a full cent below $price) or, more subtly, any
  fractional price above X.99 itself (a division result, not something
  pre-rounded to 2 decimals, e.g. 20.995 -> old formula gave 20.99, below
  the input). Rewritten as floor()+0.99, bumped by 1 if still under $price.
  Verified against 8 cases including both boundary classes: every result
  now >= its input.

- CoverSync::attach_cover(): wp_generate_attachment_metadata()'s return
  value was never checked. Assumed it'd return empty on failure — verified
  directly it does NOT: fed it 2000 bytes of garbage and got back
  ['filesize' => 2000], no width/height, since GD/Imagick couldn't decode
  it. Old code would report "attached" for a degraded image with no
  dimensions/srcset. Now checks for width+height specifically, and cleans
  up the orphaned attachment on failure so a re-run retries the product.
  Verified both the corrupt-image rejection (no orphan left, no thumbnail
  set) and that a real image still attaches normally.

- Makefile: the per-invocation .env.$(ENV).compose file (holds every API
  key and both DB passwords, stripped of DB_PASSWORD/DB_ROOT_PASSWORD only)
  was never cleaned up, left at default 644 in the repo root after every
  `make` command. Now chmod 600 on creation and removed at the end of every
  target, preserving the underlying command's exit code through the
  cleanup. Verified both the happy path (file gone after, exit 0) and the
  failure path (bad ENV: file still cleaned up, real exit code still
  propagates through make).

- docker-compose.yml: added a healthcheck to the wordpress service (bash's
  /dev/tcp against php-fpm's port 9000 — no HTTP endpoint to hit directly,
  and no `nc` in this image; verified it correctly succeeds once php-fpm is
  listening and fails against a closed port) and switched cron's and
  caddy's depends_on (across all three env overlays) from bare
  container-started to condition: service_healthy. Previously both could
  start against a wordpress container that had started but wasn't actually
  ready yet. Verified via a full down/up cycle: db+redis healthy, then
  wordpress starts and becomes healthy, only then do cron and caddy start.

- .env.dev/.env.staging/.env.production chmod'd 600 (were 644) — same
  plaintext-credential content as secrets/<env>/, which is already 700/644
  at the directory/file level respectively for a different reason (container
  UID readability); these have no such constraint, only the host CLI reads
  them. Also removed a stray .env.staging.compose left over from before the
  Makefile fix above existed. Noted the convention in .env.example so newly
  created env files follow it too.
2026-08-27 15:57:13 -04:00
twooeyandClaude Sonnet 5 71b8065f20 AO3-style tag browsing: taxonomies, display, Hardcover sync (schema unverified)
Four flat taxonomies attached to product (bsc_genre, bsc_mood,
bsc_content_warning, bsc_tag) reuse WordPress's native taxonomy archive
system for the "click a tag, see everything with it" browsing AO3 is known
for — no custom archive templates or query logic needed. Verified: all
four register correctly, render as clickable chips on the product page
(content warnings get a distinct notice instead of just another chip),
and the archive page + term count both work end-to-end with real test
data.

HardcoverAdapter + `wp bookstore sync-hardcover-tags` pull genre/mood/
content-warning/freeform tags from Hardcover's API to populate these.
This part is explicitly NOT verified against a live response — Hardcover's
API is in beta with informal docs, and there was no API token available
to confirm the exact query/response shape. Flagged clearly in the adapter
itself; needs a real token + introspection query before trusting the
field-parsing logic in production. ISBN-first-then-title/author matching
handles both real future ISBNs and the current Gutenberg-synthetic
catalog's fake ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 13:59:30 -04:00
twooeyandClaude Sonnet 5 3876a6ba30 Fix DB password corruption via Docker secrets file mechanism
The reported bug (a generated password containing "$BRjTx3tlmSpz" got
silently blanked, breaking the DB connection) is the same Compose
interpolation issue as the earlier bcrypt hash, but this time in fields
that are genuinely user-chosen and can't just be avoided by convention.

Switched DB_PASSWORD/DB_ROOT_PASSWORD to Docker's official `_FILE`
secrets convention (MARIADB_PASSWORD_FILE / WORDPRESS_DB_PASSWORD_FILE),
backed by Compose's native `secrets:` mechanism: deploy.sh writes the raw
value to secrets/<env>/db_password, and the container reads that file
directly — the value never passes through Compose's ${VAR} interpolation
at all. Verified end-to-end with an actual `$`-containing password,
including a full deploy → backup → restore → still-serving round trip.

Also fixed along the way (found while actually testing, not assumed):
- Makefile never exported ENVIRONMENT, so `make up` alone (bypassing
  deploy.sh) would have left the new secrets path unresolved.
- deploy.sh chmod'd the secret files 600, unreadable by the container's
  own UID (www-data) — fixed to 644, relying on the containing directory
  (700) to keep other host users out instead.
- backup.sh/restore.sh still called `mysqldump`/`mysql`, which don't
  exist in the mariadb:11 image under those names — renamed to
  mariadb-dump/mariadb. (This means neither script had actually
  succeeded before now; both are verified working end-to-end here.)

The supplier/payment API keys remain passed the old way — nothing reads
them yet (bookstore-core is still a stub), so there's no live bug to fix
there; noted in .env.example that the same _FILE pattern should be used
once that code exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 11:36:29 -04:00
twooeyandClaude Sonnet 5 cd48f85a47 Remove staging basic-auth wall — box is LAN-only
Staging runs on a private-network VM, not reachable from outside, so the
HTTP basic-auth layer was unnecessary defense-in-depth. Removing it also
lets the Caddyfile drop the render-around-Compose workaround entirely
(that workaround existed specifically because Compose's interpolation
mangles a bcrypt hash) — the noindex header and blog_public=0 stay, since
those guard against search-engine indexing, a separate concern from
network-level access.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 11:23:35 -04:00
twooeyandClaude Sonnet 5 b43852e733 Switch deploy pipeline to Gitea polling; fix env-parsing and volume-isolation bugs
- Replace the bare-repo post-receive hook with deploy/poll-deploy.sh: Gitea
  and the Docker hosts are separate machines, so each box polls its branch
  via host crontab instead of needing an exposed webhook receiver.
- Add Blocksy theme + Blocksy Companion auto-install to deploy.sh (free
  tier; the paid Book Store starter site still needs a manual license step).
- Fix deploy.sh/backup.sh/restore.sh sourcing .env files as bash: a bcrypt
  hash's `$2a$14$...` shape breaks under `set -u`. Replaced with
  deploy/lib/env.sh, a literal (non-executing) KEY=VALUE reader.
- Fix docker compose itself mangling the same kind of value: both
  `environment: ${VAR}` and `env_file:` run values through Compose's
  interpolation, which silently blanks `$identifier`-shaped substrings.
  The staging basic-auth hash is now rendered directly into the Caddyfile
  by deploy.sh, bypassing Compose's variable system entirely.
- Fix dev/staging/production silently sharing one Compose project (and
  therefore one db_data volume) by pinning an explicit -p per environment.
- cron and wordpress now share one environment anchor so they can't drift
  apart again (cron was silently missing WORDPRESS_CONFIG_EXTRA before).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 10:10:12 -04:00
twooeyandClaude Sonnet 5 c9d637c907 Week 1 infrastructure: Docker environments, deploy pipeline, bookstore-core scaffold
Docker Compose environments for dev/staging/production (MariaDB, Redis,
Caddy, Action Scheduler cron sidecar), an idempotent deploy script,
git-hook-based deploy pipeline, backup/restore scripts, and the
bookstore-core plugin stub with WooCommerce HPOS compatibility declared.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 09:50:18 -04:00