- seaglass (new module): bluemonday v1.0.27 policy ported from Journal
without the Blog Editor rules, byte-stable token comparison,
SanitizeReport, Sanitize, Check, Removals
- cabana: type richeditor (common keys plus size); every create and update
value sanitized before Fill, non-string values are a 422, removals become
meta.notices; settings and relation forms refuse the type
- backend lang richeditor.removed_* and markup_changed (en, pl plurals)
- TestSeaglassSmoke, TestCompanionsRichEditorSmoke, README and docs
- The process-global debounce map was keyed by token ID, so a stamp from one test database suppressed a later write in another and turned TestBackendTokenPostgres red depending on order
- Each compositeGuard now owns a stampThrottle; the backend guard and the ResolveBackend guard of one activation share it, so a single-app binary still writes last_used_at at most once a minute per token
- TestTokenLastUsedThrottlePerGuard pins the per-guard isolation
TestMain scanned os.Args for a literal -test.short, but go test passes
-test.short=true, so beachcomber, conga, lagoon and lighthouse started a
Postgres container even under -short. Parse the flags and use
testing.Short() instead; skip cabana's newListService fixture under -short
like adminGorm already does.
Account token POST/DELETE routes made TestPhase10Coverage expect 32 unsafe
routes; token_scopes.go is hand-authored and excluded from the scaffold pin.
Co-authored-by: Cursor <cursoragent@cursor.com>
Account token create/revoke added two state-changing mounts; update the
Phase 10 CSRF walk count so the release gate inventory stays green.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add compiled ResolveBackend example with acme.blog cover
- Include src= fence in authentication docs (Bearer-only)
Co-authored-by: Cursor <cursoragent@cursor.com>
- conformCases() lists every case at package level; checkConformInventory
reports every missing, extra or duplicate key in one sorted failure
- TestPhase10ConformanceInventory runs it without a database, and
TestPhase10OpenAPIConformance runs it before the testcontainers setup
- GET /field-types decodes into cabana.FieldTypesResponse and serves the
fixture type's script; POST .../fields/{field}/upload answers 201
cabana.Envelope[cabana.FieldUploadResponse] for a multipart part file
- the fixture plugin registers conformnote and the gadget form uses it,
so create, update and show run its Decode and Load (G-16.2-1, D-13, D-18)
liftMLValues validated only object-shaped submissions, so a scalar
submitted for a translatable plugin field reached Decode as nil, a
float64 or a bool although pact promises a string; a type that asserts
a string panicked into an opaque 500. decodePluginFields now passes a
string through, turns null into the empty string and refuses any other
value with a 422 on the field before Decode runs.
The route called the type's Upload on the streamed part and only then
checked that no second part followed, so a refused request left a
public, unreferenced object in the bucket. It now reads the multipart
body whole (ReadForm, spooling past 8 MiB to a temporary file), checks
it holds exactly one file part named file, and only then calls Upload.
The remaining window (a failed record transaction after Upload, an
element that stopped waiting) is documented.
The field upload route ran only protect, so the create form of a
controller without the create toolbar button could still write files to
the public bucket. It now refuses with 403 where the save route would:
create for {id} 0, update otherwise (operationDeclared).
- pact: FormFieldError message joining, nil pointer, funcs never serialized
- cabana: registry and option boot rules, compile (option kinds, general
and core keys, CompileForm vs CompileFormWithTypes, settings and relation
refusal), decode 422/500 split, localized and capped notices, Load,
ListValue with the request ctx, field writes, field-types and upload
routes
- TestFieldTypeSavePostgres (Docker host): create, show, untouched and
changed update, refused update, upload in and out of the record scope
- TestFieldTypeUntouched: served values skip Decode, writes and notices;
one changed locale writes and reports only that locale; canonical JSON
comparison; a create never skips
- PluginField: an element that echoes its value on connect is never
written a second time
- translatable types lift, store and hydrate through the ML path
(isMLField); Decode and Load run per locale and notices carry the locale
- on update a value equal to the served value (Load of the stored host
value or exact translation) is neither decoded nor written
- FieldWrite, FieldWritesFromContext and WithFieldWrites expose what a save
writes to the Form hooks
- list columns may use a plugin alias and show the type's ListValue
- POST .../{id}/fields/{field}/upload hands the file to the type's Upload
(guard, X-Requested-With, permissions, field context, record scope, body
cap); OpenAPI, route inventories, README and docs updated
- compileFieldTypes builds a per-boot field type registry (alias, core and
cross-plugin collisions, element prefix, assets, option keys) passed down
the compile functions; settings and relation forms refuse plugin aliases
- fields.yaml accepts a plugin alias with the general keys plus the type's
options (typeOptions, element, translatable in the schema); CheckOptions
runs at boot; CompileFormWithTypes for plugin tests
- save decodes plugin values inside the transaction (FormFieldError is a
422, other errors opaque 500) and answers meta.notices (FieldNotice) only
when a type reported something; show and save serve values through Load
- GET /field-types lists the types with versioned asset URLs; OpenAPI,
schema.d.ts, README and docs/backend/field-types.md updated
- TestListRelationSQL: DryRun matrix for belongsToMany (incl. custom pivot keys and self-reference), hasMany, hasOne and belongsTo search and sort, soft delete, 422 refusals, bound and LIKE-escaped search values, no outer JOIN next to ListExtendQuery and scope predicates
- TestListRelationSearchPostgres: exact total through two matching tags, string_agg sort order and hasMany search on Postgres
- TestEnabledLocalesDefaultFirst: default-locale ordering cases
- enabledContentLocales moves TranslationWriter.DefaultLocale to index 0
and keeps the other enabled codes in the writer's order
- every ML control opens on enabledLocales[0], so the form now shows the
host-column values (content, excerpt, title, slug) instead of an empty
non-default locale
- a DefaultLocale error is logged and keeps the writer's order
- README and forms docs: default locale first
- relationSubquery reads the pivot, key and table names from GORM's parsed
relationship (Schema.Relationships.Relations), never from a convention
- list search matches every relation column through a correlated EXISTS,
so the count query gains no JOIN and belongsToMany search stops failing
- sorting a hasMany or belongsToMany column orders by a correlated
string_agg subquery before the primary-key tiebreak
- polymorphic relations and unknown related columns answer 422
- DryRun smoke test TestListRelationSearchSmoke needs no Docker
- account_internal_test: metadata subtitle reader and writer (NULL, blank,
null, [], invalid JSON, non-objects, key preservation, NULL on the last
key), the attachOne contract, avatar cap, unique-violation mapping,
English fallbacks, and 401 for non-backend principals on all three routes
- account_test (Postgres): profile payload incl. copied WinterCMS metadata
and avatar rows, trimmed profile saves and metadata keys, the forbidden-key
allow-list with unchanged rows, email rules, password rules, the failed
attempt log, Bearer and cookie session effects, CSRF, avatar upload,
refusals (SVG, HTML as PNG, size, part shape, 413, no bucket) with no row
or blob left, replace and remove with blob cleanup
- lagoon: TestBackendAdminMetadataColumn covers type, Winter row round trip,
rollback of only the column and re-migrate
- SPA: AccountView, the user menu account item and avatar image,
setCurrentUser and profileInitials; lang fixture gains myaccount keys
- POST /account/avatar with no multipart part (NextPart io.EOF) or a broken
boundary fell through to the generic 500; it is now the invalid-body 422
- a body over the cap still answers 413 payload_too_large
- cabana.BackendUser is an attach.Owner (MorphName Backend\Models\User,
cabana.BackendUserMorphType) with one public attachOne avatar
(cabana.BackendUserAvatarField), as WinterCMS stores it
- POST /account/avatar stores one file_data image (jpg, jpeg, png, gif or
webp, image guard, at most cabana.AvatarMaxBytes = 2 MiB, under a
server-generated name) and replaces the previous avatar: old rows are
deleted in the transaction, their blobs after the commit
- DELETE /account/avatar removes the avatar and is idempotent
- GET /auth/me and the account routes return avatar {url, thumb_url}
(250x250 crop); cabana.BackendUserAvatar lets plugins read it
- swag annotations, regenerated admin.json and schema.d.ts; route
inventories, CSRF walk (29) and OpenAPI conformance learn both routes
- SPA: avatar upload and remove on My account, avatar image in the user
menu trigger and header; backend::lang.myaccount avatar strings
- rebuilt boardwalk dist; cabana README, users-and-permissions and
admin-spa docs
- lagoon: 202610090001_backend_users_metadata adds WinterCMS's nullable
jsonable backend_users.metadata column (TEXT); rollback drops only it
- cabana.BackendUser.Metadata and Subtitle read metadata.subtitle leniently
(NULL, blank, null, [], invalid JSON and non-strings read as empty)
- GET /auth/me returns subtitle; PUT /account edits the signed-in admin's
own names, email and subtitle, and the password with current_password;
any other key is a 422 and nothing is written
- email: required, valid, 6-255 chars, unique against other admins' email
and login ignoring case (soft-deleted included); a unique-index race is 422
- a password change sets tokens_valid_after, revokes the presented token and
gives a cookie session a fresh cookie
- swag annotation, regenerated admin.json and schema.d.ts; route inventories,
CSRF walk (27) and OpenAPI conformance learn the route
- SPA: My account item in the user menu opens /account (AccountView), which
saves through PUT /account and updates the top bar via setCurrentUser
- backend::lang.myaccount strings (en, pl), rebuilt boardwalk dist
- cabana and lagoon READMEs, users-and-permissions and admin-spa docs
- Add surf.Router.AdminPrefix (additive): the admin mount path and true
when BuildRouter mounted the cabana admin, false otherwise
- serve binds with net.Listen before printing, so a busy port fails
without a misleading URL, then prints the listen address with a
browser URL and an admin status line: the admin URL, a warning naming
summer admin:create when backend_users is empty or missing, or
admin: off with the steps to add an admin controller
- The administrator count is bounded by a 2 s timeout and never fails boot
- route:list ends with the same admin status line
- surf and cabana READMEs and the console and routing docs describe the output
pact.SettingsItem gains an additive Controller field, the equivalent of a
WinterCMS registerSettings 'url' => Backend::url(...) entry. A link entry
declares no Model, Form or NewModel and needs no AdminFS; start-up fails
when it combines Controller with a singleton form or a model, or names an
unregistered controller. Settings codes stay one namespace.
GET /settings lists a link entry with its controller only when the
principal passes the item's permissions and may open the controller.
Registry.Setting never returns a link entry, so the singleton settings
endpoints answer 404 for its code. SettingsEntry carries controller,
empty for singletons; the OpenAPI document, generated SPA types and
settings fixture follow, and the pact and cabana READMEs and the
settings docs describe the link.
- mltextarea compiles (with size), binds as a writable scalar and goes
through the mltext lift, translation write and hydrate paths
- preset on or from an mltextarea field stays refused
- tests: compile, Postgres round trip over a nullable host column with
multi-line text, invalid locale maps, preset refusals
- schema_types.go and the form-schema swag description name text or mltext
- regenerated admin/openapi/admin.json and admin/src/api/schema.d.ts
- README and docs/backend/forms.md describe the per-locale rules
- compilePresetKey admits a text or mltext target
- checkPresets admits a text or mltext source
- TestPresetML pins every text/mltext pairing and the refusals
- POST {prefix}/api/v1/markdown/preview renders {markdown} through
cabana.RenderMarkdown in the backend-guarded group behind requireAjax
- refused output is a 422 validation_failed on markdown with a fixed message
- swag annotation, regenerated admin.json and schema.d.ts
- route inventories, CSRF walk (26) and OpenAPI conformance learn the route
- README and docs/backend/forms.md document the route
- table-driven hostScalarString cases for nil and live pointers to string, ints, uint, float, bool, []byte, named string and double pointers
- DB-free hydrateMLRecord cases for nil, empty and filled *string hosts
- hostScalarString walks pointers via reflect; nil at any depth is ""
- string kinds return raw text, byte slices decode, other kinds format the dereferenced value
- Postgres round-trip regression for an mltext field on a *string column
UpdateChild and ShowChild now lift, apply, and hydrate nested locales the same way as host CRUD, so a child form keeps English on the column and Polish through the writer.
Co-authored-by: Cursor <cursoragent@cursor.com>
RelationService looks up TranslationWriter like CRUDService so CreateChild can lift locale maps before fillChild, write Polish after the child PK, and return a hydrated title map.
Co-authored-by: Cursor <cursoragent@cursor.com>
MLMarkdownField and relation-child forms share the enabled-locale
inject and adopt merge; OpenAPI, types, docs, and boardwalk dist match.
Co-authored-by: Cursor <cursoragent@cursor.com>
Form schema meta lists enabled locales, Show/save expand mltext maps
without D-11 fallback, and the SPA seeds and adopt-merges every locale.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Document markdown, mltext, and mlmarkdown plus TranslationWriter save semantics
- Extend the SPA registry tests and rebuild committed boardwalk dist
Co-authored-by: Cursor <cursoragent@cursor.com>
Lift locale maps before ProjectWritableFields so a Journal-shaped save can persist the default host scalar and non-default locales through TranslationWriter without dropping nested JSON.
Co-authored-by: Cursor <cursoragent@cursor.com>
- acme-demo-lookup fixture is a reorder widget: a click reverses its items,
sends the new id order as detail.payload and renders the returned items from
the data attribute and the summer-result event with textContent, no HTTP
- partials-and-widgets gains a Widget element contract subsection listing the
attributes the SPA sets and the summer-action / summer-result events
- cabana README names both events in the form widgets bullet
- pact.AdminActionInput.Payload (json.RawMessage) carries the widget's own
JSON value untouched; pact.AdminActionResult.Data is passed through as data
- cabana decodes payload with a 64 KiB cap (422 on body), refuses it on the
toolbar and record routes, and embeds Data once encoded with a 256 KiB cap
(opaque 500 when larger or unencodable); fill stays filtered
- root .swaggo overrides json.RawMessage so swag keeps record_id and values;
admin.json and schema.d.ts regenerated (payload?: unknown, data?: unknown)
- TestWidgetPayloadAndData covers pass-through, cap, refusal and data 500
- cabana and pact READMEs, partials-and-widgets and admin-spa docs updated