docs(19.3): capture phase context
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# Phase 19.3: Framework transport and infrastructure for WavePath - Context
|
||||
|
||||
**Gathered:** 2026-10-15
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
The framework gains the transport and infrastructure pieces a multi-client, multi-instance application needs:
|
||||
|
||||
- JWT cookie transport for refresh across subdomains.
|
||||
- Credentialed CORS.
|
||||
- A cache module with memory and Redis drivers, plus a Redis-backed `surf.Store` so rate limits hold across instances.
|
||||
- An S3 blob option selected through config.
|
||||
- An opt-in `/api-docs` page and `/api-docs.json`.
|
||||
- Cloudflare Turnstile verification.
|
||||
|
||||
Two todos are folded in: a framework-level site-cache purge service, adopted by the Journal, menu and FAQ plugins, and hardening of upload disk-name extensions.
|
||||
|
||||
Framework code never names WavePath or any other application (READMEs, docs and examples use neutral names). Repos touched: `summercms.go`. For the purge adoption only: `sm-journal-plugin`, `sm-menu-plugin`, `sm-faq-plugin` and the matching vue layers (`vue-journal-layer`, `vue-menu-layer`, `vue-faq-layer`).
|
||||
|
||||
Not in this phase: OIDC (19.4), social login (19.5), 2FA and device flow (19.6), and plugin-registrable token scopes (dropped, see the division note).
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Cookie transport (bouncer)
|
||||
- **D-01:** `bouncer` ships config-driven cookie builders (`TokenCookie` and `ForgetTokenCookie` or similar) and an optional ready-made cookie refresh/logout handler. The handler reads the token cookie, calls the existing `Refresh*` path (which already rotates the token and blacklists the old JTI) and sets the rotated cookie. The app supplies a writer func so it keeps its own response envelope. The shared `sm-user-plugin` is NOT changed to set cookies; apps opt in.
|
||||
- **D-02:** Cookie attributes live in one framework block, `auth.cookie.*`: `name`, `domain`, `secure`, `same_site`, `path`. Cookie Max-Age comes from the guard's `refresh_ttl`, not its `ttl`: the cookie must outlive the access token, or refresh has no cookie to read (PHP comment in `AuthApiController::tokenCookie`). The existing per-guard `ttl`/`refresh_ttl` keys (`golem15.user.jwt.*`, `admin.jwt.*`) stay where they are. — **Reversibility:** costly — apps' config files and deploy docs reference these keys once they ship.
|
||||
- **D-03:** Defaults when unset match PHP: name `token`, path `/`, httpOnly, `SameSite=Lax`, no Domain (host-only), Secure when the app URL is https, Max-Age = refresh window (20160 min default).
|
||||
|
||||
### Credentialed CORS (surf)
|
||||
- **D-04:** Harden the existing path-scoped `http.cors` (`surf.CORSConfig.SupportsCredentials`):
|
||||
- Refuse, or at least loudly reject at boot, credentials combined with a `*` origin.
|
||||
- Always echo the exact origin, and set `Vary: Origin` on preflight too.
|
||||
- Test the api ↔ www subdomain case.
|
||||
- **D-05:** Also add a named CORS middleware (for example `cors:<profile>`) so a route group can carry its own CORS policy on top of the global path-scoped one.
|
||||
|
||||
### Cache and rate-limit store (new module `cooler`)
|
||||
- **D-06:** New module **`cooler`** for the cache. The name comes from the research: `.planning/research/ARCHITECTURE.md` lists `cooler` for "in-process cache, rate-limit buckets", and `IDEA_LIB_NAMES.md` has `summer-cooler`. It gets a beach-name README and a row in the root table.
|
||||
- **D-07:** Redis client is **go-redis v9** (`github.com/redis/go-redis/v9`). A decision note under `.planning/notes/` names it before code lands. The memory driver is otter v2 (research/go-ecosystem.md) or a stdlib map; the researcher picks one and the note records it. — **Reversibility:** costly — every Redis-touching module and app config builds on this client.
|
||||
- **D-08:** The cache API is a Laravel-core subset: Get, Set, Has, Forget, Remember, Increment and Flush, with TTL and a per-app key prefix. No tags. An atomic lock only if a 19.x port is shown to need it.
|
||||
- **D-09:** Selection uses one shared Redis block, `redis.url`, plus `cache.driver: memory|redis` and `http.rate_limit.store: memory|redis`. The framework wires everything at boot, so apps import nothing. `cooler` implements `surf.Store` for Redis (an atomic fixed-window script with the same semantics as `surf.MemoryStore`), so `surf` takes no Redis dependency.
|
||||
|
||||
### Site-cache purge (folded todo, in `cooler`)
|
||||
- **D-10:** A framework purge service in `cooler`:
|
||||
- One configured target: `site_cache.purge_url` and `site_cache.purge_token`.
|
||||
- A loopback POST with a bearer token, sent after a committed admin save.
|
||||
- Failures are logged and never fail the save.
|
||||
- **D-11:** Plugin adoption is in this phase:
|
||||
- Journal moves onto the service. Its current `golem15.journal.site_cache.*` keys stay as aliases for one release.
|
||||
- The menu plugin purges after a menu or menu-entry save, reorder or delete. The FAQ plugin purges after a FAQ save or delete.
|
||||
- The Nuxt purge route moves out of `vue-journal-layer` to a shared place, so a site without Journal still gets purges.
|
||||
- Plugin repos are committed and pushed first; pointer bumps in apps are separate commits.
|
||||
- Core plugins must not break: the alias keys keep existing Journal deployments working.
|
||||
|
||||
### S3 and uploads (lagoon/attach)
|
||||
- **D-12:** New key `storage.uploads.public_url`. When set (a bucket or CDN base), `File` URLs are that base plus the partition path. When unset, files go through the existing `/storage/uploads` proxy route, which works for S3 too.
|
||||
- **D-13:** The S3 driver (`gocloud.dev/blob/s3blob`) is opt-in through a **build tag**. `summer build` sets the tag from `summer.yaml`, so an app still never imports the driver itself. A `bucket_url: s3://…` in a binary built without the tag must fail at boot with an error that names the tag.
|
||||
- **D-14:** S3-compatible endpoints (MinIO, R2, Hetzner) are configured through gocloud's native `bucket_url` query (`region`, `endpoint`, `use_path_style`). Credentials come from the standard AWS env vars. No parallel `s3.*` key set.
|
||||
- **D-15:** Folded stored-XSS fix: `attach.Store` takes the disk-name extension from the sniffed MIME type, through one shared helper. The client name stays only in `file_name`. A regression test checks that a PNG uploaded as `x.html` gets a `.png` disk name and URL. The deploy docs gain a note on nosniff and a file-type allow-list. App call sites (fonoteka `storeUpload`/`StorePublicFile`) move to the helper in a follow-up, not in this phase.
|
||||
|
||||
### API docs (surf)
|
||||
- **D-16:** The spec is supplied by an app or plugin: an embedded YAML or JSON file, converted to JSON once and cached. This matches PHP, which serves a hand-written YAML covering only the public surface. Nothing is generated from swag here.
|
||||
- **D-17:** Routes are opt-in through config (`http.api_docs.enabled` plus the path), off by default. `/api-docs` serves a Swagger UI page from pinned jsDelivr `swagger-ui-dist@5` with SRI, like PHP. `/api-docs.json` serves the spec. Both are public and unauthenticated.
|
||||
|
||||
### Turnstile (bouncer)
|
||||
- **D-18:** `bouncer` gets a Turnstile verifier, `Verify(ctx, token, ip, failClosed)`, with a 5 s timeout, plus a named middleware factory such as `turnstile:open|closed`. Semantics follow PHP `TurnstileVerifier`:
|
||||
- Fail-open: an empty secret passes (dev no-op, the registration path).
|
||||
- Fail-closed: 503 when unconfigured in production, 422 on a missing token, a siteverify false, an exception or a timeout (the guest entry path).
|
||||
- Error bodies come from a writer func, so the app keeps the PHP shape. The secret is read from config, which maps the `TURNSTILE_*` env.
|
||||
|
||||
### Module placement
|
||||
- **D-19:** `cooler` holds the cache, the Redis `surf.Store` adapter and the site-cache purge. `surf` holds `/api-docs` and the named CORS middleware. `bouncer` holds the cookie helpers and handler, and Turnstile. `lagoon/attach` holds the S3 tag, `public_url` and the extension fix.
|
||||
|
||||
### Claude's Discretion
|
||||
- The exact Go identifiers, the handler signatures and the name of the named-CORS-middleware profile config.
|
||||
- otter vs a stdlib map for the memory cache driver (recorded in the decision note).
|
||||
- Whether the token cookie builder lives on a guard or as free functions.
|
||||
- The exact build-tag name and how `summer.yaml` declares it.
|
||||
|
||||
### Folded Todos
|
||||
- **Purge the Nuxt site cache on menu and FAQ admin saves** (`.planning/todos/pending/site-cache-purge-for-menu-and-faq.md`, g15office `zugdxorb`). Today the purge is Journal-specific, so menu and FAQ edits show up only after the 300 s SWR TTL. Folded as D-10 and D-11 with full plugin adoption.
|
||||
- **Uploads keep the client file extension (stored XSS)** (`.planning/todos/pending/2026-10-11-uploads-keep-client-extension-stored-xss.md`, g15office `bemfi2zs`). Folded as D-15. Only the framework side is in scope; fonoteka call sites are a follow-up.
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Scope and division
|
||||
- `.planning/ROADMAP.md` § Phase 19.3: goal and success criteria.
|
||||
- `.planning/notes/wavepath-framework-asks-division.md`: which WavePath asks land in 19.3 and which were dropped (token scopes).
|
||||
- `/media/nvme/dev/jz/wavepath.org/.planning/notes/summercms-framework-asks-for-wavepath.md`: the hand-off list, sections A3, A6 and A8.
|
||||
|
||||
### PHP reference behaviour
|
||||
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/AuthApiController.php`: `tokenCookie`, `forgetTokenCookie`, `refresh`, `logout` (cookie attributes, Max-Age = refresh window, rotation).
|
||||
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/classes/auth/TurnstileVerifier.php`: fail-open vs fail-closed semantics, 5 s timeout.
|
||||
- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/ApiDocsController.php`: the `/api-docs` UI and `/api-docs.json` spec serving, pinned CDN.
|
||||
- `/media/nvme/dev/jz/wavepath.org/config/session.php`: SESSION_DOMAIN and same_site.
|
||||
|
||||
### Research and dependency rules
|
||||
- `.planning/research/ARCHITECTURE.md` (line ~73): the `cooler` module role.
|
||||
- `.planning/research/go-ecosystem.md` (Cache row): otter and go-redis.
|
||||
- `.planning/research/STACK.md`: the decided dependency set. go-redis needs a decision note.
|
||||
- `../IDEA_LIB_NAMES.md`: the original `summer-cooler` name.
|
||||
|
||||
### Folded todos
|
||||
- `.planning/todos/pending/site-cache-purge-for-menu-and-faq.md`
|
||||
- `.planning/todos/pending/2026-10-11-uploads-keep-client-extension-stored-xss.md`
|
||||
- `.planning/phases/12-p-ytarium-api-collections-and-albums/12-REVIEW.md` (WR-01): origin of the XSS finding.
|
||||
|
||||
### Repo conventions
|
||||
- `CLAUDE.md`: doc rules (README and docs in the same commit), beach-module rules, sibling-repo commit order.
|
||||
- `.planning/notes/core-plugins-own-repos.md`: plugin repo and submodule flow for the purge adoption.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `modules/bouncer/refresh.go`: `Refresh`, `RefreshAudience` and `RefreshAudienceFor` already rotate the token and blacklist the old JTI with a grace window. The cookie handler wraps these.
|
||||
- `modules/bouncer/jwt.go`: `NewJWTGuard` and `NewBackendJWTGuard` already read tokens from configured cookie names after the bearer token.
|
||||
- `modules/bouncer/blacklist.go`: the `BlacklistStore` interface. A Redis implementation in `cooler` is a natural extra, but it is not required.
|
||||
- `modules/surf/cors.go`: `CORSConfig` (Laravel `config/cors.php` keys, including `supports_credentials` and origin patterns) and `pathScopedCORS`.
|
||||
- `modules/surf/limiter_store.go`: the `surf.Store` interface (`Attempt(key, max, decay)`) and `MemoryStore`. The Redis store must match its semantics.
|
||||
- `modules/lagoon/attach/bucket.go`: `OpenBucket` and `normalizeBucketURL` on `gocloud.dev/blob` (file and mem blank-imported today). S3 is the `s3blob` driver behind a build tag.
|
||||
- `modules/lagoon/attach/store.go`: `Store` already sniffs the content type, which is where the disk-name extension fix goes.
|
||||
- `../sm-journal-plugin/classes/site_cache.go`: the existing Journal purge to generalise.
|
||||
- `internal/build`: generates `main.go` and `plugins.gen.go` from `summer.yaml`, which is where build tags get set.
|
||||
|
||||
### Established Patterns
|
||||
- Opt-in routes gated by config (`storage.uploads.serve` mounts the uploads route in `surf.BuildRouter`). `/api-docs` follows the same pattern.
|
||||
- Named middleware and middleware factories (`RegisterMiddlewareFactory`, `name:param`). Turnstile and named CORS use them.
|
||||
- Per-guard JWT config keys (`golem15.user.jwt.ttl/refresh_ttl`, `admin.jwt.*`).
|
||||
- New dependencies need a decision note first. YAML goes through `goccy/go-yaml`.
|
||||
|
||||
### Integration Points
|
||||
- Boot wiring: the framework reads `redis.url`, `cache.driver` and `http.rate_limit.store` and installs the `cooler` Redis store into surf's limiter before routes are built.
|
||||
- `summer build` and `summer.yaml`: the S3 build tag.
|
||||
- Plugins (Journal, menu, FAQ): call the purge service after committed admin saves.
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- The cookie must outlive the access token (Max-Age = `refresh_ttl`). The PHP comment explains the re-login loop that happens otherwise.
|
||||
- Swagger UI is pinned to `swagger-ui-dist@5` on jsDelivr, as in PHP.
|
||||
- Turnstile keeps the two PHP modes exactly: registration fails open, guest entry fails closed.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- Switching fonoteka.go's `storeUpload`/`StorePublicFile` to the new extension helper: a follow-up quick task in fonoteka.go.
|
||||
- Cache tags and atomic locks: add them only when a ported plugin needs them.
|
||||
- Generating `/api-docs` specs from swag annotations for frontend routes: later, if wanted.
|
||||
- A Redis-backed `bouncer.BlacklistStore`: optional, not required by the criteria.
|
||||
|
||||
### Reviewed Todos (not folded)
|
||||
- **Benchmark Płytarium on WinterCMS vs SummerCMS** (`2026-10-01-benchmark-...`): matched on keywords only; unrelated to 19.3.
|
||||
- **Port the FAQ plugin as sm-faq-plugin**: already its own repo; only the purge wiring touches it here.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 19.3-framework-transport-and-infrastructure-for-wavepath*
|
||||
*Context gathered: 2026-10-15*
|
||||
@@ -0,0 +1,123 @@
|
||||
# Phase 19.3: Framework transport and infrastructure for WavePath - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-10-15
|
||||
**Phase:** 19.3-framework-transport-and-infrastructure-for-wavepath
|
||||
**Areas discussed:** Cookie refresh ownership, Cache + Redis store, S3 option & public URLs, api-docs & Turnstile, Module placement
|
||||
|
||||
---
|
||||
|
||||
## Todos
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Site cache purge (menu/FAQ) | Framework-level Nuxt purge service | ✓ |
|
||||
| Uploads stored-XSS extension | Disk-name extension from sniffed MIME in lagoon/attach | ✓ |
|
||||
| None | Keep todos separate | |
|
||||
|
||||
## Cookie refresh ownership
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Helpers + optional handler | Cookie builders plus a ready-made refresh/logout handler with a writer func | ✓ |
|
||||
| Helpers only | Apps write their own handler | |
|
||||
| Full handler in sm-user-plugin | Change the shared plugin's /refresh | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Framework `auth.cookie.*` | One block; Max-Age from the guard's refresh_ttl | ✓ |
|
||||
| Per guard | `golem15.user.jwt.cookie.*`, `admin.jwt.cookie.*` | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Match PHP | token, /, httpOnly, Lax, host-only, Secure on https | ✓ |
|
||||
| Stricter | Strict + always Secure | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Harden + test existing | Boot check, exact-origin echo, Vary, subdomain tests | |
|
||||
| Add named middleware too | Hardening plus a per-group named CORS middleware | ✓ |
|
||||
|
||||
## Cache + Redis store
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| go-redis v9 | Official, most widely used, already in research | ✓ |
|
||||
| rueidis | Faster, smaller ecosystem | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Laravel-core subset | Get/Set/Has/Forget/Remember/Increment/Flush, no tags | ✓ |
|
||||
| Include tags | Laravel cache tags | |
|
||||
| Minimal KV | Get/Set/Delete only | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| One redis block, auto-wired | redis.url + cache.driver + http.rate_limit.store | ✓ |
|
||||
| App wires it | App builds client and sets the store | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Framework service only | Plugin adoption as follow-up | |
|
||||
| Service + plugin adoption | Journal (alias keys), menu, FAQ and Nuxt route in this phase | ✓ |
|
||||
|
||||
## S3 option & public URLs
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Configurable base URL | `storage.uploads.public_url`, else proxy | ✓ |
|
||||
| Always proxy | Stream everything through Go | |
|
||||
| Presigned URLs | Direct and presigned | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Always linked | Blank import, +10–15 MB | |
|
||||
| Build tag | Opt-in tag set by summer build from summer.yaml | ✓ |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Via bucket_url query | gocloud native params, AWS env creds | ✓ |
|
||||
| Explicit config keys | `storage.uploads.s3.*` | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Sniffed-MIME extension in attach | Framework helper + test; fonoteka sites follow-up | ✓ |
|
||||
| Also fix fonoteka call sites | Include app changes in this phase | |
|
||||
|
||||
## api-docs & Turnstile
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| App/plugin-supplied file | Embedded YAML/JSON, as PHP | ✓ |
|
||||
| Generated from swag | Reuse the cabana pipeline | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Opt-in via config | `http.api_docs.enabled`, Swagger UI from pinned CDN | ✓ |
|
||||
| Plugin capability | pact interface | |
|
||||
| Embed Swagger UI | ~1.5 MB in binary | |
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Verifier + middleware factory | PHP fail-open/closed semantics | ✓ |
|
||||
| Verifier only | Plugins call it in handlers | |
|
||||
|
||||
## Module placement
|
||||
|
||||
**User's choice:** First asked for a suggestion ("api-docs can go to surf, turnstile i guess too, or do we have a better module for that? what do you suggest for cache itself, do we have some earlier research around it maybe?"). Claude pointed to `cooler` in research/ARCHITECTURE.md and IDEA_LIB_NAMES.md and proposed: cooler (cache, Redis Store, purge), surf (api-docs), bouncer (Turnstile).
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Yes, as suggested | cooler / surf / bouncer | ✓ |
|
||||
| Turnstile in surf instead | | |
|
||||
| Purge in its own module | | |
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- Go identifiers, handler signatures, the named CORS profile config, otter vs stdlib map, the build-tag name.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- fonoteka call sites onto the extension helper; cache tags and locks; swag-generated api-docs; a Redis BlacklistStore.
|
||||
Reference in New Issue
Block a user