137 lines
7.5 KiB
Markdown
137 lines
7.5 KiB
Markdown
---
|
|
phase: quick-261006-sne
|
|
plan: 01
|
|
status: complete
|
|
subsystem: cabana admin API + admin SPA
|
|
tags: [markdown, preview, xss, csrf, admin-spa, openapi]
|
|
requires: [cabana.RenderMarkdown, requireAjax, decodeStrictBody]
|
|
provides:
|
|
- POST {prefix}/api/v1/markdown/preview (AdminMarkdownPreviewRequest -> Envelope[AdminMarkdownPreviewResult])
|
|
- MarkdownField Preview rendering server-sanitized HTML (markdown + mlmarkdown)
|
|
- .summer-markdown style kit
|
|
affects: [admin/openapi/admin.json, admin/src/api/schema.d.ts, modules/boardwalk/dist, raw-HTML hygiene gates]
|
|
tech-stack:
|
|
added: []
|
|
patterns:
|
|
- "Stateless backend-guarded POST behind requireAjax with strict capped body decode"
|
|
- "SPA request sequence counter + 300 ms debounce; raw-HTML binding fed only from a 2xx data field"
|
|
- "Hygiene gate exemption matched by file and exact attribute line"
|
|
key-files:
|
|
created:
|
|
- modules/cabana/markdown_preview_test.go
|
|
- modules/cabana/markdown_preview_route_test.go
|
|
modified:
|
|
- modules/cabana/field_markdown.go
|
|
- modules/cabana/http.go
|
|
- modules/cabana/admin_openapi.go
|
|
- modules/cabana/security_coverage_test.go
|
|
- modules/cabana/phase10_csrf_test.go
|
|
- modules/cabana/phase10_coverage_test.go
|
|
- modules/cabana/openapi_conformance_test.go
|
|
- modules/cabana/README.md
|
|
- admin/openapi/admin.json
|
|
- admin/src/api/schema.d.ts
|
|
- admin/src/components/form/fields/MarkdownField.vue
|
|
- admin/src/styles/main.css
|
|
- admin/tests/form/MarkdownField.test.ts
|
|
- admin/tests/form/MLFields.test.ts
|
|
- docs/backend/forms.md
|
|
- modules/boardwalk/dist/
|
|
- scripts/check-phase10.sh
|
|
- scripts/check-phase12.1.sh
|
|
- scripts/check-phase12.2.sh
|
|
- scripts/check-phase14.2.1.sh
|
|
decisions:
|
|
- "Markdown preview is a stateless POST /markdown/preview open to any backend session (no permission code); refused output is a 422 on markdown with a fixed message, never the renderer error text"
|
|
- "The SPA binds only data.data.html of a 2xx preview answer; refusals and network errors are text notices"
|
|
- "Raw-HTML hygiene gates exempt exactly one line: MarkdownField.vue `v-html=\"sanitizedHtml\"`"
|
|
metrics:
|
|
duration: 11m
|
|
completed: 2026-10-06
|
|
tasks: 3
|
|
files: 25
|
|
actuals:
|
|
tokens: 15800
|
|
tasks: 3
|
|
commits: 3
|
|
commits: 3
|
|
plan_head_before: 0ff928d6cf7ee6f43b2457c99ef401cb1855f535
|
|
plan_head_after: 27711b7c21a8545594fa9d548925d599bb91a237
|
|
---
|
|
|
|
# Quick 261006-sne: Render markdown field preview via sanitized server HTML Summary
|
|
|
|
The admin markdown Preview now renders headings, lists and links. The HTML comes from a new backend-guarded, CSRF-protected `POST {prefix}/api/v1/markdown/preview` route that runs `cabana.RenderMarkdown`. MarkdownField binds only that answer, and on mlmarkdown fields the preview follows the active locale.
|
|
|
|
## Tasks
|
|
|
|
| Task | Name | Commit | Key files |
|
|
|------|------|--------|-----------|
|
|
| 1 | Server route POST /markdown/preview (tracer) | b492e79 | field_markdown.go, http.go, admin_openapi.go, Go tests, admin.json, schema.d.ts, README, forms.md |
|
|
| 2 | MarkdownField Preview renders server-sanitized HTML, rebuilt dist | a0116df | MarkdownField.vue, main.css, MarkdownField.test.ts, MLFields.test.ts, forms.md, modules/boardwalk/dist |
|
|
| 3 | Raw-HTML gates allow the single sanctioned binding | 27711b7 | check-phase10/12.1/12.2/14.2.1.sh |
|
|
|
|
## What was built
|
|
|
|
- **Route:** `service.markdownPreview` re-checks the backend principal and answers 401 `unauthenticated` without one. It decodes `{markdown}` through `decodeStrictBody`: unknown keys, malformed JSON and trailing data get 422, and a body over the cap gets 413. It renders through `RenderMarkdown` and answers `{html}`. When the gate refuses the output, the answer is 422 `validation_failed` with a fixed message on `markdown`. The route is mounted in the `backend` guard group behind `requireAjax`. `RenderMarkdown` and its gate are unchanged.
|
|
- **Tracer gate:** the route ran end to end through guard, CSRF, renderer and OpenAPI conformance (`TestMarkdownPreviewRoute`, `TestPhase10OpenAPIConformance`) before the SPA work started.
|
|
- **SPA:** Preview fetches when it opens. While it is open, a source change triggers one re-fetch after 300 ms. A sequence counter drops stale answers. Blank sources send nothing. Error answers show the server's `details.markdown[0]` as text, and failed requests show "Preview unavailable.". `aria-busy` reflects loading.
|
|
- **Styles:** `.summer-markdown` style kit with low-specificity `:where()` selectors that read only `--c-*` variables and `--font-mono`.
|
|
|
|
## Verification
|
|
|
|
- `go vet ./...` and `go test ./...` passed before each of the three commits (testcontainers Postgres included).
|
|
- `scripts/check-admin-openapi.sh --check`, `scripts/check-admin-dist.sh` and `go test ./cmd/summer -run TestDocsTree` all pass.
|
|
- `npm --prefix admin test` passes (73 files, 1054 tests), and `npm --prefix admin run typecheck` is clean.
|
|
- `check-phase12.1.sh --hygiene` and `check-phase12.2.sh --hygiene` now pass; both failed before this change. `check-phase14.2.1.sh --forbidden` passes. `check-phase10.sh --hygiene` no longer reports a raw-HTML directive.
|
|
- The exemption was tested against the planted `<div v-html="raw" />` and against the variants `<div v-html="sanitizedHtml" />` and `v-html="other"` in MarkdownField.vue, using the real GNU grep. All three are still refused.
|
|
|
|
## Deviations from Plan
|
|
|
|
### Auto-fixed Issues
|
|
|
|
**1. [Rule 3 - Blocking] Second route inventory in phase10_coverage_test.go**
|
|
- **Found during:** Task 1 (full `go test ./...`)
|
|
- **Issue:** `TestPhase10Coverage/every_unsafe_mounted_route_is_CSRF-walked` keeps its own sorted list of unsafe routes and says "expects 25". The plan did not list this file.
|
|
- **Fix:** Added `POST /markdown/preview` to the list in sort order and changed the message to 26.
|
|
- **Files modified:** modules/cabana/phase10_coverage_test.go
|
|
- **Commit:** b492e79
|
|
|
|
**2. [Rule 1 - Bug] vue-tsc noUncheckedIndexedAccess in the new vitest file**
|
|
- **Found during:** Task 2 typecheck
|
|
- **Fix:** Added non-null assertions on indexed `sent[n]` / `pending[n]` accesses in MarkdownField.test.ts.
|
|
- **Commit:** a0116df
|
|
|
|
Small additions beyond the plan's behavior list:
|
|
- A trailing-data case in the Go invalid-body test.
|
|
- Closing Preview or unmounting bumps the sequence, so an answer still in flight is dropped.
|
|
|
|
## Pre-existing issues (not touched)
|
|
|
|
These `scripts/check-phase10.sh --hygiene` refusals predate this task and are out of scope:
|
|
- `admin/src/api/files.ts:150` uses `new XMLHttpRequest()`, which the direct-fetch rule catches.
|
|
- `admin/src/api/files.ts` is an unexpected file in `admin/src/api`.
|
|
- `admin/src/components/form/fields/FileCaptionModal.vue` is imported by no test.
|
|
- `admin/src/components/form/mlLocale.ts` is imported by no test.
|
|
|
|
Because of them, `scripts/check-phase10.sh --self-test` stops with "rejected the clean scratch copy" before it plants anything. The plant refusal was checked by hand instead (see Verification).
|
|
|
|
## Threat Flags
|
|
|
|
None beyond the plan's threat model. The one new surface, the T-261006sne-01..07 route and the raw-HTML sink, is mitigated as the plan specifies.
|
|
|
|
## Known Stubs
|
|
|
|
None.
|
|
|
|
## Manual UAT (not gating)
|
|
|
|
1. Rebuild and restart the proof host.
|
|
2. Open a markdown field at /backend and click Preview. `# Title` should render as a heading.
|
|
3. Open an mlmarkdown field, click Preview and switch the locale. The preview should follow the locale.
|
|
|
|
## Self-Check: PASSED
|
|
|
|
- FOUND: modules/cabana/markdown_preview_test.go, modules/cabana/markdown_preview_route_test.go, admin/src/components/form/fields/MarkdownField.vue
|
|
- FOUND commits: b492e79, a0116df, 27711b7
|