📄

RecipeMemoir code review, August 2026

Private deliverable. Enter the hub password to continue.

That's not right. Try again.

← Back to the hub

RecipeMemoir code review, August 2026

Run by Codex CLI (OpenAI billing, model gpt-5.5), read-only, no files changed. Everything below the first section is Codex's own analysis. The first section is mine, found while checking Codex's top claim against your live Cloudflare setup.


🔴 Fix this first: your preview URLs are wide open, against the live database

What is happening. RecipeMemoir's write protection is a shared secret called RM_WRITE_KEY. The code says: if that key is not configured, let everybody through. Your production site has the key set, so production is fine. Your preview environment does not have it set, and preview is wired to the same live database and the same live photo bucket as production.

So anyone who lands on a preview URL gets owner-level access to your real recipes, your real photos, and the copyright-held recipes that are supposed to stay private until you review them.

Proof. I asked the same owner-only endpoint on each, as an anonymous visitor with no login:

URL Response
recipememoir.com (production) 401 Unauthorized ✅ correct
REDACTED.recipememoir-styles.pages.dev (preview) 200 OK ❌ let straight in
REDACTED.recipememoir-styles.pages.dev (preview) 200 OK ❌ let straight in

Three preview deployments from the dev branch have been live since July 28, 2026. Both environments bind the same D1 database (DB) and the same R2 bucket (PHOTOS).

I did not test any write, because a write would have changed your real data. The read result is enough: the same gate gates both.

