docs(19.3): add phase verification report (gaps found)
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
---
|
||||
phase: 19.3-framework-transport-and-infrastructure-for-wavepath
|
||||
verified: 2026-10-15T20:45:00Z
|
||||
status: gaps_found
|
||||
score: 3/4 roadmap success criteria verified (SC-1 partial); plan must-haves 35/36 verified (D-05 failed); 0 present-behavior-unverified
|
||||
covered_files:
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-01-PLAN.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-01-SUMMARY.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-02-PLAN.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-02-SUMMARY.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-03-PLAN.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-03-SUMMARY.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-04-PLAN.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-04-SUMMARY.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-05-PLAN.md
|
||||
- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-05-SUMMARY.md
|
||||
- go.mod
|
||||
- go.sum
|
||||
- internal/build/build.go
|
||||
- internal/build/manifest.go
|
||||
- modules/bouncer/cookie.go
|
||||
- modules/bouncer/turnstile.go
|
||||
- modules/cabana/reorder.go
|
||||
- modules/cooler/cooler.go
|
||||
- modules/cooler/memory.go
|
||||
- modules/cooler/ratelimit.go
|
||||
- modules/cooler/redis.go
|
||||
- modules/cooler/rediscache.go
|
||||
- modules/cooler/sitecache.go
|
||||
- modules/lagoon/attach/bucket.go
|
||||
- modules/lagoon/attach/diskext.go
|
||||
- modules/lagoon/attach/s3_off.go
|
||||
- modules/lagoon/attach/s3_on.go
|
||||
- modules/lagoon/attach/store.go
|
||||
- modules/lagoon/attach/thumb.go
|
||||
- modules/pact/capabilities.go
|
||||
- modules/surf/apidocs.go
|
||||
- modules/surf/cors.go
|
||||
- modules/surf/router.go
|
||||
- modules/surf/serve.go
|
||||
- scripts/check-phase19.3.sh
|
||||
covered_digest: "v2:sha256:f9d58003dac25f701e22b19bc467b0e06c88a94264b5dce19b90c2a29bacc6b5"
|
||||
behavior_unverified: 0
|
||||
overrides_applied: 0
|
||||
gaps:
|
||||
- truth: "SC-1 / D-05: credentialed CORS for a configured SPA origin is available as middleware; a named middleware cors:<profile> applies the profile's policy to the routes that carry it"
|
||||
status: partial
|
||||
reason: >-
|
||||
Review CR-01, reproduced in this verification. Router.wrap (modules/surf/router.go:416-466) composes
|
||||
route middleware in declaration order and puts bodyLimit, locale and recoverJSON/recoverBare outside
|
||||
all of them. profileMiddleware (modules/surf/cors.go:255-262) only sets the CORS headers when the
|
||||
request reaches it. A cross-origin GET from the allowed origin got these answers on HEAD:
|
||||
["deny","cors:spa"] -> 401 with no Access-Control-Allow-Origin; group Use("deny") with route
|
||||
cors:spa -> 401 with no ACAO; a panicking cors:spa route -> 500 with no ACAO. Only ["cors:spa","deny"]
|
||||
and a group-level Use("cors:spa") carried ACAO and ACAC. A body-limit 413 and a recovered 500 never
|
||||
carry them, whatever the order. docs/services/routing.md tells readers to list cors:spa "next to its
|
||||
guard", which does not prevent the failing order. The browser then reports an opaque network error, so the SPA cannot read the
|
||||
401 that should trigger its cookie refresh (the phase's headline flow), the 429 from a throttle, or the 422
|
||||
that should re-show the Turnstile widget. The global path-scoped http.cors wrapper is NOT affected (it
|
||||
wraps the mux: 401 and 500 both carried ACAO and ACAC in the same probe), so credentialed CORS works
|
||||
for apps that configure it through http.cors.paths. The per-route-group profile that D-05 delivered is the part that fails.
|
||||
artifacts:
|
||||
- path: "modules/surf/router.go"
|
||||
issue: "wrap applies the cors:<profile> middleware at its list position, inside bodyLimit and recovery; refusals from outer layers and from guards/throttle/turnstile listed before it carry no CORS headers"
|
||||
- path: "modules/surf/cors.go"
|
||||
issue: "profileMiddleware relies on being outermost; nothing enforces it"
|
||||
- path: "docs/services/routing.md"
|
||||
issue: "line ~265 suggests placing cors:spa 'next to its guard' without stating that order matters"
|
||||
missing:
|
||||
- "In Router.wrap, skip the cors:<profile> entry in the middleware loop and apply r.corsProfiles[profile].profileMiddleware after recoverJSON/recoverBare (just inside recordPattern), so every response on a profiled route, including 401/403/413/422/429/500, carries the profile's headers"
|
||||
- "Test: a cross-origin request to a cors:spa route behind a refusing guard listed BEFORE cors:spa answers 401 with Access-Control-Allow-Origin, Access-Control-Allow-Credentials and Vary: Origin; same for a body-limit 413 and a recovered panic 500; add it to SURF_TESTS in scripts/check-phase19.3.sh"
|
||||
- "docs/services/routing.md and modules/surf/README.md: state that the profile applies to every response of the route regardless of its position in the list"
|
||||
---
|
||||
|
||||
# Phase 19.3: Framework transport and infrastructure for WavePath Verification Report
|
||||
|
||||
**Phase Goal:** The framework has the transport and infrastructure pieces a multi-client, multi-instance application needs: JWT cookie refresh across subdomains, credentialed CORS, a cache module with a Redis-backed rate-limit store, an S3 blob option, an OpenAPI page and Turnstile verification.
|
||||
**Mode:** mvp (ROADMAP marks the phase `mode: mvp`, but `user-story.validate` returns `valid: false` for the goal because it is not written as "As a ..., I want to ..., so that ...". As in the 18 and 18.1 reports, this report uses standard goal-backward verification against the goal text and its four success criteria, with a User Flow Coverage table derived from the goal. To make the mode consistent, run `/msd mvp-phase 19.3` to restate the goal, or drop `mode: mvp`.)
|
||||
**Verified:** 2026-10-15T20:45:00Z
|
||||
**Status:** gaps_found
|
||||
**Re-verification:** No, initial verification
|
||||
|
||||
## User Flow Coverage (derived; the goal is not a user story)
|
||||
|
||||
Flow: a SPA on `www.example.com` talks to an API on `api.example.com` run as several instances.
|
||||
|
||||
| Step | Expected | Evidence | Status |
|
||||
|------|----------|----------|--------|
|
||||
| Log in, get a cookie | httpOnly `token` cookie, Domain/SameSite/Secure from `auth.cookie.*`, Max-Age = refresh window | `bouncer.LoadCookieConfig`, `CookieConfig.TokenCookie` (`modules/bouncer/cookie.go`); `TestTokenCookieMaxAgeIsRefreshWindow`, `TestLoadCookieConfigDefaults/Errors` passed | VERIFIED |
|
||||
| Access token expires, API answers 401 | The SPA can read the 401 cross-origin and calls refresh | Global `http.cors` path: 401 and 500 carry ACAO+ACAC (my probe). `cors:<profile>` path: 401 carries no ACAO when the guard is listed before the profile or comes from a group; a 500 or 413 never carries them (my probe, CR-01) | PARTIAL (gap) |
|
||||
| Refresh through the cookie | New cookie with the same attributes, old jti blacklisted, replay refused | `CookieRefreshHandler`; `TestCookieRefreshHandlerRotatesAndBlacklists` uses the real `bouncer.Refresh` and asserts the old jti is blacklisted and the replay answers 401; `TestCookieRefreshAcrossSubdomainsSmoke` passed in my run | VERIFIED |
|
||||
| Requests land on different instances | Throttle counts one window across instances | `TestRateLimitAcrossInstancesSmoke` (two `Assemble`d apps, one real Redis container, `throttle:2,1`: 200, 200, then 429 from the other instance) passed in my run | VERIFIED |
|
||||
| Upload a file to S3 | `bucket_url: s3://` works in a tagged binary, the app imports nothing | `s3_on.go` is the only `s3blob` importer (`//go:build summercms_s3`); `summer build` passes `-tags` from `summer.yaml` `build.tags`; `TestOpenBucketS3` passed in both tag modes | VERIFIED (real bucket: human item 2) |
|
||||
| Read the API docs | `/api-docs` and `/api-docs.json` | `mountAPIDocs`; SRI hashes in `apidocs.go` match the live jsDelivr files (checked with curl and openssl) | VERIFIED (rendering under CSP: human item 4) |
|
||||
| Register behind Turnstile | `turnstile:open|closed` middleware | `bouncer.Turnstile`, surf factory; 7 Turnstile tests + `TestTurnstileFactoryParams` passed | VERIFIED |
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths (roadmap success criteria)
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| SC-1 | `bouncer` refresh issues a new httpOnly `token` cookie with configurable `Domain`/`SameSite`/`Secure`, blacklists the old JTI; 60-min TTL and 14-day refresh window are config keys; credentialed CORS for a configured SPA origin is available as middleware | ✗ FAILED (partial) | Cookie half: VERIFIED. `cookie.go` builders, loader and handlers; old jti blacklist proven by `TestCookieRefreshHandlerRotatesAndBlacklists`. TTL keys `golem15.user.jwt.ttl` (60) / `refresh_ttl` (20160) exist in the user plugin (`api_controller.go:825-836`) and `admin.jwt.*` in `cabana/auth.go:454-463`. CORS half: global `http.cors` with `supports_credentials` works on every response, including 401 and 500. The named `cors:<profile>` middleware (D-05) drops its headers on refusals depending on order, and always on 413 and 500. See gap |
|
||||
| SC-2 | Cache module with memory and Redis drivers; `surf` gets a Redis-backed `Store` so rate limits hold across instances; Redis client decision note before code | ✓ VERIFIED | `modules/cooler` (otter v2 memory driver, go-redis v9 Redis driver, Get/Set/Has/Forget/Remember/Increment/Flush). `RateLimitStore.Attempt` is one atomic Lua script (PTTL/INCR). `serve.go` `installCooler` publishes `surf.Store` before `BuildRouter`, and `BuildRouter` looks it up (`router.go:497`). Note commit `77dd11c` is an ancestor of `fa4d89d`, the first commit that adds go-redis to go.mod (`git merge-base --is-ancestor`). Cross-instance 429 proven against real Redis |
|
||||
| SC-3 | S3 blob driver selectable through framework config without the app importing it; `/api-docs` + `/api-docs.json` servable; Turnstile middleware available | ✓ VERIFIED | `s3_on.go`/`s3_off.go` build tag, `checkS3Driver` error names `summercms_s3`, `goBuildArgs` adds `-tags=`. `mountAPIDocs` gated by `http.api_docs.enabled`, exactly-one-provider check, pinned 5.33.1 with SRI that matches the CDN. `turnstile:open|closed` factory registered in `BuildRouter` (`router.go:528-538`) |
|
||||
| SC-4 | Touched modules' READMEs and `docs/` updated; new code has unit tests delivered in the last plan | ✓ VERIFIED | READMEs of bouncer, cabana, cooler (new, standard structure, root table row), lagoon, pact, surf changed; 13 docs pages changed; each new config key appears in docs (grep). `TestDocsTree` and `docs:build --check` passed in the gate. Plan 05 commits touch only `_test.go`, the gate script, go.mod/go.sum (miniredis) and the cooler README |
|
||||
|
||||
**Score:** 3/4 roadmap truths verified (SC-1 partial). Plan must-haves: 35/36 verified; the D-05 truth failed for the same reason as SC-1. 0 present-but-behavior-unverified.
|
||||
|
||||
### Plan must-haves (36 truths across 5 plans)
|
||||
|
||||
| Plan | Truths | Status | Notes |
|
||||
|------|--------|--------|-------|
|
||||
| 01 (cooler, Redis store) | 8 | 8 ✓ | Decision note ordering, module + README + root row, cross-instance window (`TestRateLimitAcrossInstancesSmoke`), MemoryStore parity (`TestRateLimitParity` miniredis + `TestRateLimitParityDocker`), fallback + once-per-minute warning (`TestRateLimitFallbackOnError`), `BuildRouter` never dials (`TestBuildRouterNeverDials`), Cache API + prefix-only Flush (`TestRedisFlushOnlyPrefix`), `FromApp` lazy (`TestFromAppLazy`) |
|
||||
| 02 (cookies, CORS, Turnstile, api-docs) | 7 | 6 ✓, 1 ✗ | D-01, D-02, D-03, D-04, D-16/17 and D-18 verified by named tests. **D-05 failed (partial):** the profile config, validation and OPTIONS routes work (`TestCORSProfilePreflightRoute`, `...Unknown`, `...WithPathsRefused`, `...OverlapGlobal`), but the middleware does not reach error responses (CR-01) |
|
||||
| 03 (S3 tag, DiskExtension, public_url) | 6 | 6 ✓ | `TestManifestBuildTags`, `TestSaveManifestKeepsBuildTags`, `TestGoBuildArgs`, `TestOpenBucketS3` (both tag modes), `TestDiskExtensionTable`, `TestStoreDiskExtension` (Postgres), `TestPublicURLBase/Validation`; deploy nosniff note at `docs/services/storage.md:122`; `store.go:154` calls `DiskExtension` |
|
||||
| 04 (site-cache purge, plugins, layers) | 7 | 7 ✓ | `cooler/sitecache.go`; `pact.ReorderAfterSave` called inside the reorder transaction (`cabana/reorder.go:354`); menu/FAQ/Journal adoption with Docker admin tests; Journal alias keys and deprecated names kept (`sm-journal-plugin/classes/site_cache.go`); three byte-identical layer routes (gate `cmp`); plugin and layer repos clean and in sync with origin |
|
||||
| 05 (tests, gate) | 8 | 8 ✓ | The gate ran green with `PHASE193_REQUIRE_DOCKER=1` (self-test, go, plugins, layers, docs, docker); detectors fail on a skipped or missing test name |
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `.planning/notes/2026-10-15-cooler-go-redis-otter.md` | decision note | ✓ VERIFIED | committed in `77dd11c`, before go-redis lands |
|
||||
| `modules/cooler/{cooler,memory,redis,rediscache,ratelimit,sitecache}.go` | cache, store, purge | ✓ VERIFIED | 1,142 lines, imports only backpack/compass/lagoon |
|
||||
| `modules/surf/serve.go` | `installCooler` | ✓ VERIFIED | called before `BuildRouter` |
|
||||
| `modules/bouncer/cookie.go` | cookie config, builders, handlers | ✓ VERIFIED | bouncer imports no SummerCMS module (`go list`) |
|
||||
| `modules/bouncer/turnstile.go` | verifier + middleware | ✓ VERIFIED | siteverify URL, 5 s timeout, 2048 limit |
|
||||
| `modules/surf/cors.go` | validated CORS, profiles, preflights | ⚠️ PARTIAL | global wrapper correct; profile middleware placement defect (gap) |
|
||||
| `modules/surf/apidocs.go` | provider, pinned page, spec route | ✓ VERIFIED | SRI matches CDN |
|
||||
| `modules/lagoon/attach/{s3_on,s3_off,diskext}.go` | S3 tag, DiskExtension | ✓ VERIFIED | |
|
||||
| `internal/build/manifest.go` | `Manifest.Tags` | ✓ VERIFIED | |
|
||||
| `modules/pact/capabilities.go` | `ReorderAfterSave` | ✓ VERIFIED | |
|
||||
| `../vue-*-layer/server/api/__site__/cache/purge.post.ts` | shared purge route | ✓ VERIFIED | identical in three layers |
|
||||
| `scripts/check-phase19.3.sh` | phase gate | ✓ VERIFIED | ran green |
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status |
|
||||
|------|----|-----|--------|
|
||||
| `surf/serve.go` | `cooler.Install` | `installCooler` → `app.Publish[Store]` when `http.rate_limit.store: redis` | WIRED |
|
||||
| `surf/router.go` BuildRouter | published `Store` | `app.Lookup[Store]()` else `NewMemoryStore` | WIRED |
|
||||
| `cooler/ratelimit.go` | `surf.Store` | structural `Attempt(key, max, decay)` | WIRED |
|
||||
| `surf/router.go` | `cors.go` | `"cors"` factory + `addCORSPreflights` | WIRED (placement defect, see gap) |
|
||||
| `surf/router.go` | `bouncer/turnstile.go` | `"turnstile"` factory, `buildTurnstile`, `ClientIP` with trusted proxies | WIRED |
|
||||
| `bouncer/cookie.go` | `bouncer/refresh.go` | app-supplied Refresh wraps `Refresh`/`RefreshAudienceFor` | WIRED (test uses real `Refresh`) |
|
||||
| `surf/router.go` | `apidocs.go` | `mountAPIDocs` | WIRED |
|
||||
| `internal/build/build.go` | `manifest.go` | `goBuildArgs(binRel, m.Tags)` | WIRED |
|
||||
| `attach/bucket.go` | `s3_off.go` | `checkS3Driver` / `s3Enabled` | WIRED |
|
||||
| `attach/store.go` | `diskext.go` | `newDiskName(DiskExtension(sniffed, ext))` | WIRED |
|
||||
| `attach/thumb.go` | `bucket.go` | `PublicURLBase()` | WIRED |
|
||||
| `cabana/reorder.go` | `pact.ReorderAfterSave` | type assertion inside the Transaction closure with `txCtx` | WIRED |
|
||||
| FAQ/menu/Journal controllers | `cooler.PurgeSiteCacheAfterCommit` | `cabana.TxFromContext` + `SiteCacheFromApp` | WIRED |
|
||||
|
||||
### Data-Flow Trace (Level 4)
|
||||
|
||||
Not applicable: no rendered dynamic data. The equivalent runtime flows (cookie → Refresh → blacklist; request → throttle → shared Redis counter; save → after-commit → purge POST) are each exercised by a passing test listed above.
|
||||
|
||||
### Behavioral Spot-Checks
|
||||
|
||||
| Behavior | Command | Result | Status |
|
||||
|----------|---------|--------|--------|
|
||||
| Phase gate, all stages incl. Docker | `PHASE193_REQUIRE_DOCKER=1 bash scripts/check-phase19.3.sh --all` | exit 0; PASS self-test, go, plugins, layers, docs, docker | ✓ PASS |
|
||||
| Cross-instance rate limit + cookie refresh smoke | `go test -count=1 -run '^(TestRateLimitAcrossInstancesSmoke\|TestCookieRefreshAcrossSubdomainsSmoke)$' ./modules/surf` | both PASS (real Redis container) | ✓ PASS |
|
||||
| CR-01 reproduction (`cors:<profile>`) | scratch module (outside the repo) with `surf.Assemble` and a refusing `deny` middleware | `[deny,cors:spa]` 401 ACAO="" ; `[cors:spa,deny]` 401 ACAO set ; group `Use(cors:spa)` 401 ACAO set ; group `Use(deny)` + route `cors:spa` 401 ACAO="" ; panic 500 ACAO="" | ✗ FAIL |
|
||||
| Global `http.cors` on error responses | same scratch module, credentialed `http.cors.paths` | 401 and 500 both carry ACAO + ACAC | ✓ PASS |
|
||||
| Swagger UI SRI values | `curl` jsDelivr 5.33.1 assets, `openssl dgst -sha384` | all three equal the constants in `apidocs.go` | ✓ PASS |
|
||||
|
||||
### Probe Execution
|
||||
|
||||
No `scripts/*/tests/probe-*.sh` exist and no plan declares one. The phase gate (`scripts/check-phase19.3.sh`) was run by the verifier in its own process (see above).
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
REQUIREMENTS.md maps no IDs to phase 19.3 (no orphans). Phase-local IDs from PLAN frontmatter:
|
||||
|
||||
| ID | Source Plans | Status | Evidence |
|
||||
|----|--------------|--------|----------|
|
||||
| SC-1 | 02, 05 | ✗ PARTIAL | see gap (D-05 profile middleware) |
|
||||
| SC-2 | 01, 05 | ✓ SATISFIED | cooler, Redis store, note ordering |
|
||||
| SC-3 | 02, 03, 05 | ✓ SATISFIED | S3 tag, api-docs, Turnstile |
|
||||
| SC-4 | 01-05 | ✓ SATISFIED | READMEs, docs, tests in plan 05 |
|
||||
| D-01, D-02, D-03 | 02, 05 | ✓ SATISFIED | cookie tests |
|
||||
| D-04 | 02, 05 | ✓ SATISFIED | literal contract met; WR-04 hardening gap is a warning |
|
||||
| D-05 | 02, 05 | ✗ BLOCKED (partial) | CR-01 |
|
||||
| D-06, D-07, D-08, D-09 | 01, 05 | ✓ SATISFIED | |
|
||||
| D-10, D-11 | 04, 05 | ✓ SATISFIED | live purge is human item 3 |
|
||||
| D-12, D-13, D-14, D-15 | 03, 05 | ✓ SATISFIED | real bucket is human item 2 |
|
||||
| D-16, D-17 | 02, 05 | ✓ SATISFIED | rendering under CSP is human item 4 |
|
||||
| D-18 | 02, 05 | ✓ SATISFIED | |
|
||||
| D-19 | 01-04 | ✓ SATISFIED | placement matches (cooler / surf / bouncer / lagoon/attach) |
|
||||
|
||||
### Prohibitions (all judgment-tier; non-authoritative verifier verdicts)
|
||||
|
||||
| Plan | Prohibition | Verdict | Evidence |
|
||||
|------|-------------|---------|----------|
|
||||
| 01 | cooler imports neither surf nor cabana | holds | `go list` imports: backpack, compass, lagoon |
|
||||
| 01/04 | Framework code and docs never name a consuming application | holds | gate docs stage: "no consuming application named in framework lines since 2d352ff^" (older test files are logged in deferred-items.md) |
|
||||
| 01 | Cache Flush never empties the whole Redis database | holds | SCAN MATCH `<prefix>cache:*` + UNLINK; `TestRedisFlushOnlyPrefix` |
|
||||
| 02 | bouncer imports no SummerCMS module | holds | `go list` shows none |
|
||||
| 02 | Non-credentialed `*` CORS answers byte-identical | holds | `TestCORSOptionsMatchesLaravel`, `TestCORSStarUnchanged` |
|
||||
| 02 | Turnstile secret/token never logged | holds | `TestTurnstileNeverLogsSecret` |
|
||||
| 03 | No new `s3.*` keys | holds | no `"s3.` config reads in modules/internal |
|
||||
| 03 | Uploads passing today's allow-list still pass | holds | allow-list check unchanged; only the disk name derives from the sniff |
|
||||
| 03 | App call sites outside the framework untouched | holds | no fonoteka.go commits today |
|
||||
| 04 | No breaking change to Journal/menu/FAQ | holds | alias keys and deprecated names kept; `TestSiteCacheAliasKeysDeprecated`, admin Docker suites green |
|
||||
| 04/05 | Nothing pushed/tagged without the human's reply | not code-verifiable | SUMMARY records the checkpoint replies; v0.1.27 points at `9921768`, and no code commits follow it |
|
||||
| 05 | Tests reach no network beyond localhost/httptest/miniredis/testcontainers | holds (sampled) | Turnstile tests set `Endpoint` to httptest servers |
|
||||
| 05 | No production code changes in plan 05 except recorded deviations | holds | plan 05 commits touch only tests, the gate, go.mod/go.sum and the cooler README |
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
No `TBD`/`FIXME`/`XXX`/`TODO`/`HACK` markers in any file the phase changed (the gate script's `TODO` match is its TAP directive parser). The open code-review findings are the real debt:
|
||||
|
||||
| File | Finding | Severity | Impact |
|
||||
|------|---------|----------|--------|
|
||||
| `modules/surf/router.go`, `cors.go` | CR-01: profile CORS headers missing from refusals/413/500 | 🛑 Blocker | SPA cannot read the 401 that triggers cookie refresh (gap above) |
|
||||
| `modules/cooler/sitecache.go` | WR-01: purge is synchronous after commit; bulk delete sends N purges | ⚠️ Warning | slow admin saves when the site is slow; accepted "no debounce" (user answer in plan 04), but the 2 s × N latency was not weighed |
|
||||
| `modules/cooler/sitecache.go` | WR-02: purge client follows redirects with the bearer token | ⚠️ Warning | token leak on an https→http redirect |
|
||||
| `vue-*-layer/.../purge.post.ts` | WR-03: route refuses proxied requests; Go side accepts https remote targets | ⚠️ Warning | remote mode cannot work; D-10 only promised loopback |
|
||||
| `modules/surf/cors.go` | WR-04: `https://*` entries and unanchored patterns still trust arbitrary origins with credentials | ⚠️ Warning | same exposure D-04 meant to close, by another spelling |
|
||||
| `modules/bouncer/cookie.go` | WR-05: inferred Secure=false in production when `app.url` is http | ⚠️ Warning | matches D-03's literal rule but produces an insecure cookie silently |
|
||||
| `modules/surf/apidocs.go` | WR-06: CSP allowlists all of jsDelivr for scripts | ⚠️ Warning | weakens the nonce |
|
||||
| `modules/lagoon/attach/bucket.go` | WR-07: protected files land in a public bucket when `public_url` is set | ⚠️ Warning | Info-level log only |
|
||||
| `modules/surf/router.go` | WR-08: `turnstile:*` with no secret logs nothing at boot | ⚠️ Warning | silent bot-protection off |
|
||||
| `modules/bouncer/cookie.go` | WR-09: zero `CookieConfig` has no SameSite and no Secure | ⚠️ Warning | contradicts its own field doc |
|
||||
|
||||
Info items IN-01..IN-11 and deferred-items.md (S3 object content type, Redis outage latency in `Remember`) remain as recorded in 19.3-REVIEW.md. 19.3-VALIDATION.md is still `status: draft`, `nyquist_compliant: false`.
|
||||
|
||||
### Human Verification Required (after the gap closes)
|
||||
|
||||
These remain open from 19.3-VALIDATION.md and are not satisfiable by tests. Status is `gaps_found`, so they are listed here and not in the frontmatter.
|
||||
|
||||
1. **Cookie refresh across real subdomains.** Deploy to staging with `auth.cookie.domain` set to the parent domain and `cors:<profile>` or `http.cors` credentialed for the www origin. Log in on www and wait past the access TTL. Expected: the 401 is readable in the SPA, the refresh call sends the cookie and the rotated cookie replaces it. Why human: needs real DNS and TLS, and a browser's cookie and CORS enforcement.
|
||||
2. **Real S3/R2/MinIO upload.** Build with `build.tags: [summercms_s3]`, set `bucket_url: s3://...?endpoint=...`, upload, fetch via `public_url` and via `/storage/uploads`. Expected: both serve the file. Note deferred-items' stored Content-Type caveat. Why human: needs bucket credentials.
|
||||
3. **Live Nuxt purge.** Save a menu entry and a FAQ in admin. Expected: the page updates without waiting for the 300 s SWR TTL. Why human: needs a running Nuxt site and the app.
|
||||
4. **Swagger UI under its CSP.** Enable `http.api_docs`, open `/api-docs`. Expected: the UI renders the spec with no CSP violations in the console. Why human: browser rendering; the SRI values were checked automatically.
|
||||
5. **Triage the open review warnings WR-01..WR-09.** Set each disposition in 19.3-REVIEW-DISPOSITION.md (fix in the gap-closure plan, defer with a reason, or skip). WR-04 and WR-02 are security-relevant and cheap to fix next to CR-01.
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
The phase delivers almost all of its goal, and the evidence is solid: the gate runs green including Docker, the cross-instance rate limit is proven against a real Redis, the cookie refresh path uses the real `Refresh` and its blacklist, the S3 tag guard works in both tag modes, and the api-docs SRI values match the CDN.
|
||||
|
||||
One gap remains, and it hits the phase's headline flow. `cors:<profile>`, the named credentialed-CORS middleware that D-05 added for SPA route groups, is applied at its list position inside body-limit and panic recovery. A guard, throttle or Turnstile refusal listed before it, a guard from an enclosing group, a 413 and a recovered 500 all leave without `Access-Control-Allow-Origin`, so the browser hides the 401 the SPA needs in order to refresh its cookie. I reproduced this on HEAD. Apps that use the global `http.cors` paths are not affected, which is why SC-1 is partial rather than absent. The fix is local: apply the profile outermost in `Router.wrap`, add a refusing-guard test to the gate, and adjust the routing docs. That needs a small gap-closure plan and a patch release (v0.1.28). No later phase (19.4 OIDC, 19.5 social login, 19.6 2FA) covers it, so it is not deferred.
|
||||
|
||||
**If you decide to accept the current behaviour instead** (for example, by documenting that `cors:<profile>` must come first and only the global `http.cors` covers 413/500), record it as an override:
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- must_have: "D-05: a named middleware cors:<profile> applies the profile's CORS policy to routes that carry it"
|
||||
reason: "Documented that cors:<profile> must be listed first (or set on the group); 413/500 are not covered; apps that need every response covered use http.cors paths"
|
||||
accepted_by: "{name}"
|
||||
accepted_at: "{ISO timestamp}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-10-15T20:45:00Z_
|
||||
_Verifier: Claude (msd-verifier)_
|
||||
Reference in New Issue
Block a user