From 2325d80ecb5d93e093a4388e45dd53a96fbea9d1 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 28 Sep 2026 23:54:08 +0200 Subject: [PATCH] docs(10.1-01): complete framework Go extension point plan --- .../10.1-01-SUMMARY.md | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 .planning/phases/10.1-runtime-admin-extension-point/10.1-01-SUMMARY.md diff --git a/.planning/phases/10.1-runtime-admin-extension-point/10.1-01-SUMMARY.md b/.planning/phases/10.1-runtime-admin-extension-point/10.1-01-SUMMARY.md new file mode 100644 index 0000000..4da5890 --- /dev/null +++ b/.planning/phases/10.1-runtime-admin-extension-point/10.1-01-SUMMARY.md @@ -0,0 +1,282 @@ +--- +phase: 10.1-runtime-admin-extension-point +plan: 01 +subsystem: api +tags: [go, cabana, pact, boardwalk, html-template, x-net-html, openapi, swag, admin] + +requires: + - phase: 10-admin-vue-spa + provides: cabana admin API, requireAjax CSRF guard, boardwalk SPA serving, typed admin OpenAPI pipeline, conformance and inventory tests + - phase: 10.2-nest-framework-packages-under-modules-and-write-run-docs + provides: modules/ layout and module README rules +provides: + - pact contracts AdminClientAssets, AdminAction, AdminActionInput, AdminActionResult, HasAdminActions, AdminPartialData + - fields.yaml type widget (widget, action, fill) and type partial (path); config_list.yaml headerPartial; registered toolbar.buttons names + - POST {prefix}/api/v1/{vendor}/{plugin}/{controller}/widgets/{field} and .../toolbar/{action} + - GET {prefix}/api/v1/{vendor}/{plugin}/{controller}/partials/{name} returning an allowlisted node tree + - GET {prefix}/assets/{vendor}/{plugin}/{file...} exact-allowlist plugin asset route with SPA fall-through + - list and form schema keys assets, toolbarActions, headerPartial and field keys widget, action, actionLabel, fill, path + - exported boardwalk.ContentType and boardwalk.SetSecurityHeaders + - typed OpenAPI operations and schemas (AdminActionRequest, AdminActionResult, ControllerAssets, ToolbarAction, PartialNode, PartialView) +affects: [10.1-02 SPA extension seams, 10.1-03 Albums extension, 10.1-04 unit tests and gate, 14 Discogs] + +actuals: + tokens: 39500 + tasks: 3 + commits: 3 +plan_head_before: 9b98d8409fcfc25e815983ced43853f21ae3be64 +plan_head_after: 771d2ccce0bf3357d4be1d200bebe906f5a6bb70 + +tech-stack: + added: [golang.org/x/net/html (promoted from indirect to direct, no new module)] + patterns: + - "Cabana-owned action routes: plugins register Go Run functions; cabana owns the route, CSRF, permissions and record scope" + - "Single action namespace: toolbar.buttons and widget action: both resolve through CompiledController.Actions; create and delete are reserved" + - "Exact-key asset allowlist built at boot, miss falls through to the SPA handler" + - "Server-sanitized partials: html/template Clone per request, ParseFragment, allowlist walk into JSON nodes with caps" + +key-files: + created: + - modules/cabana/extension.go + - modules/cabana/actions.go + - modules/cabana/plugin_assets.go + - modules/cabana/partial_render.go + modified: + - modules/pact/capabilities.go + - modules/pact/README.md + - modules/boardwalk/boardwalk.go + - modules/boardwalk/README.md + - modules/cabana/form_schema.go + - modules/cabana/list_schema.go + - modules/cabana/schema_types.go + - modules/cabana/contracts.go + - modules/cabana/registry.go + - modules/cabana/messages.go + - modules/cabana/settings.go + - modules/cabana/http.go + - modules/cabana/admin_openapi.go + - modules/cabana/README.md + - modules/cabana/openapi_conformance_test.go + - modules/cabana/security_coverage_test.go + - admin/openapi/admin.json + - admin/src/api/schema.d.ts + - go.mod + - ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_security_test.go + - ../fonoteka.go/plugins/golem15/fonoteka/admin_collections_test.go + +key-decisions: + - "An action Label containing :: is treated as a phrase key and must resolve at boot; other labels are literal button text" + - "A toolbar action body must be {} exactly: record_id or values (even an empty values object) is a 422" + - "The partial view-model guard also refuses types containing html/template trusted content types (template.HTML and siblings), enforcing the plan's no-raw-HTML prohibition" + - "Partial and asset href/src URLs with whitespace or control characters are refused, since browsers strip them and could turn //host into a protocol-relative URL" + - "The empty-body POST is a 422 like any malformed body; the SPA sends {}" + +patterns-established: + - "compileExtension runs after BindWritableFields and owns every controller-level extension check (actions, widgets, assets, partials)" + - "Per-request locale for action messages and partial trans through towel.WithLocale(schemaLocale(...))" + +requirements-completed: [ADMIN-07] + +coverage: + - id: D1 + description: "Widget field runs its registered action through POST .../widgets/{field}; the response fill holds only the declared fill keys" + requirement: ADMIN-07 + verification: + - kind: integration + ref: "modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance/POST_/{vendor}/{plugin}/{controller}/widgets/{field}" + status: pass + - kind: unit + ref: "modules/cabana/phase10_csrf_test.go#TestPhase10CSRF" + status: pass + - kind: unit + ref: "modules/cabana/security_coverage_test.go#TestPhase09PermissionMatrix" + status: pass + human_judgment: false + - id: D2 + description: "Controller JS/CSS served at {prefix}/assets/{vendor}/{plugin}/... with explicit MIME, nosniff, CSP, CORP, no-cache and ETag; schema URLs carry ?v=; undeclared files fall through to the SPA 404" + requirement: ADMIN-07 + verification: + - kind: integration + ref: "modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance/GET_/{vendor}/{plugin}/{controller}/schema/list (assertPluginAsset)" + status: pass + - kind: integration + ref: "../fonoteka.go/plugins/golem15/fonoteka#TestPhase10TracerSPA" + status: pass + human_judgment: false + - id: D3 + description: "Registered toolbar actions: toolbar.buttons resolves create, delete and registered names; toolbarActions is permission-filtered; POST .../toolbar/{action} runs the action" + requirement: ADMIN-07 + verification: + - kind: integration + ref: "modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance/POST_/{vendor}/{plugin}/{controller}/toolbar/{action}" + status: pass + - kind: unit + ref: "modules/cabana/messages_test.go#TestPhase10Toolbar" + status: pass + - kind: unit + ref: "modules/cabana/list_schema_test.go#TestListSchemaRejects" + status: pass + human_judgment: false + - id: D4 + description: "Header and form partials render through html/template into an allowlisted, capped node tree served by GET .../partials/{name}" + requirement: ADMIN-07 + verification: + - kind: integration + ref: "modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance/GET_/{vendor}/{plugin}/{controller}/partials/{name}" + status: pass + - kind: unit + ref: "modules/cabana/form_schema_test.go#TestFormSchemaRejects" + status: pass + human_judgment: false + - id: D5 + description: "Sanitizer edge behaviour (dropped script/svg/on*/style/id, javascript: and // URLs, 64 KiB, 2000-node and depth-32 caps) and the model-type guard have dedicated unit tests" + verification: [] + human_judgment: true + rationale: "Checked here only with a throwaway probe; the named sanitizer, asset and action unit tests (TestPhase101*) are plan 10.1-04's scope per CLAUDE.md rule 3" + - id: D6 + description: "OpenAPI document and generated TypeScript types cover every new admin API route" + requirement: ADMIN-07 + verification: + - kind: other + ref: "scripts/check-admin-openapi.sh --check" + status: pass + - kind: unit + ref: "modules/cabana/phase09_contract_test.go#TestPhase09ContractInventory" + status: pass + - kind: other + ref: "npm --prefix admin run typecheck" + status: pass + human_judgment: false + +duration: 25min +completed: 2026-09-28 +status: complete +--- + +# Phase 10.1 Plan 01: Runtime admin extension point (framework Go) Summary + +**cabana-owned widget, toolbar and partial routes backed by new pact contracts, an exact-allowlist plugin asset route under the admin prefix, and an html/template partial renderer whose output is sanitized with x/net/html into a capped JSON node tree, all typed in the admin OpenAPI document and proven on the acme conformance fixture** + +## Performance + +- **Duration:** 25 min +- **Started:** 2026-09-28T21:28:17Z +- **Completed:** 2026-09-28T21:52:58Z +- **Tasks:** 3 +- **Files modified:** 31 in summercms.go, 2 in fonoteka.go + +## Accomplishments + +- Six pact contracts (`AdminClientAssets`, `AdminAction`, `AdminActionInput`, `AdminActionResult`, `HasAdminActions`, `AdminPartialData`) that plans 02 and 03 build against. +- `type: widget` fields with boot checks: the tag must start with the plugin's `{vendor}-{plugin}-` prefix and must not be a reserved name, the action must be registered, fill keys must be writable scalar fields of the same form, and the controller must declare JS. `POST .../widgets/{field}` loads the record through `FormExtendQuery` without a row lock and filters fill keys on the server. +- Registered toolbar actions share one namespace with widget actions, with `create` and `delete` reserved. `toolbarActions` in the list schema is filtered by permission, and `POST .../toolbar/{action}` accepts only `{}`. +- Plugin JS/CSS is read and sha256-hashed at boot and served by exact key with `boardwalk.SetSecurityHeaders`, `Cross-Origin-Resource-Policy: same-origin`, `no-cache` and an ETag. List and form schemas carry `assets` URLs with `?v=`. A lookup miss falls through to the SPA, so dist assets still load. +- `type: partial` is supported and `headerPartial` is added. Templates are parsed at boot and cloned for each request. Output goes through x/net/html `ParseFragment` and a tag, attribute and URL allowlist, with caps of 64 KiB, 2000 nodes and depth 32. `GET .../partials/{name}` with an optional scoped `?id=` serves the result. +- OpenAPI regenerated with the recursive `cabana.PartialNode`. Route inventories were updated in both repositories, and the acme conformance fixture exercises widget, toolbar, both partials and both assets end to end. + +## Task Commits + +summercms.go: +1. **Task 1: widget action tracer** - `f928194` (feat) +2. **Task 2: controller assets and toolbar actions** - `8b1cb24` (feat) +3. **Task 3: header and form partials** - `771d2cc` (feat) + +fonoteka.go (route inventory, separate repository): +1. `dcb64c9` test(10.1-01): expect the framework widget action route +2. `75ab47f` test(10.1-01): expect the framework toolbar action route +3. `be3fbf4` test(10.1-01): expect the framework partial route and new partial error + +## Files Created/Modified + +- `modules/pact/capabilities.go`: the six extension contracts +- `modules/cabana/extension.go`: boot validation of actions, widgets, client assets and partials +- `modules/cabana/actions.go`: widget and toolbar handlers, strict body decode, `readScopedRecord`, fill filter +- `modules/cabana/plugin_assets.go`: exact-allowlist asset handler and schema URL builder +- `modules/cabana/partial_render.go`: template parse and render, allowlist walk, caps, view-model guard, partial handler +- `modules/cabana/form_schema.go`, `list_schema.go`, `settings.go`: widget, partial, `headerPartial` and toolbar YAML rules +- `modules/cabana/schema_types.go`, `contracts.go`, `registry.go`, `messages.go`, `http.go`, `admin_openapi.go`: types, registry wiring, label checks, routes, annotations +- `modules/boardwalk/boardwalk.go`: exported `ContentType`, `SetSecurityHeaders` +- `admin/openapi/admin.json`, `admin/src/api/schema.d.ts`: regenerated +- `admin/tests/fixtures/*.json`: `assets` and `toolbarActions` keys for the typed fixtures +- READMEs of pact, cabana and boardwalk + +## Decisions Made + +- An action label containing `::` is a phrase key and must resolve at boot. Any other label is literal text. +- A toolbar action body must be exactly `{}`. A `record_id` or `values` key is a 422, so a toolbar action can never become a record lookup. +- The partial view-model guard also refuses types that contain html/template's trusted content types (`template.HTML` and its siblings). This enforces the plan's "no raw HTML string marked safe" rule at the type level. The allowlist still backs it up for values stored behind `any`. +- Any href or src containing whitespace or control characters is refused. Browsers strip those characters, so `//host` would otherwise become a protocol-relative URL. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Renamed the new scalar helper to avoid a collision** +- **Found during:** Task 1 +- **Issue:** `scalarValue` already exists in `query.go` (it converts a filter jsonScalar), so the package did not compile. +- **Fix:** Named the new predicate `isJSONScalar`. +- **Files modified:** modules/cabana/actions.go +- **Committed in:** f928194 + +**2. [Rule 3 - Blocking] Updated the unsafe-route pins in TestPhase10CSRF and TestPhase10Coverage** +- **Found during:** Tasks 1 and 2 +- **Issue:** Both tests pin the exact set of mounted unsafe routes (9, besides login). The plan's inventory step named only `phase09Routes` and `phase09ProtectedCalls`. +- **Fix:** Added each new POST to the pinned list and raised the count to 10, then 11. The CSRF walk still exercises every new route automatically. +- **Files modified:** modules/cabana/phase10_csrf_test.go, modules/cabana/phase10_coverage_test.go +- **Committed in:** f928194, 8b1cb24 + +**3. [Rule 3 - Blocking] Updated the exact-JSON list schema expectations** +- **Found during:** Task 2 +- **Issue:** `TestListSchemaCompile/Empty/Single/Filter` compare whole `ListSchema` JSON, and `toolbarActions` and `assets` are always emitted. +- **Fix:** Inserted `"toolbarActions":[],"assets":{"scripts":[],"styles":[]}` after `toolbarButtons` in the expected strings. +- **Files modified:** modules/cabana/list_schema_test.go +- **Committed in:** 8b1cb24 + +**4. [Rule 3 - Blocking] Updated a fonoteka test that pinned the Phase 9 partial rejection text** +- **Found during:** Task 3 (full fonoteka suite) +- **Issue:** `TestCollectionsAdminRejectsPartial` expected "type partial is not supported", which the plan removes. +- **Fix:** It now expects "type partial needs a path". A bare legacy `type: partial` still fails boot, for the new reason. +- **Files modified:** ../fonoteka.go/plugins/golem15/fonoteka/admin_collections_test.go +- **Committed in:** be3fbf4 (fonoteka.go) + +**5. [Rule 2 - Missing critical] Extra URL and view-model hardening** +- **Found during:** Task 3 +- **Issue:** The plan's URL rule (one leading `/`) is bypassable with tab or newline characters. Its model-type guard does not cover `template.HTML` fields. +- **Fix:** `safePartialURL` refuses whitespace and control characters. `refusedViewModel` refuses trusted template content types. +- **Files modified:** modules/cabana/partial_render.go +- **Committed in:** 771d2cc + +--- + +**Total deviations:** 5 auto-fixed (4 blocking, 1 missing critical) +**Impact on plan:** All were needed to keep pinned tests green or to close a security gap. No scope creep. + +## Issues Encountered + +- `go.sum` did not change; `go mod tidy` only moved `golang.org/x/net` to the direct block. `go.sum` is listed in the plan's files but needed no edit. +- Task 1 and Task 2 ran the plan's targeted fonoteka tests. The full fonoteka suite ran at Task 3 and surfaced deviation 4, which was fixed in the same task. + +## Known Stubs + +None. The fixture actions are test doubles by design, and the Discogs stubs belong to plan 10.1-03. + +## User Setup Required + +None. No external service configuration required. + +## Next Phase Readiness + +- Plan 10.1-02 (SPA) can consume `assets`, `toolbarActions`, `headerPartial`, the widget field keys, and the `PartialNode`/`PartialView`/`AdminActionResult` schema aliases from `schema.d.ts`. +- Plan 10.1-03 (Albums) implements `AdminClientAssets`, `HasAdminActions` and `AdminPartialData` on the albums controller. +- Plan 10.1-04 owns the named unit tests (TestPhase101FormExtensionSchema, Actions, Toolbar, Assets, PartialSanitizer) and `scripts/check-phase10.1.sh`. +- The README partial example uses the `summer-stats` class names. The admin SPA only styles them after plan 10.1-02. + +## Self-Check: PASSED + +- FOUND: modules/cabana/extension.go, actions.go, plugin_assets.go, partial_render.go, modules/pact/capabilities.go, admin/openapi/admin.json +- FOUND commits: f928194, 8b1cb24, 771d2cc (summercms.go); dcb64c9, 75ab47f, be3fbf4 (fonoteka.go) +- Plan verification: `go vet ./... && go test ./...` (summercms.go), `go vet` and `go test ./plugins/golem15/fonoteka/...` (fonoteka.go), `scripts/check-admin-openapi.sh --check`, `npm --prefix admin run typecheck`, `scripts/check-phase10.sh --hygiene`: all pass. + +--- +*Phase: 10.1-runtime-admin-extension-point* +*Completed: 2026-09-28*