How to close it. Three steps, smallest first:

  1. Delete the three dev preview deployments. Removes the live exposure immediately. Irreversible, so it is your call.
  2. Set RM_WRITE_KEY on the preview environment too. Stops the next preview deploy reopening the hole.
  3. Make the code fail closed (item 1 in Codex's list below). The durable fix, so a missing key never again means "let everyone in."

Say the word and I will do all three. I have not touched your Cloudflare settings or deleted anything.

The three exact preview URLs and the endpoint used to prove this are deliberately left out of this page. A hub page is a courtesy lock and its content sits in the page source, so naming them here while the hole is open would make it easier to find. They are in the full copy in the RecipeMemoir repo at reports/codex-review-2026-08-12.md.


Verified by me

I spot-checked Codex's two most serious claims against the actual source. Both are real:

  • Item 1, write auth fails open: confirmed at style-gallery/functions/api/lib/util.mjs:27. The line reads if (!expected) return true;.
  • Item 3, magic-link sign-in tokens: confirmed at style-gallery/functions/api/[[path]].js:1500-1504. The code checks the token is unused, then marks it used as a separate step. Two clicks arriving at the same moment can both pass the check and both get a session from one link.

The remaining items are Codex's assessment and I have not verified them individually.


Codex's prioritized list

Ranked by impact divided by effort. Effort: S is under an hour, M is a few hours, L is a day or more.

1. Fail closed when RM_WRITE_KEY is missing Files: style-gallery/functions/api/lib/util.mjs:25-28, style-gallery/functions/api/[[path]].js:714,733-754. Problem: isWriteAuthorized() returns true when RM_WRITE_KEY is absent. That makes anonymous users pass the global write gate, and also exposes pending content and held photos. Fix: require an exact secret match by default. Allow open writes only under an explicit local flag such as RM_ALLOW_OPEN_DEV_WRITES=1, ideally limited to localhost. Add a regression test for the missing secret. Effort: S. Impact: high on security and user data integrity.

2. Add a safe public URL fetcher for imports and image capture Files: style-gallery/functions/api/[[path]].js:1624-1640,1667-1678,1783-1794,1860-1869, style-gallery/functions/api/lib/jsonld-recipe.mjs:184-185. Problem: user supplied URLs and JSON-LD image URLs are fetched server-side with only an http/https check. That leaves request-forgery risk against localhost, private networks, cloud metadata endpoints, redirects, and oversized responses. Fix: centralize fetching through one guarded helper: block private and link-local addresses, localhost, metadata IPs, suspicious redirects, and cap bytes before reading. Require image content types for photo capture. Effort: M. Impact: high on security and cost.

3. Consume magic-link tokens atomically Files: style-gallery/functions/api/[[path]].js:1496-1509, import/migrations/008_auth.sql:5-10. Problem: the verify flow selects an unconsumed token, then updates it. Two concurrent requests can both pass the select and create two sessions from one token. Fix: replace select-then-update with a single conditional update, then create a session only if exactly one row changed. Effort: S. Impact: high on auth correctness and security.

4. Rate-limit magic-link requests Files: style-gallery/functions/api/[[path]].js:1480-1493. Problem: /api/auth/request-link can send emails without a visible per-email or per-IP cooldown, while other public endpoints already have rate limits. That is a cost and abuse path. Fix: add per-email and per-IP throttles, return a generic response, and log attempts. Effort: S. Impact: medium-high on cost and abuse resistance.

5. Harden family photo unlocks Files: style-gallery/functions/api/[[path]].js:1406-1425,1452-1455. Problem: /api/family/unlock accepts anonymous password guesses, uses a fast hash, and allows 4-character passwords. A successful guess exposes third-party held photos for that owner. Fix: add per-owner and per-IP throttling, raise the minimum password strength, and migrate to a salted slow hash while keeping old hashes readable during migration. Effort: M. Impact: medium-high on privacy and security.

6. Scope export child-table reads to the selected collection Files: style-gallery/functions/api/[[path]].js:603-617. Problem: export selects recipes by collection, but ingredients, directions, tags, and nutrition load from every recipe in the database and are filtered afterward in memory. Cost grows with every tenant, and unrelated users' rows sit in memory during one export. Fix: join each child query to recipes and filter by collection and archive state in SQL. Effort: S-M. Impact: medium on speed, cost, and tenant isolation.

7. Scope cook streaks by collection Files: style-gallery/functions/api/[[path]].js:3429-3433, style-gallery/functions/api/lib/util.mjs:34-42. Problem: /api/site/streak reads all cook_log rows without filtering by collection, so one user's streak can include another collection's cooking history. Fix: join cook_log to recipes and filter by the active collection, or add and backfill a collection column on cook_log. Effort: S. Impact: medium on user correctness and privacy.

8. Stop returning full edited photo data inside recipe detail JSON Files: style-gallery/functions/api/[[path]].js:5263-5301, test/photos.test.mjs:28. Problem: recipe detail returns the whole edited photo inline when one exists, even though it also returns a photo URL and there is a dedicated photo endpoint. Normal recipe JSON can get very large on mobile. Existing photo tests cover a helper, not this route. Fix: return only the photo URL and metadata from public recipe detail. If the editor needs the raw image, put that behind an owner-only endpoint it opts into. Effort: M. Impact: medium-high on speed, bandwidth, and mobile experience.

9. Retry staging recipe inserts only on actual slug conflicts Files: style-gallery/functions/api/[[path]].js:268-285,1540-1558. Problem: insert helpers catch any database error, change the slug, and retry. Real failures such as schema, bind, or constraint errors get hidden as random suffix attempts and then a generic failure. Fix: inspect the error and retry only for unique slug conflicts. Rethrow or return a structured error for everything else. Effort: S. Impact: medium on data integrity and debuggability.

10. Validate import uploads before storing large bodies Files: style-gallery/functions/api/lib/import-job.mjs:52-93. Problem: upload streams the request body straight to storage before checking size, type, or ZIP signature. Bad or huge uploads consume storage and only fail later. Fix: require a content length under a configured cap, reject missing or excessive bodies, check the ZIP signature, and delete the stored object immediately if validation fails. Effort: S-M. Impact: medium on cost, abuse resistance, and import experience.

11. Make imported nutrition number parsing stricter Files: style-gallery/functions/api/lib/jsonld-recipe.mjs:142,147-158, test/jsonld-recipe.test.mjs. Problem: the number parser grabs the first run of digits. A value like 1,250 kcal becomes 1, and ranges or multiple numbers get guessed instead of flagged as low confidence. Fix: normalize thousands separators, reject ambiguous multiple-number strings unless the policy is explicit, and add tests for comma numbers, ranges, and approximate values. Effort: S. Impact: medium on nutrition correctness.

12. Reuse a safer servings parser for imported recipe yield Files: style-gallery/functions/api/lib/jsonld-recipe.mjs:167-178, style-gallery/site/lib/paprika-parse.mjs:75-90. Problem: yield parsing takes the first integer, so 2 loaves or 30-32 cookies can be misread as servings, which then corrupts per-serving nutrition. Fix: centralize servings parsing with confidence rules: convert obvious servings, reject non-serving units unless mapped, handle ranges deliberately, and send low-confidence imports to review. Effort: M. Impact: medium on nutrition and import data integrity.

13. Resolve the import publishing policy contradiction Files: style-gallery/functions/api/[[path]].js:263-267,1653-1656,1855-1858. Problem: comments say URL and video imports should stay owner-only until copyright review, but the import paths publish immediately. Codex flagged this as a question rather than an assertion, because the nearby comments conflict. Fix: make the product decision explicit. If immediate publishing is intended, update the comments and add a regression test. If review is required, hold imported directions as pending until approved. Effort: M. Impact: medium on legal risk and data governance.

14. Add route-level tests for the riskiest guards Files: test/, plus the routes above. Problem: the suite has many helper tests, but no direct tests for a missing write key, private URL rejection, magic-link double use, auth-link throttling, family unlock throttling, collection-scoped exports, or collection-scoped streaks. Fix: extract small pure guards where needed and add focused route or mocked-database tests for those cases. Effort: M. Impact: high on maintainability and security regression prevention.

15. Bring validation coverage up to current owner tools Files: validate.config.json, validation-report.md, style-gallery/site/photo-triage.html:58. Problem: the checked-in validation report is green, but appears to cover fewer pages than the current config. photo-triage.html also has a fixed modal, and fixed or sticky UI has been a recurring project risk. Fix: include the owner and admin pages that matter, and either document allowed modal exceptions or refactor them to match the project rule. Effort: S-M. Impact: medium on accessibility and UI regression prevention.

Codex's own top three, in order: fail closed on the missing write key, add safe server-side URL fetching, then fix magic-link consumption and throttling together.


Three side findings

  1. Three stray null characters sit in style-gallery/functions/api/[[path]].js at roughly lines 488, 931, and 2184. Search tools treat the file as binary because of them and skip it silently, which means any grep-based check across your codebase has quietly been ignoring your largest API file. Harmless to the running app, annoying for tooling.
  2. Two of your skill files have malformed YAML headers: evaluate-idea and weekly-checkin in ~/.agents/skills/. Claude Code tolerates them, Codex refuses to load them, so Codex loses those two skills on every run.
  3. Your Codex model pin is broken. ~/.codex/config.toml pins gpt-5.6-sol, which the installed CLI (0.130.0-alpha.5) is too old to use, so every Codex run fails with a 400 before doing any work. This review had to be forced onto gpt-5.5. codex update cannot fix itself here because it cannot detect how it was installed.

Published to Annette's hub. Rebuilt from the source markdown, so edit the source and rerun rather than editing this page.