From ae3dd24cbc485a9bc374d7a2614439fa5c33cf66 Mon Sep 17 00:00:00 2001 From: Luan Trindade Date: Wed, 23 Sep 2026 08:41:49 -0300 Subject: [PATCH] docs: record C7 production hydrated-browser smoke Close the last acceptance criterion of Priority 2 with the production stack evidence: hydrated browser calls, SSR metadata, runtime config, forged Host header, Reverb websocket and production dev-login. Record the facts the smoke exposed: seeding is broken in the --no-dev production image, no CSP header is served anywhere, and the API answers Access-Control-Allow-Origin: *. Co-Authored-By: Claude Opus 5 --- brain/canonico/CURRENT_STATE.md | 12 +++++- brain/canonico/NEXT_ACTIONS.md | 33 +++++++++++++--- .../2026-09-23-production-hydrated-smoke.md | 38 +++++++++++++++++++ 3 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 brain/handoffs/2026-09-23-production-hydrated-smoke.md diff --git a/brain/canonico/CURRENT_STATE.md b/brain/canonico/CURRENT_STATE.md index f4dec55..f085efa 100644 --- a/brain/canonico/CURRENT_STATE.md +++ b/brain/canonico/CURRENT_STATE.md @@ -1,6 +1,6 @@ # Current State -Updated: 2026-09-20 +Updated: 2026-09-23 ## Product Status @@ -23,7 +23,7 @@ The product is positioned as a documentation discovery platform for plugin ecosy - Scheduled sync command for latest plugin versions. - Reverb private channels by community for ingestion status and command index updates. - Angular SSR shell with lazy standalone routes, Signals-based UI state, Transloco i18n, command palette, protected routes, and reusable UI components. -- Browser/SSR runtime configuration separates internal SSR API calls from public browser API, Reverb, allowed hosts, and canonical public origin. +- Browser/SSR runtime configuration separates internal SSR API calls from public browser API, Reverb, allowed hosts, and canonical public origin. Proven in the production stack: the hydrated browser calls only the configured public origin, `publicUrlFor()` in `frontend/src/server.ts` builds canonical and metadata from `COMMANDSPHERE_PUBLIC_ORIGIN` and ignores the request `Host`, and the browser websocket reaches the configured public Reverb host even though the `frontend` service still pins `COMMANDSPHERE_REVERB_HOST: localhost`. - Markdown viewer sanitization and heading/code enhancement. - Server-side HTML sanitization of `content_html` in two layers: allowlist sanitizer applied at ingestion and again through a `Document` accessor on every read, so stored documents are served sanitized without rewriting the column. - SEO metadata, canonical URLs, Open Graph, JSON-LD, robots, sitemap, and portfolio screenshots, with SSR post-processing normalizing public origin metadata. @@ -101,6 +101,14 @@ Documented evidence in `README.md` and `docs/PROGRESS.md` says v1 has passed pha Current Brain bootstrap did not rerun the full product gate set because this change only adds documentation. Future product changes must run relevant gates and record `VALIDATED` versus `PENDING`. +## Production Stack Facts + +Confirmed on 2026-09-23 by running `docker-compose.prod.yml` with public values that differ from the defaults, twice and independently. Details in `brain/handoffs/2026-09-23-production-hydrated-smoke.md`. + +- Seeding does not work in the production image. `fakerphp/faker` sits in `require-dev` in `backend/composer.json` while `docker/backend.prod.Dockerfile` installs with `composer install --no-dev`, so any factory calling `fake()` raises `Call to undefined function Database\Factories\fake()`. Migrations and Scout index sync work normally. +- No `Content-Security-Policy` header is served by SSR, by `/runtime-config.js`, or by the API. A clean browser console proves the absence of a policy, not compliance with one. +- The API answers `Access-Control-Allow-Origin: *`, the Laravel default with `config/cors.php` unpublished, so a cross-origin public frontend is not blocked and no origin is restricted either. + ## Known Constraints - Real Discord OAuth requires operator-provided credentials. diff --git a/brain/canonico/NEXT_ACTIONS.md b/brain/canonico/NEXT_ACTIONS.md index 4970b2f..22ac730 100644 --- a/brain/canonico/NEXT_ACTIONS.md +++ b/brain/canonico/NEXT_ACTIONS.md @@ -1,6 +1,6 @@ # Next Actions -Updated: 2026-09-20 +Updated: 2026-09-23 This roadmap is intentionally product-oriented. New work should strengthen portfolio signal, production realism, and architecture maturity rather than adding generic CRUD. @@ -30,7 +30,7 @@ Risks: Problem: The browser API base URL is hardcoded to `http://localhost:8000/api/v1`, while production Compose only configures the SSR server-side API URL. Reverb runtime config also relies on local defaults/localStorage. -Status: Implemented in branch `fix/runtime-production-config` as the first build-loop remediation phase. Keep the Docker production hydrated-browser smoke as a remaining validation item before treating this as fully production-proven. +Status: Implemented in branch `fix/runtime-production-config` and production-proven. The Docker production hydrated-browser smoke ran on 2026-09-23 against `docker-compose.prod.yml` with public values that differ from the defaults, under adversarial audit, and closed the last validation item. See `brain/handoffs/2026-09-23-production-hydrated-smoke.md`. Recommended direction: @@ -42,8 +42,10 @@ Acceptance criteria: - Production-mode browser calls do not point to localhost. `VALIDATED` through SSR artifact smoke with public runtime config. - SSR uses the configured production origin for canonical, Open Graph, and JSON-LD metadata. `VALIDATED` through SSR artifact smoke. -- Reverb uses documented production runtime config. `VALIDATED` at configuration level; websocket integration remains part of Docker/E2E validation. -- Docker production smoke validates hydrated browser API calls. +- Reverb uses documented production runtime config. `VALIDATED` in the production stack: the browser opens `ws://:/app/` from the served runtime config, and Reverb answers `pusher:connection_established`. The public variable wins over the fixed `COMMANDSPHERE_REVERB_HOST: localhost` in the `frontend` service. +- Docker production smoke validates hydrated browser API calls. `VALIDATED`: 53 post-hydration requests, every API call on the configured origin, zero requests to the default origin, which was bound away and answered connection refused. +- SSR ignores a forged `Host` header for canonical and metadata. `VALIDATED`: `publicUrlFor()` in `frontend/src/server.ts` builds from `COMMANDSPHERE_PUBLIC_ORIGIN` only. +- Production `dev-login` stays disabled. `VALIDATED`: `403 auth.dev_login_disabled`, refused before request validation. Risks: @@ -268,9 +270,30 @@ Risks: - An Angular major upgrade touches SSR, build and tests at once; it needs its own branch, gates and audit. +## Priority 13 - Seeding Is Broken In The Production Image + +Problem: `fakerphp/faker` is declared only in `require-dev` in `backend/composer.json`, and `docker/backend.prod.Dockerfile` installs with `composer install --no-dev`. Every factory that calls `fake()`, starting at `backend/database/factories/UserFactory.php`, is therefore undefined in the production image, so `php artisan migrate --seed` and `php artisan db:seed` abort with `Call to undefined function Database\Factories\fake()`. + +Reproduced twice and independently on 2026-09-23, in the smoke and in the audit. + +Recommended direction: + +- Decide whether the production image is supposed to seed at all. If it is, move `fakerphp/faker` to `require`, or split demo factories from the production-facing seeder so that the production path has no Faker dependency. +- Keep the split explicit, so a demo or portfolio dataset never becomes a production seeding requirement by accident. + +Acceptance criteria: + +- Seeding either succeeds in the `--no-dev` image or is explicitly documented as unsupported there, with the supported path written down. +- The production Docker smoke can create a dataset without direct SQL inserts. + +Risks: + +- Moving Faker to `require` ships a development library in the production image. Splitting the seeders is more work but keeps the image lean. + ## Backlog -- Add Content Security Policy on Laravel and SSR Node responses. +- Add Content Security Policy on Laravel and SSR Node responses. The 2026-09-23 production smoke confirmed zero `Content-Security-Policy` headers on SSR, `/runtime-config.js`, and API responses, so a clean console proves nothing about policy. +- Review the default `Access-Control-Allow-Origin: *` on the API. The production smoke saw it on `/api/v1/auth/dev-login`, which means `config/cors.php` is unpublished and every origin is allowed. - Update `league/commonmark`, `guzzlehttp/guzzle`, `guzzlehttp/psr7`, and `phpseclib/phpseclib` to clear the 22 advisories reported by `composer audit`, including CVE-2026-71478 in commonmark 2.8.2. ADR-28 already neutralizes that link-filter bypass class independently, so this is dependency hygiene, not an open XSS hole. - Fix the two `runtime-config.spec.ts` Jest failures caused by Docker Compose environment variables leaking into the test `process.env`. - Extract optional bearer-token user resolution shared by public discovery controllers. diff --git a/brain/handoffs/2026-09-23-production-hydrated-smoke.md b/brain/handoffs/2026-09-23-production-hydrated-smoke.md new file mode 100644 index 0000000..9860f58 --- /dev/null +++ b/brain/handoffs/2026-09-23-production-hydrated-smoke.md @@ -0,0 +1,38 @@ +# Handoff - Production Hydrated-Browser Smoke + +Date: 2026-09-23 +Branch: `claude/estruturar-agentes-20566a` (validation only, no product change) + +## Scope + +Item C7: close the last open acceptance criterion of Priority 2, "Docker production smoke validates hydrated browser API calls". Nothing in the product was allowed to change; the item only had to prove that the hydrated browser uses the configured public origin instead of the `localhost` defaults. + +## How It Was Validated + +The stack ran twice, with two disjoint sets of public values, in throwaway Compose projects with their own volumes. + +- First pass: `127.0.0.1:4200` as public origin, `127.0.0.1:8000/api/v1` as public API, Reverb on `127.0.0.1:8080`. +- Adversarial pass: `127.0.0.55:4373` as public origin, `127.0.0.55:8373/api/v1` as public API, Reverb on `127.0.0.55:8473`, with `COMMANDSPHERE_ALLOWED_HOSTS` matching. Ports were bound to `127.0.0.55` only, so the default origin answered connection refused and any leak would fail loudly instead of silently succeeding. + +Secrets were generated per run into an env file outside the repository. No versioned file was touched in either pass; `git diff main` stayed empty. + +## Proven + +- Canonical, `og:url`, `og:image`, `twitter:image`, and JSON-LD carry the configured public origin on the landing page, a plugin page, and a command page. Zero occurrences of the default origin. +- `/runtime-config.js` serves `apiBaseUrl`, `publicOrigin`, and the Reverb block exactly as configured, with `Cache-Control: no-store`, and the served Reverb app key matches the configured one. +- After hydration, with hydration proven by a typed search that produced DOM results and a `routerLink` navigation that issued no new document request, 53 requests were captured and every one landed on a configured origin. API calls covered search, command detail, plugin detail, and plugin version documents. +- The browser websocket opens `ws://:/app/` and Reverb answers `pusher:connection_established`. The public variable wins over the `COMMANDSPHERE_REVERB_HOST: localhost` pinned in the `frontend` service of `docker-compose.prod.yml`. +- A forged `Host` header (`evil.example.com`, `attacker.test`) never reaches canonical or metadata, because `publicUrlFor()` in `frontend/src/server.ts` builds URLs from `COMMANDSPHERE_PUBLIC_ORIGIN` alone. +- `POST /api/v1/auth/dev-login` answers `403 auth.dev_login_disabled` in production, and refuses before request validation. + +## Remaining Work + +- Seeding is broken in the production image: `fakerphp/faker` is a dev dependency and the image installs `--no-dev`. Tracked as Priority 13. The adversarial pass worked around it with direct SQL inserts into the throwaway volume, which is how the plugin and command pages could be tested at all. +- No Content Security Policy exists anywhere in the stack. Tracked in the backlog; the clean console in this smoke says nothing about policy. +- The API serves `Access-Control-Allow-Origin: *`. Tracked in the backlog. + +## Gates + +- Production Docker runtime gate: `VALIDATED` in both passes, including migrations and Scout index sync. +- `docker compose ... migrate:fresh --seed`: `FAILED` for the reason above, not by regression. +- Backend and frontend unit, lint, and build gates: `PENDING`. This item changed no code, so they were not run.