← back to Stayclaim

REVIEW-2026-05-04.md

74 lines

# stayclaim (pastdoor) — overnight debate-team review (2026-05-04)

Code-reviewer + architect-reviewer parallel run. **3 P0 patches applied + 2 P0 surfaced to Steve** (Maps key rotation + Maps key history scrub). Architecture verdict: stayclaim is **NOT** a `directory-core` consumer — different shape.

## P0 patches APPLIED (local — NOT deployed)

| # | File:line | Issue | Fix |
|---|---|---|---|
| 1 | `src/middleware.ts` | `/admin/*` was completely unauthenticated — `admin/layout.tsx:45` literally said `auth: dev-mode (no auth)`. Anyone hitting `wholivedthere.com/admin` saw the full PG dashboard, claim-request emails, sponsored-placement contact_email | HTTP Basic gate via `ADMIN_SECRET` env var (constant-time compare); fail-closed in production if secret unset (503), allow in dev |
| 2 | `src/middleware.ts` | `/dashboard?email=victim@example.com` returned that user's claim list — trivial IDOR | Block `/dashboard` in production (404); dev still has it for local testing |
| 3 | `src/app/api/health/route.ts` + new `pastdoor-health/route.ts` | `/api/health` collides with Site Factory's nginx reserved prefix — every probe was hitting Site Factory, never reaching pastdoor. `deploy-pastdoor.sh` + `smoke-test.sh` were silently broken | New handler at `/api/pastdoor-health`; old path returns 410 Gone with redirect message. Update `deploy-pastdoor.sh` + `smoke-test.sh` to call new URL. |

## P0 NEEDS STEVE (3 actions)

### 1. **Live Google Maps API key in `.env.local`** (`${GOOGLE_MAPS_API_KEY}-redacted-NEEDS-ROTATION-2026-05-07`)

`NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` ships embedded in the JS bundle to every browser visitor. Required actions:
1. **Rotate immediately via GCP console** — old key compromised by being public.
2. Add HTTP referrer restrictions to the new key (only `wholivedthere.com`, `claimmyaddress.com`, `bubbesblock.com`).
3. Re-save via `~/Projects/secrets-manager/cli.js add` so it's tracked.
4. Confirm the key was never committed to a git history (`git log --all --full-history -- .env.local`).

### 2. Set `ADMIN_SECRET` in pm2 env

Without it, the new `/admin/*` middleware returns 503 in production. Mint via `/secrets` skill, route to pastdoor's pm2 env, `pm2 reload pastdoor --update-env`.

### 3. Update `deploy-pastdoor.sh` + `smoke-test.sh` to call `/api/pastdoor-health`

Old `/api/health` still returns 410 so callers will fail loud, but updating them in the same release is cleaner.

## P1 / P2 deferred (in REVIEW)

- `src/app/admin/data/page.tsx:220` — `getDistinctValues` interpolates `col`/`table` into raw SQL via template literals. Today `SOURCES` is hardcoded so not injectable, but the pattern is fragile. Use allowlist map.
- `src/app/api/submit/route.ts:19`, `api/promote/route.ts:46` — Zod errors return `String(e)` which echoes received PII (emails) back. Map to `issues.map(i=>i.message).join('; ')`.
- `src/app/api/la-records/lookup/route.ts:44` — leaks raw `e.message` (DB / ArcGIS connection details) to client. Generic `lookup failed`; full server-log only.
- No `headers()` export in `next.config.mjs` — missing CSP, X-Frame-Options, X-Content-Type-Options, HSTS.
- No CSRF / Origin check on `/api/submit`, `/api/promote`. Add origin allowlist for the 3 sister domains.
- No rate limiting on any endpoint.
- `src/lib/la-arcgis.ts:160`, `la-permits.ts:53` — `interval '${CACHE_TTL_DAYS} days'` interpolated into SQL (low risk: const number, but fragile pattern).
- `*.archive.org` and `*.flickr.com` wildcards in `next.config.mjs` `remotePatterns` — Next.js Image proxy could be abused via attacker-controlled subdomains. Scope to known hosts.
- `/api/promote` `external_url` accepts `http://localhost`/`http://169.254.169.254` (no SSRF today since URL not fetched server-side, but exploitable when crawler lands).
- **Dark/light toggle missing** per standing rule. Tailwind refactor harder — flagged but deferred per review brief.

## Architecture roadmap

**Architectural Impact: HIGH.** Schema strain from Airbnb→address-history pivot, admin tier was unauth, codebase at the inflection point.

### What stayclaim actually is now (multi-tenant archive masquerading as listing site)

- `db/schema.sql:7-43` — central table is still named **`listing`** with airbnb-shaped columns (`nightly_price`, `currency`, `sleeps`, `amenities jsonb`). Films, restaurants, parcels, stars all FK into `listing.id` as if they were Airbnb listings.
- `src/middleware.ts` reveals this single Next.js process serves **3 domains**: wholivedthere.com (flagship), claimmyaddress.com (onboarding → /submit), bubbesblock.com (community → /neighborhoods). Plus stayclaim/pastdoor itself. The real architectural innovation is the host-routing middleware.

### 5 highest-leverage structural changes

1. **(DONE tonight)** Add admin auth — was the most urgent gap.
2. **(needs Steve)** Rotate the Maps key + domain-restrict it in GCP.
3. **Split `src/lib/db.ts` (679 LOC)** into per-domain repos: `repos/{listings,entities,events,films,restaurants,stars,media,parcels,owners,neighborhoods}.ts`. Same anti-pattern as lawyer/doctor reviews.
4. **Rename `listing` → `place`** (or split it into `place` root + `place_listing_meta` rental-specific). Currently every new vertical inherits Airbnb-shaped baggage. Restaurant has its own table (`016_restaurants.sql`) but `entity_place_association.listing_id` FKs to `listing` only — so a restaurant-as-place can't be associated with a person without a fake listing row.
5. **Document deploy story in this repo.** `deploy-pastdoor.sh` is referenced by `api/health/route.ts` but lives elsewhere. Add `scripts/deploy.sh` checked-in; survive context wipes.

### `directory-core` verdict: **NOT A FIT**

stayclaim is a different shape from doctor/lawyer/animals/restaurant verticals:
- Those four: scrape provider list → enrich → render directory page per provider → 3 mockups → claim/upgrade. **One entity type per site.**
- stayclaim: aggregate **9+ heterogeneous source datasets** (LADBS permits, BH permits, LA assessor, film permits, Walk of Fame, Wikipedia houses, restaurants, newspapers, Sanborn maps) across **6 place types** (address/parcel/entity/film/restaurant/star) into **3 tenant-branded sites** sharing one DB.

What stayclaim COULD share is narrower: a `place-graph-core` package containing (a) the source-tier A/B/C/D pattern, (b) the `public_visible` privacy gate trigger, (c) entity↔place association model, (d) media-asset rights tracking. For bubbesblock/claimmyaddress to consume — not for the directory verticals.

## Files touched

- `/Users/stevestudio2/Projects/stayclaim/src/middleware.ts`
- `/Users/stevestudio2/Projects/stayclaim/src/app/api/health/route.ts`
- `/Users/stevestudio2/Projects/stayclaim/src/app/api/pastdoor-health/route.ts` (NEW)