@actuate-media/cms-core 0.59.1 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1722 -0
- package/README.md +33 -0
- package/dist/__tests__/api/page-sections-routes.test.js +103 -0
- package/dist/__tests__/api/page-sections-routes.test.js.map +1 -1
- package/dist/__tests__/collections/presets.test.js +19 -0
- package/dist/__tests__/collections/presets.test.js.map +1 -1
- package/dist/__tests__/forms/client-validation.test.d.ts +2 -0
- package/dist/__tests__/forms/client-validation.test.d.ts.map +1 -0
- package/dist/__tests__/forms/client-validation.test.js +98 -0
- package/dist/__tests__/forms/client-validation.test.js.map +1 -0
- package/dist/api/routes/meta.d.ts.map +1 -1
- package/dist/api/routes/meta.js +1 -0
- package/dist/api/routes/meta.js.map +1 -1
- package/dist/api/routes/sections.d.ts.map +1 -1
- package/dist/api/routes/sections.js +15 -4
- package/dist/api/routes/sections.js.map +1 -1
- package/dist/collections/presets.d.ts +10 -1
- package/dist/collections/presets.d.ts.map +1 -1
- package/dist/collections/presets.js +26 -5
- package/dist/collections/presets.js.map +1 -1
- package/dist/config/types.d.ts +8 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/forms/client-validation.d.ts +50 -0
- package/dist/forms/client-validation.d.ts.map +1 -0
- package/dist/forms/client-validation.js +215 -0
- package/dist/forms/client-validation.js.map +1 -0
- package/dist/forms/index.d.ts +1 -0
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +3 -0
- package/dist/forms/index.js.map +1 -1
- package/dist/forms/validation.d.ts.map +1 -1
- package/dist/forms/validation.js +14 -83
- package/dist/forms/validation.js.map +1 -1
- package/package.json +9 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1722 @@
|
|
|
1
|
+
# @actuate-media/cms-core
|
|
2
|
+
|
|
3
|
+
## 0.61.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0c3a6eb: Posts UX improvements from external-consumer feedback:
|
|
8
|
+
- New `admin.excludeFromPostsAggregate: true` collection flag keeps a post-shaped collection out of the admin's aggregate Posts area (cross-collection list, type tabs, New Post picker) while leaving its own admin list and API routes fully accessible. Enforced server-side in `GET /posts` and mirrored in the admin client.
|
|
9
|
+
- `blogPostCollection()` now satisfies the admin Post Editor's full field contract out of the box: it gains `sections` (json), `category`, and `publishDate` (replacing `publishedDate` — update seeds/queries that referenced the old key), and `body` is no longer hard-required so section-driven posts can publish.
|
|
10
|
+
- `create-actuate-cms` scaffolds now define `posts: blogPostCollection()` instead of an inline collection, so new projects always start with an editor-compatible posts schema.
|
|
11
|
+
|
|
12
|
+
## 0.60.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 854152d: Developer experience improvements from external-consumer feedback:
|
|
17
|
+
- Every published package now ships its `CHANGELOG.md` in the npm tarball, and packages that were missing a `README.md` (cli, cms-admin, cms-core, import, platform-vercel, all plugins, sections-react) now have one — so release notes and usage docs are visible on npmjs.com and in `node_modules`.
|
|
18
|
+
- New client-safe form validation entry point: `@actuate-media/cms-core/forms/client` exports `validateFormFieldClient()`, `validateFormValuesClient()`, visibility/conditional helpers, and the exact `FORM_EMAIL_RE` / `FORM_PHONE_RE` regexes the server enforces — zero Node dependencies, so public-site forms can mirror server rules without reimplementing them. The server validator now imports the same primitives, so client and server can never drift.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- bf5f857: `GET`/`PUT /post-templates/:type` now preserve sections whose type has no registered definition instead of silently dropping them. A template saved from the admin (which round-trips unmanaged sections) no longer loses seed/code-owned section types server-side; the derived `unmanaged` marker is stripped before persisting.
|
|
23
|
+
- Updated dependencies [854152d]
|
|
24
|
+
- @actuate-media/realtime@0.1.1
|
|
25
|
+
|
|
26
|
+
## 0.59.1
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- 15b3f45: Fix silent media upload data loss: `/media/upload` now returns a 500 (and creates no media record) when blob/adapter storage fails, instead of returning 201 with a dead `/api/cms/media/file/…` URL that nothing serves. Storage errors are logged with their cause (typically a missing `BLOB_READ_WRITE_TOKEN`). `/media/:id/optimize` likewise fails without touching the record when the optimized variant cannot be stored. Successful uploads now include `url` in the response so the admin can render previews immediately.
|
|
31
|
+
|
|
32
|
+
## 0.59.0
|
|
33
|
+
|
|
34
|
+
### Minor Changes
|
|
35
|
+
|
|
36
|
+
- f962e2d: New `defineSectionTypes()` authoring helper for custom section types. It is to `sections.types` what `defineConfig` is to the config file: a typed identity function with eager, fail-fast validation that throws one aggregated error naming every problem (blank/duplicate ids, built-in id collisions, unknown field types, select fields without options, repeaters without a sub-schema — with recursive sub-field checks). This replaces the silent-skip behavior consumers previously hit at runtime registration, where a malformed type simply never appeared in the Add Section gallery. Also exports `validateSectionTypeInputs()` (non-throwing issue list for tooling) and `SECTION_FIELD_TYPES` (runtime mirror of the field-type union). All three are available from both the package root (beside `defineConfig`) and the `/page-sections` subpath, along with the `CustomSectionTypeInput` and `SectionsConfig` types now re-exported from the root.
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- 73a117a: Add API-handler contract tests covering the full REST surface (265 routes). A checked-in contract table pins every registered route (in first-match-wins registration order) to the exact status an unauthenticated request must receive, with a documented allowlist for the 19 deliberately-anonymous endpoints. The suite fails loudly when a route is dropped or renamed, when a protected route accidentally becomes public, when a handler 500s on anonymous traffic, or when an error response stops emitting the `{ error: string }` JSON envelope. No runtime changes.
|
|
41
|
+
|
|
42
|
+
## 0.58.0
|
|
43
|
+
|
|
44
|
+
### Minor Changes
|
|
45
|
+
|
|
46
|
+
- 363bb3e: Strongly-typed database surface (M4): replaced the pervasive `db: any` / `type Db = any` pattern with a structural `ActuateDB` client type — one typed delegate per shipped model plus the `$`-method surface — backed by generated row/enum types for all 34 models and 17 enums (`scripts/generate-db-types.mjs`, output committed at `src/db/model-types.ts`).
|
|
47
|
+
- `guardedDb()`, `getDB<ActuateDB>()`, action transactions, auth/setup/forms/webhooks/cron/scheduling/SEO services now get delegate-name, method-name, and row-shape checking.
|
|
48
|
+
- New public type exports: `ActuateDB`, `ModelDelegate`, `QueryArgs`.
|
|
49
|
+
- Drift is caught two ways in CI: the e2e package compiles the real PrismaClient from the canonical schema and asserts it satisfies `ActuateDB` (`db-type-guard.ts`), and a freshness test verifies every schema model/enum appears in the generated types.
|
|
50
|
+
- Typed rows surfaced two latent unguarded relation accesses (password-reset token user, webhook retry endpoint) which now fail closed.
|
|
51
|
+
|
|
52
|
+
Query arguments deliberately remain loose (`QueryArgs = any`) — reproducing Prisma's generic argument inference by hand isn't maintainable, and anything narrower breaks real-client assignability.
|
|
53
|
+
|
|
54
|
+
## 0.57.1
|
|
55
|
+
|
|
56
|
+
### Patch Changes
|
|
57
|
+
|
|
58
|
+
- a5f8bc0: Internal refactor: split the 14,381-line `api/handlers.ts` monolith into 25 domain route modules under `api/routes/` (auth, documents, media, forms, SEO, users, webhooks, etc.), plus shared `api/route-helpers.ts` and `api/guarded-db.ts` modules. `registerCMSRoutes` is now a thin aggregator that preserves the original route registration order. No behavior or public API changes — `registerCMSRoutes`, `handleActuateAPI`, and all route paths are unchanged.
|
|
59
|
+
|
|
60
|
+
## 0.57.0
|
|
61
|
+
|
|
62
|
+
### Minor Changes
|
|
63
|
+
|
|
64
|
+
- 8e73cf5: Custom section types are now first-class. `actuate.config.ts` accepts `sections.types` as the canonical key (the previously-unreleased `sections.customTypes` remains as a deprecated back-compat alias, resolved via `resolveCustomSectionTypes`). The unscoped `GET /page-sections/types` endpoint now returns custom types alongside built-ins so admins can hydrate their registry from the API.
|
|
65
|
+
|
|
66
|
+
Add an explicit `onUnknown` strategy to `coerceSections` (`'drop' | 'preserve' | 'error'`) plus `coerceSectionsWithReport` and `UnknownSectionTypeError`, so callers can warn or block on unknown section types instead of silently dropping them. The legacy `preserveUnknown` flag still maps to `'preserve'`.
|
|
67
|
+
|
|
68
|
+
Add a generic `repeater` field type with a nested `fields` sub-schema and `minRows`/`maxRows`/`rowLabel` constraints; `stats` and `gallery` are now repeater presets. New helpers: `resolveRepeaterField`, `isRepeaterField`, `blankRepeaterRow`. Repeater rows (including required sub-fields and image alt text) are validated with row-scoped error/warning paths.
|
|
69
|
+
|
|
70
|
+
Add a `pagePathField()` field preset for an explicit page URL path.
|
|
71
|
+
|
|
72
|
+
## 0.56.0
|
|
73
|
+
|
|
74
|
+
### Minor Changes
|
|
75
|
+
|
|
76
|
+
- 99565a1: Add config-declared custom section types and preserve externally-managed sections (Opal feedback).
|
|
77
|
+
- New `sections.customTypes` on `ActuateCMSConfig` — declare custom section types (id, name, scope, fields, optional presentation/defaults) that are recognized, listed in discovery, and validated like built-ins. Registered at server boot in `handleActuateAPI`.
|
|
78
|
+
- New registry API on `@actuate-media/cms-core/page-sections`: `registerSectionTypes`, `resetCustomSectionTypes`, `getAllSectionTypes`, plus `CustomSectionTypeInput` / `SectionsConfig` types. `getSectionType` / `getSectionTypesForScope` / `isKnownSectionType` / `sectionTypeLabel` now merge built-ins with custom types (built-ins always win an id collision).
|
|
79
|
+
- `coerceSections(raw, { preserveUnknown: true })` keeps entries whose type isn't registered, flagged `unmanaged`, with content/settings verbatim and order intact, instead of dropping them. The page-section API (REST + MCP) now loads/persists with preservation so add/reorder/update/remove never drop seed/code-owned sections, and the derived `unmanaged` marker is never written back to storage.
|
|
80
|
+
- `PageSection.sectionType` is now a `string` (was the built-in `SectionTypeId` union) and `PageSection` gains an optional `unmanaged` flag. `createSection` / `buildSection` accept any registered type id.
|
|
81
|
+
|
|
82
|
+
## 0.55.0
|
|
83
|
+
|
|
84
|
+
### Minor Changes
|
|
85
|
+
|
|
86
|
+
- 74662c0: Standardize `MediaUsage.fieldName` to `fieldPath` and harden Blob diagnostics.
|
|
87
|
+
- Renames the `MediaUsage.fieldName` column to `fieldPath` to match the API layer, server actions, and `actuate doctor` expectations, with a new additive migration `0002_media_usage_field_path` (run `prisma migrate deploy`). Note: instances that already hand-renamed this column should mark 0002 as applied.
|
|
88
|
+
- Runtime env diagnostics now escalate a partial Vercel Blob link (a store is connected via `BLOB_STORE_ID`/webhook key but `BLOB_READ_WRITE_TOKEN` is missing) from a warning to an error, since uploads fail in that state.
|
|
89
|
+
- `createCMSMiddleware` now accepts `ActuateCMSConfig` directly (type-only), so consumers can pass their config without an `as unknown as` cast.
|
|
90
|
+
- The optional MySQL/SQLite database adapters now mark their lazy `import()`s as bundler-ignored (`webpackIgnore`/`turbopackIgnore`), eliminating spurious "Module not found" build warnings for drivers a Postgres project never installs.
|
|
91
|
+
|
|
92
|
+
## 0.54.0
|
|
93
|
+
|
|
94
|
+
### Minor Changes
|
|
95
|
+
|
|
96
|
+
- 29f32ba: Deepen `GET /api/cms/collections` field introspection (roadmap WS-B4). The discovery endpoint now recursively summarizes `array` element subfields (with `minRows`/`maxRows`) and `blocks` variant subfields (keyed by block `slug`, with labels), in addition to the existing `relationTo`/`hasMany`/`options` hints — so an AI agent can plan a deep create/update without guessing nested shapes. The summary stays an allow-listed projection (no validators/hooks/access functions leak), is depth-capped defensively, and is exposed as a reusable `summarizeCollectionFields()` export (`FieldSummary`/`BlockSummary` types).
|
|
97
|
+
|
|
98
|
+
## 0.53.0
|
|
99
|
+
|
|
100
|
+
### Minor Changes
|
|
101
|
+
|
|
102
|
+
- aa4bd1a: Unify the page content model at write time (ADR 0002). Visual Page-Builder pages now compile their `_layout` tree to the canonical `data.sections[]` on every save, so every reader (SDK, MCP, search, content graph, `/resolve`) sees one contract instead of the lazy read-time bridge.
|
|
103
|
+
- **cms-core**: `createDocument`/`updateDocument` derive `sections` from a `layout` envelope; the `/page-sections/*` write routes refuse a builder-owned page (one with `_layout`) with HTTP 409 to prevent silently-overwritten edits; the `/resolve` bridge is now a legacy fallback for un-migrated pages. New `migratePageBuilderSections()` export backfills legacy pages (idempotent).
|
|
104
|
+
- **cms-admin**: the flat Page Editor opens a builder-owned page read-only with an "Open in Page Builder" action; `EditorPage` gains a `builderOwned` flag.
|
|
105
|
+
- **cli**: new `actuate migrate:sections` command (with `--dry-run` / `--batch-size`) to backfill canonical sections for legacy page-builder documents.
|
|
106
|
+
|
|
107
|
+
## 0.52.0
|
|
108
|
+
|
|
109
|
+
### Minor Changes
|
|
110
|
+
|
|
111
|
+
- 01eef99: Security & reliability hardening from the production-readiness audit.
|
|
112
|
+
- **Write-time HTML sanitization is now recursive (stored-XSS fix).** Previously only top-level `richText` fields were sanitized on write. HTML nested in `array`/`blocks` fields, the canonical `sections[]` rich-text slots, and the page-builder `_layout` tree was stored raw — a stored-XSS path reachable by any authenticated writer because the public renderer trusts stored HTML. All reachable author-HTML sinks are now sanitized before storage via the new `sanitize-document` module.
|
|
113
|
+
- **AI analysis routes are rate-limited and input-capped.** `/ai/audit-document` and `/ai/suggest-internal-links` now enforce a per-user limit (60/hr) and a 50k-character input cap, and emit `ai_audit_document` / `ai_suggest_internal_links` audit-log events.
|
|
114
|
+
- **`/media/upload` gains a dedicated strict limit** (20/hr/user) in addition to the global IP limit.
|
|
115
|
+
- **Vision image fetches go through `safeFetch`** (SSRF-safe: host re-validation, DNS-rebinding defense, no redirects) with an 8 MB cap; `/ai/media-metadata` now passes real image bytes to the vision model instead of a bare URL.
|
|
116
|
+
- **`/redirects/record-404` is now reachable from the public site.** It is CSRF-exempt (it only increments an unauthenticated 404 counter) and bounds its stored `path`/`referrer` to 2048 chars.
|
|
117
|
+
- **`/public/globals/:slug` and `/public/navigations/:slug` no longer serve `ARCHIVED` documents.** They remain unauthenticated site chrome (not PUBLISHED-gated), but an explicitly retired record is excluded.
|
|
118
|
+
- A runtime warning is logged when a collection's `fields` is defined as an array instead of an object map (silent data loss otherwise).
|
|
119
|
+
|
|
120
|
+
## 0.51.2
|
|
121
|
+
|
|
122
|
+
### Patch Changes
|
|
123
|
+
|
|
124
|
+
- aeee3a4: Docs: correct the migration-baseline upgrade note shipped in 0.51.1.
|
|
125
|
+
|
|
126
|
+
The previous note told deployments "previously migrated with `prisma migrate deploy`" to run `prisma migrate resolve --applied 0001_init`. That guidance is wrong for exactly that audience: such a database already has a `0001_init` row in `_prisma_migrations`, so `resolve --applied 0001_init` errors with `P3008: migration is already recorded as applied`. Those deployments need **no action** — `migrate deploy` applies only _pending_ migrations and does not re-validate the squashed baseline, so it is a no-op.
|
|
127
|
+
|
|
128
|
+
`prisma migrate resolve --applied 0001_init` is only needed to baseline a database that already has the `actuate_*` tables but has **no** migration history (e.g. one provisioned via `db push`, or restored from a dump) before adopting `migrate deploy`. Run `prisma migrate status` first if unsure.
|
|
129
|
+
|
|
130
|
+
## 0.51.1
|
|
131
|
+
|
|
132
|
+
### Patch Changes
|
|
133
|
+
|
|
134
|
+
- f88a406: Fix document-create/update hijacking a collection's own `status` field, and reconcile the migration history into a clean baseline.
|
|
135
|
+
- **Forms (and any collection with a `status` field) can be created again.** `createDocument`/`updateDocument` treated a `data.status` value as the document lifecycle status. Collections such as Forms store their own `status` (`draft`/`active`/`archived`) inside `data`, so creating a form threw `Cannot create a document with status "draft"` and activating one would have written an invalid enum value. We now only hoist `status` onto the document column when it is an actual `DocumentStatus`; any other value stays in `data`. Regression tests added in `actions/document-crud.test.ts` and exercised end-to-end in the new `@actuate-media/e2e` harness.
|
|
136
|
+
- **Migration history baseline.** The committed migrations did not apply from an empty database (`0003` indexed `actuate_documents.title` before any migration created it) and were ~223 columns / several tables behind `schema.prisma`, so `prisma migrate deploy` on a fresh DB crashed. Migrations `0001`–`0012` are squashed into a single `0001_init` baseline generated from the current schema (plus the raw full-text-search GIN index). A PGlite-based drift guard now asserts the migrations apply from empty and reproduce the schema.
|
|
137
|
+
|
|
138
|
+
**Action required for existing deployments** that were previously migrated with `prisma migrate deploy`: run `prisma migrate resolve --applied 0001_init` once so Prisma records the baseline as already applied instead of attempting to re-create existing tables. Fresh installs need no action.
|
|
139
|
+
|
|
140
|
+
- **Config footgun guard.** `setActuateConfig` now logs a one-time warning when a collection declares `fields` as an array instead of the object-map shape cms-core expects (an array silently drops every custom field on write).
|
|
141
|
+
|
|
142
|
+
## 0.51.0
|
|
143
|
+
|
|
144
|
+
### Minor Changes
|
|
145
|
+
|
|
146
|
+
- a413616: Add MCP relationship-resolver tools (WS-C C2) so agents stop guessing document IDs.
|
|
147
|
+
- **cms-core (additive, backward-compatible):**
|
|
148
|
+
- `listDocuments` / `GET /collections/:slug` now accept an exact `slug` filter. Because `slug` is `@@unique([collection, slug])`, this is the canonical single-query slug→id lookup.
|
|
149
|
+
- The `GET /collections` discovery endpoint now surfaces `relationTo` (for relationship fields), `hasMany`, and select `options` in each field summary, so an agent can learn where a relationship field points. (Overlaps the WS-B4 schema-surfacing work.)
|
|
150
|
+
- **mcp-server:** three new tools — `get_relationship_fields` (list relationship/media/select fields + their `relationTo`), `resolve_relationship` (exact slug → `{ id, slug, title, status }`), and `list_relationship_targets` (browse candidate documents when the slug is unknown). README updated with a "Resolving relationships" section.
|
|
151
|
+
|
|
152
|
+
- 204da7a: WS-E (E3): media resolution + responsive images in the public render path.
|
|
153
|
+
- **sections-react:** new pluggable `renderImage` `RenderOptions` hook (with
|
|
154
|
+
exported `RenderImage`/`RenderImageProps` types and a `defaultRenderImage`).
|
|
155
|
+
The default emits a lazy, `decoding="async"` `<img>` and forwards intrinsic
|
|
156
|
+
`width`/`height` when known; a Next.js consumer can pass a `next/image`-backed
|
|
157
|
+
renderer for responsive `srcset` + optimization. The `feature`, `gallery`, and
|
|
158
|
+
post-header images now route through it (`<PostHeader>` gains an optional
|
|
159
|
+
`options` prop). The package stays React-only — no `next` dependency.
|
|
160
|
+
- **cms-core:** the `/resolve` endpoint now enriches section images with
|
|
161
|
+
intrinsic `width`/`height` and library `altText`, matched from `actuate_media`
|
|
162
|
+
by stored URL (`storageKey`/`publicUrl`). Additive and best-effort — the
|
|
163
|
+
stored document is never mutated and a lookup failure never breaks resolve.
|
|
164
|
+
`feature` sections gain `imageWidth`/`imageHeight` (+ backfilled `imageAlt`);
|
|
165
|
+
`gallery` rows gain `width`/`height` (+ backfilled `alt`).
|
|
166
|
+
- **create-actuate-cms:** the generated catch-all page renders CMS images via a
|
|
167
|
+
`next/image`-backed `renderImage` (falling back to a fluid `<img>` when the
|
|
168
|
+
CMS doesn't know dimensions), and `next.config` now allow-lists the storage
|
|
169
|
+
host under `images.remotePatterns` so optimization works out of the box.
|
|
170
|
+
|
|
171
|
+
- 2f18c56: WS-E (E2): public form rendering — a built site can now render and submit a CMS form end to end.
|
|
172
|
+
- **sections-react:** new `<ActuateForm>` Client Component fetches the public form
|
|
173
|
+
schema (`GET /api/cms/public/forms/:slug`), renders the fields (text/email/
|
|
174
|
+
url/tel/number/date, textarea, select, checkbox, radio), and submits to the
|
|
175
|
+
schema's `submitPath`. Spam protection is wired automatically: a hidden
|
|
176
|
+
honeypot (keyed from `spam.honeypotFieldKey`), an `elapsedMs` "time to submit"
|
|
177
|
+
timer, and an optional consumer-supplied `captchaToken`. Pre-fetch a schema via
|
|
178
|
+
the `form` prop to skip the round-trip (SSR/streaming). Adds a `baseUrl`
|
|
179
|
+
`RenderOptions` field (defaults to same-origin). Exposes pure, tested helpers
|
|
180
|
+
(`coercePublicForm`, `buildSubmitBody`, `fieldControl`, `firstMissingRequired`).
|
|
181
|
+
- **cms-core:** new canonical `form` section type. Set `content.formSlug` (plus
|
|
182
|
+
optional heading/intro) on any page or post and the matching renderer embeds
|
|
183
|
+
the form. The registry-drift guard now covers it (renderer ↔ type).
|
|
184
|
+
- **cms-admin:** the form Embed panel now shows the real public endpoints
|
|
185
|
+
(`/api/cms/public/forms/:slug` + `/submit`) and a copy-paste `<ActuateForm>`
|
|
186
|
+
snippet, replacing the previously-advertised `/api/forms/*`, iframe, and
|
|
187
|
+
`embed.js` routes that were never implemented. Adds a `mail` section icon.
|
|
188
|
+
|
|
189
|
+
- e2e0bd7: WS-E (E4): public navigation endpoint + navigation rendering in the scaffold.
|
|
190
|
+
- **cms-core:** new unauthenticated `GET /api/cms/public/navigations/:slug` that
|
|
191
|
+
returns a clean `{ name, slug, location, items }` tree. Like globals it is site
|
|
192
|
+
chrome — not status-gated — and safe when the navigation plugin isn't installed
|
|
193
|
+
(no match → 404). A shared, pure `coercePublicNavItems` helper sanitizes the
|
|
194
|
+
open `data.items` JSON: drops unlabeled nodes, strips internal `id`s, caps
|
|
195
|
+
recursion depth, and only passes through `_blank`/`_self` targets. Both
|
|
196
|
+
`createSiteClient` and `createServerSiteClient` gain `getNavigation(slug)`
|
|
197
|
+
returning `PublicNavigation | null`, and `PublicNavigation` / `PublicNavItem`
|
|
198
|
+
are now exported types.
|
|
199
|
+
- **sections-react:** new presentational `<SiteNav>` — an accessible `<nav>`
|
|
200
|
+
landmark whose submenus use native `<details>`/`<summary>` disclosure
|
|
201
|
+
(keyboard-operable, no client JS). New-tab links get
|
|
202
|
+
`rel="noopener noreferrer"`; an empty menu renders nothing. Exports `SiteNav`
|
|
203
|
+
plus `SiteNavProps`/`SiteNavItem`.
|
|
204
|
+
- **create-actuate-cms:** `lib/cms.ts` re-exports `getNavigation`, and the
|
|
205
|
+
generated site layout fetches the `main` menu and renders a header with the
|
|
206
|
+
site name + `<SiteNav>`, degrading gracefully to just the name when no menu
|
|
207
|
+
exists yet.
|
|
208
|
+
|
|
209
|
+
## 0.50.0
|
|
210
|
+
|
|
211
|
+
### Minor Changes
|
|
212
|
+
|
|
213
|
+
- d7a144c: Add a page-builder → canonical sections bridge so existing `_layout` documents aren't stranded (ADR 0001, WS-A5).
|
|
214
|
+
- New `layoutTreeToSections()` export on the `@actuate-media/cms-core/page-sections` subpath converts a page-builder `_layout` `BlockNode` tree into a best-effort canonical `PageSection[]`: it flattens the page → section → container/row/column → block nesting in document order, maps each block's `blockType` to the closest canonical section type, degrades unknown/complex blocks to a `text` section that surfaces their content, and propagates the enclosing builder section's anchor (`htmlId`) and hidden visibility. It is pure, DB-free, and never throws (`savedSectionRef` nodes are skipped — they require a separate lookup).
|
|
215
|
+
- The `/resolve` endpoint now applies the bridge at read time: when a page/post has a `_layout` tree but no flat `sections`, the resolved response is enriched with derived sections so the content renders through `@actuate-media/sections-react`'s `<Sections>`. This is non-destructive — the stored document is never mutated, and a document's own `sections` always take precedence.
|
|
216
|
+
|
|
217
|
+
## 0.49.0
|
|
218
|
+
|
|
219
|
+
### Minor Changes
|
|
220
|
+
|
|
221
|
+
- 9ba332c: Converge the scaffolder onto the canonical page-content contract (ADR 0001, WS-A4).
|
|
222
|
+
- **create-actuate-cms**: New projects now use the canonical `sections` (JSON `PageSection[]`) field for both `pages` and `posts`, and the generated public catch-all renders `data.sections` with `<Sections>` from `@actuate-media/sections-react` — passing cms-core's allow-list `sanitizeHtml` so author HTML (e.g. the article-body section) is sanitized before the DOM, and rendering a `<PostHeader>` for post-type documents. The deprecated Model C path is retired: the `content[]`/`BLOCK_MAP` `BlockRenderer`, `HeroBlock`, `CTABlock`, `TextBlock`, `ImageTextBlock`, `SiteHeader`, and `SiteFooter` generators (and the `blocks`-gated `layout`/headers/footers config) are removed. Generated apps now depend on `@actuate-media/sections-react` and emit the matching Tailwind `@source` directive.
|
|
223
|
+
- **@actuate-media/cms-core**: `ResolveResult` (the typed `createSiteClient().resolveDocument()` return) now exposes the optional `meta`, `metadata`, `jsonLd`, `jsonLdHtml`, `template`, `post`, and `preview` fields the `/resolve` endpoint already returns, plus the new `ResolvePostAuthor`, `ResolveRelatedPost`, `ResolvePostContext`, and `ResolvePreview` types — so consumers can render the full post experience (header, author, related posts, SEO metadata) without re-declaring the shape.
|
|
224
|
+
|
|
225
|
+
## 0.48.0
|
|
226
|
+
|
|
227
|
+
### Minor Changes
|
|
228
|
+
|
|
229
|
+
- ae1af8f: Authoring correctness fixes for the AI site-building workflow (roadmap WS-B/E quick wins).
|
|
230
|
+
- **cms-core**: `createDocument` now honors a requested `status` (`DRAFT` or
|
|
231
|
+
`PUBLISHED`) instead of always forcing `DRAFT`. Publishing on create is gated
|
|
232
|
+
to EDITOR/ADMIN, stamps `publishedAt`, and emits the `document.published`
|
|
233
|
+
webhook + sitemap notify. This fixes `POST /page-builder/create?publish=true`
|
|
234
|
+
(and `status: 'PUBLISHED'`), which previously created a draft silently.
|
|
235
|
+
Statuses other than `DRAFT`/`PUBLISHED` on create are rejected with guidance
|
|
236
|
+
to schedule/archive via update.
|
|
237
|
+
- **client**: `listMedia()` now returns the canonical
|
|
238
|
+
`{ docs, total, page, pageSize, totalPages }` (`MediaList`) envelope. The
|
|
239
|
+
server returns the asset array under `items`/`data` (never `docs`), so the
|
|
240
|
+
previous typing made `listMedia().docs` always `undefined`. Adds exported
|
|
241
|
+
`MediaAsset` and `MediaList` types and an optional `init` (signal/cache) arg.
|
|
242
|
+
- **mcp-server**: the `list_media` tool now normalizes the same `items`→`docs`
|
|
243
|
+
shape so its output matches `list_documents`.
|
|
244
|
+
- **create-actuate-cms**: generated `TextBlock`/`ImageTextBlock` now sanitize
|
|
245
|
+
author-supplied HTML with `sanitizeHtml` before `dangerouslySetInnerHTML`
|
|
246
|
+
(was an XSS vector on hostile content).
|
|
247
|
+
|
|
248
|
+
## 0.47.0
|
|
249
|
+
|
|
250
|
+
### Minor Changes
|
|
251
|
+
|
|
252
|
+
- 7d11b8d: Production-ready Settings → Updates tab with real release intelligence.
|
|
253
|
+
- **cms-core**: Fix a critical permission bug where `GET/PUT /updates/config` and
|
|
254
|
+
`POST /updates/apply` rejected real admins (they compared `session.role` to the
|
|
255
|
+
lowercase `'admin'` while roles are stored uppercase). They now use
|
|
256
|
+
`requireAdminScope`, so OWNER/ADMIN sessions (and admin-scoped API keys) can
|
|
257
|
+
manage updates. `GET /updates/check` no longer leaks the GitHub repo to
|
|
258
|
+
non-admins and reports a `canManage` flag. Update checks now run through a new
|
|
259
|
+
hybrid resolver `getUpdateStatus()` that queries the Actuate update server
|
|
260
|
+
first (rich, real changelog/severity/migration/breaking-change data derived
|
|
261
|
+
from the published CHANGELOG) and transparently falls back to a direct npm
|
|
262
|
+
registry check. New exports: `getUpdateStatus`, `checkForUpdatesViaServer`, and
|
|
263
|
+
the `ChangeType` / `ChangelogItem` / `PluginCompatibilityInfo` types; richer
|
|
264
|
+
`VersionInfo`. `fetchChangelog()` now targets the real `/api/changelog`
|
|
265
|
+
endpoint (overridable via `ACTUATE_UPDATE_SERVER_URL`).
|
|
266
|
+
- **cms-admin**: Rebuild the Updates tab as a dedicated `UpdatesTab` + `useUpdates`
|
|
267
|
+
hook (extracted from `Settings.tsx`). The GitHub integration now uses the shared
|
|
268
|
+
`SettingsControlRow` + sticky `SettingsSaveBar` dirty-state pattern, and the tab
|
|
269
|
+
surfaces real release notes (categorized changelog), a security-update badge,
|
|
270
|
+
"migrations required" and breaking-change callouts, and plugin compatibility.
|
|
271
|
+
Write actions are permission-aware (hidden for non-admins).
|
|
272
|
+
- **cli**: `update-check` / `upgrade` now resolve the update-server base URL from
|
|
273
|
+
`ACTUATE_UPDATE_SERVER_URL` (defaulting to the production host), so the
|
|
274
|
+
`/api/versions` endpoint is configurable for self-hosted update servers.
|
|
275
|
+
|
|
276
|
+
## 0.46.0
|
|
277
|
+
|
|
278
|
+
### Minor Changes
|
|
279
|
+
|
|
280
|
+
- 4e6ead2: Add Brand Voice. Admins can define a reusable brand voice (tone, audience, guidelines, and
|
|
281
|
+
sample copy) in Settings > AI. When enabled, the governed AI runtime injects it into the system
|
|
282
|
+
prompt of content-generation features — co-author and SEO titles/descriptions — so every AI
|
|
283
|
+
surface writes in one consistent, on-brand voice.
|
|
284
|
+
- `cms-core`: new `BrandVoiceProfile` type on `AISettings`, a `brand-voice` module
|
|
285
|
+
(`buildBrandVoiceSystemPrompt`, `applyBrandVoice`, `getActiveBrandVoice`, `normalizeBrandVoice`),
|
|
286
|
+
a `useBrandVoice` option on `generateTextForFeature`, and `PUT /ai/settings` persistence
|
|
287
|
+
(length-clamped, no secrets).
|
|
288
|
+
- `cms-admin`: a Brand Voice card in the AI settings tab wired through `useAiSettings`.
|
|
289
|
+
|
|
290
|
+
## 0.45.0
|
|
291
|
+
|
|
292
|
+
### Minor Changes
|
|
293
|
+
|
|
294
|
+
- df17a3f: Unify AI execution behind a governed runtime. Provider adapters now support text
|
|
295
|
+
generation (OpenAI/xAI/custom via the OpenAI-compatible chat API, Anthropic via
|
|
296
|
+
the messages API), and a new `ai/runtime` resolves a feature to its connection +
|
|
297
|
+
routed model, enforces the feature toggle and monthly token budget, resolves the
|
|
298
|
+
API key from env or the encrypted store, and records real token usage for the
|
|
299
|
+
usage meter. The `/ai/coauthor` endpoint now runs through this governed path
|
|
300
|
+
first — using the provider and per-feature model configured in Settings > AI —
|
|
301
|
+
and falls back to the legacy `@actuate-media/plugin-ai` global provider only when
|
|
302
|
+
no governed provider is configured. Disabled features return 403 and budget
|
|
303
|
+
overruns return 429.
|
|
304
|
+
|
|
305
|
+
New exports: `generateTextForFeature`, `resolveFeatureRuntime`,
|
|
306
|
+
`isAiFeatureEnabled`, and `adapterGenerateText`.
|
|
307
|
+
|
|
308
|
+
- 11b4dfa: Route the SEO assistant endpoints through the governed AI runtime. `/ai/seo/generate-field`
|
|
309
|
+
(titles, meta descriptions, focus keyword), `/ai/seo/explain-issue`, `/ai/seo/generate-schema`,
|
|
310
|
+
and `/ai/seo/summarize` now generate via the provider + per-feature model configured in
|
|
311
|
+
Settings > AI (gated by the `ai.features.seoMetaGeneration` toggle, enforcing the monthly token
|
|
312
|
+
budget and recording real usage). They fall back to the legacy `@actuate-media/plugin-ai`
|
|
313
|
+
provider only when no governed provider is configured. A disabled feature returns 403, a budget
|
|
314
|
+
overrun 429, and a provider failure 502; when nothing is configured the response is 503 with a
|
|
315
|
+
"Connect a provider in Settings > AI" message.
|
|
316
|
+
|
|
317
|
+
## 0.44.0
|
|
318
|
+
|
|
319
|
+
### Minor Changes
|
|
320
|
+
|
|
321
|
+
- fcede90: Settings > AI: per-feature model routing. Each AI feature can now run on a
|
|
322
|
+
specific model (e.g. a cheap model for alt-text, a frontier model for content
|
|
323
|
+
writing) or inherit the default. Adds `resolveFeatureModel` /
|
|
324
|
+
`resolveAllFeatureModels` to cms-core as the authoritative feature→model
|
|
325
|
+
resolver (capability-aware, with capability-typed and fallback defaults), and
|
|
326
|
+
extends `PUT /ai/settings` to persist + validate per-feature `modelId`/
|
|
327
|
+
`providerId` overrides. The admin AI Features card gains a per-feature model
|
|
328
|
+
picker that filters to capability-compatible models and warns when a routed
|
|
329
|
+
model is missing a required capability (e.g. a text-only model on a vision
|
|
330
|
+
feature).
|
|
331
|
+
|
|
332
|
+
## 0.43.0
|
|
333
|
+
|
|
334
|
+
### Minor Changes
|
|
335
|
+
|
|
336
|
+
- 89eebe0: Add Settings > AI (Phases 1-2): DB-backed provider connections + dynamic model catalog.
|
|
337
|
+
|
|
338
|
+
**cms-core**
|
|
339
|
+
- New `src/ai` module: provider connections (OpenAI, Anthropic, xAI, custom OpenAI-compatible), a normalized model catalog, feature flags, and budget settings, all stored in an `__ai_config__` config document (no Prisma migration, mirroring the SEO config-store).
|
|
340
|
+
- Provider API keys are encrypted at rest with `encryptSecret` and never returned to the client — responses expose only a masked `apiKeyLast4` and a source flag. When a provider's env var (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `XAI_API_KEY`) is set, that key wins and is surfaced as env-managed/read-only.
|
|
341
|
+
- SSRF-safe adapters: built-in providers use fixed hostnames; custom base URLs are validated and DNS-resolved via the existing webhook guards.
|
|
342
|
+
- Dynamic catalog merges live `/v1/models` listings with a curated capability/pricing map, with a curated fallback when live listing is unavailable and `unavailable` marking for configured-but-missing models.
|
|
343
|
+
- Monthly usage summary aggregated from existing audit-log token totals (real counts; 0 until per-run metering lands).
|
|
344
|
+
- New ADMIN-gated, audited routes: `GET/PUT /ai/settings`, `POST/PUT/DELETE /ai/providers(/:id)`, `POST /ai/providers/:id/test`, `POST /ai/providers/:id/sync-models`, `GET /ai/models`, `GET /ai/usage/summary`.
|
|
345
|
+
|
|
346
|
+
**cms-admin**
|
|
347
|
+
- Replaced the read-only AI status panel with a full `AISettingsTab`: AI Assistant card (default provider + model selector with capability badges and Refresh), provider connection management (add/test/disconnect, masked or env-managed key handling with confirm dialogs), global AI feature toggles, and a real monthly usage meter with a budget limit.
|
|
348
|
+
- Feature enforcement across modules and per-run usage metering are deferred to later phases; toggles and budget persist now.
|
|
349
|
+
|
|
350
|
+
## 0.42.0
|
|
351
|
+
|
|
352
|
+
### Minor Changes
|
|
353
|
+
|
|
354
|
+
- 8dfeb94: Add a **Settings > SEO** tab for site-wide SEO defaults, backed by the shared SEO configuration service (no duplicate state with `/seo` or Settings > General).
|
|
355
|
+
- **cms-core**: extend `SiteSEOConfig` with `titleTemplate`, `defaultMetaDescription`, `defaultSocialImageAssetId`, and a `verification` block; add title-template token validation/application, search-engine verification meta-tag rendering, and wire all three into the public page-meta renderer. New `notifySitemapOnPublish` service performs production-gated, SSRF-safe sitemap regeneration + Google/Bing pings, fired as a best-effort effect when public content is published, updated, unpublished, or deleted. Cache invalidation runs on every change while the timestamp write + outbound ping are debounced (default 60s) so bursts of publishes don't hammer the shared `__seo_config__` row or spam search engines. `PUT /seo/config` and `PUT /seo/crawl-settings` now validate new fields and write field-level audit logs (verification token values redacted).
|
|
356
|
+
- **cms-admin**: new `SeoSettingsTab` (Meta Defaults with SERP/social preview, Search Engine Verification, Sitemap Defaults, full-workspace callout, Advanced disclosure) driven by a `useSeoSettings` hook that reads/writes the shared `/seo/config` and `/seo/crawl-settings` endpoints with dirty tracking, validation, permission-aware read-only states, and dangerous-change confirmation. `MediaPickerModal` gains an `onSelectItem` callback so the default social image stores its asset id.
|
|
357
|
+
|
|
358
|
+
## 0.41.0
|
|
359
|
+
|
|
360
|
+
### Minor Changes
|
|
361
|
+
|
|
362
|
+
- 7336b5f: Add content-ownership transfer to Settings > Users.
|
|
363
|
+
|
|
364
|
+
cms-core: new `transferContentOwnership` / `countOwnedContent` helpers reassign
|
|
365
|
+
a member's authored documents, uploaded media, and created templates to another
|
|
366
|
+
active user (immutable version/audit history is never touched). New admin-only,
|
|
367
|
+
audited `POST /users/:id/transfer-content` endpoint validates the recipient
|
|
368
|
+
(must exist and be active) and applies the same Owner/last-Owner guards as the
|
|
369
|
+
other per-user actions. `contentOwnedCount` in the user detail now reflects all
|
|
370
|
+
three content types, not just documents.
|
|
371
|
+
|
|
372
|
+
cms-admin: the user detail drawer Overview now offers "Transfer content" with an
|
|
373
|
+
active-member picker before revoking access. Auxiliary dialogs (transfer,
|
|
374
|
+
confirm, reauth) now render inside the Drawer's Radix focus scope so they are
|
|
375
|
+
keyboard-reachable and no longer marked `aria-hidden` behind the drawer.
|
|
376
|
+
|
|
377
|
+
## 0.40.0
|
|
378
|
+
|
|
379
|
+
### Minor Changes
|
|
380
|
+
|
|
381
|
+
- 951963a: Build the Settings > Users access-control system: real invites, roles, seats, and access review.
|
|
382
|
+
|
|
383
|
+
**@actuate-media/cms-core**
|
|
384
|
+
- Schema: add `OWNER` and `VIEWER` to the `UserRole` enum, a new `InviteStatus` enum, a new `Invite` model (`actuate_invites`), and additive user-lifecycle columns (`suspendedAt`, `lockedAt`, `forcePasswordReset`, `mfaRequired`, `failedLoginCount`, `lastActiveAt`, `invitedById`, `acceptedInviteAt`). Two hand-authored migrations (`0011_user_roles_owner_viewer`, `0012_invites_and_user_lifecycle`) — enum values are added in their own migration so Postgres can use them as defaults afterward. All user columns are nullable/defaulted so existing rows migrate cleanly.
|
|
385
|
+
- Add a `roles` module (also exported as the `@actuate-media/cms-core/roles` subpath): display mapping (`OWNER/ADMIN/EDITOR/VIEWER` plus legacy `AUTHOR→Editor`, `CLIENT→Viewer`), assignable roles, privilege ranking, and per-role permission derivation.
|
|
386
|
+
- Add an `Invite` lifecycle service (`createInvite`, `listInvites`, `resendInvite`, `cancelInvite`, `updateInviteRole`, `extendInvite`, `expireStaleInvites`, `markInviteAcceptedForEmail`). Invite tokens are hashed at rest, single-use, and expire (7/14/30 days). Completing the password-reset flow for an invited email now marks the invite `ACCEPTED` atomically.
|
|
387
|
+
- Add a `team` module (`listTeamMembers`, `getUserStats`, `computeAccessReview`) that derives seat usage and access-review recommendations (inactive 90d+, external domain, admin/owner without MFA, stale invite) from real user/security signals.
|
|
388
|
+
- New admin endpoints: enriched `GET /users` (search/role/status filters), `GET /users/stats`, `PATCH /users/:id/role`, hardened `DELETE /users/:id` (revoke = suspend, last-Owner protection), `GET /invites` + `POST /invites/:id/resend|cancel` + `PATCH /invites/:id`, and `GET /access-review` + `POST /access-review/:id/dismiss`. Role assignment/removal of Owner is Owner-only, self-escalation is blocked, role downgrades revoke sessions, and seat limits are enforced. All sensitive actions are audit-logged.
|
|
389
|
+
|
|
390
|
+
**@actuate-media/cms-admin**
|
|
391
|
+
- Rebuild the Settings > Users tab to match the design: dynamic summary cards (members, pending invites, admins, seats used), an actionable access-review banner, a combined team table (active members + pending invites) with role-filter pills and search, per-row role dropdowns and revoke/resend/cancel actions, and an invite modal with role radio cards and advanced options (custom message, expiry). Includes loading, empty, filtered-empty, and error states; destructive and Owner-assignment actions require confirmation.
|
|
392
|
+
|
|
393
|
+
**create-actuate-cms**
|
|
394
|
+
- Sync the scaffold's Prisma schema and migrations with the new Users-management schema so newly created projects start with the `Invite` model, new roles, and user-lifecycle columns.
|
|
395
|
+
|
|
396
|
+
- 951963a: Settings > Users phase 2: per-user detail, sessions, activity, and security actions.
|
|
397
|
+
|
|
398
|
+
**@actuate-media/cms-core**
|
|
399
|
+
- Add a `user-admin` read-model module (`getUserDetail`, `listUserSessions`, `listUserActivity`, `getUserPermissionView`, `parseUserAgent`) that shapes a single user's overview, security posture (MFA / password / lock / SSO-only), sessions (with device parsing + "this device" marker), audit activity (events where the user is actor or target), and derived permissions.
|
|
400
|
+
- New admin-only, audited per-user endpoints: `GET /users/:id`, `GET /users/:id/sessions`, `DELETE /users/:id/sessions/:sessionId`, `POST /users/:id/sessions/revoke-all`, `GET /users/:id/activity`, `GET /users/:id/permissions`, `POST /users/:id/password-reset`, `POST /users/:id/force-password-reset`, `POST /users/:id/mfa-reset`, `POST /users/:id/require-mfa`, `POST /users/:id/suspend`, `POST /users/:id/reactivate`, `POST /users/:id/lock`, `POST /users/:id/unlock`. Owner actions are Owner-only, self-actions and last-Owner removal are blocked, MFA reset and Admin/Owner password resets require password reauthentication, and session-affecting actions revoke sessions.
|
|
401
|
+
- Login now rejects suspended (`suspendedAt`) and locked (`lockedAt`) accounts so an admin Suspend/Lock takes effect on the next sign-in attempt — distinct from full deactivation (`isActive=false`), which preserves the account's content and history.
|
|
402
|
+
|
|
403
|
+
**@actuate-media/cms-admin**
|
|
404
|
+
- Add the User detail drawer opened from a Team members row (and the row "View details" action): Overview, Security, Sessions, Activity, and Permissions tabs. Security actions (send/force password reset, reset/require MFA, lock/unlock, suspend/reactivate, revoke one/all sessions) are wired to the new endpoints with confirmation dialogs and an inline re-authentication prompt that retries with the `x-reauth-password` header when the server requires it. Includes loading, empty, and error states across all tabs.
|
|
405
|
+
|
|
406
|
+
## 0.39.0
|
|
407
|
+
|
|
408
|
+
### Minor Changes
|
|
409
|
+
|
|
410
|
+
- f043e18: Fix day-one auth dead-ends and wire up transactional email + user invites.
|
|
411
|
+
|
|
412
|
+
**@actuate-media/cms-core**
|
|
413
|
+
- Add a transactional email sender registry (`setEmailSender`, `getRegisteredEmailSender`, `resolveEmailSender`). Password-reset and form-notification email now resolve a sender via this registry, so apps that configure email purely through `emailPlugin()` actually send (previously only `config.platform.email` was consulted, so reset/notification mail silently never sent).
|
|
414
|
+
- Password reset links are now built from the configured admin path instead of a hardcoded `/admin`, so resets work on apps mounted elsewhere.
|
|
415
|
+
- Add an admin-only `POST /users/invite` endpoint plus `createUserInvite` / `normalizeInviteRole` / `VALID_INVITE_ROLES`. Invites provision the user with an unusable password and a single-use, 7-day token, then email a "set your password" link (reusing the existing `/reset-password` page). When no email sender is configured the response returns the link so an admin can share it manually. Rate-limited per admin.
|
|
416
|
+
|
|
417
|
+
**@actuate-media/cms-admin**
|
|
418
|
+
- `AdminRoot` gains an `onLogout` prop. The account-menu "Logout" item now revokes the server session (`POST /auth/logout`), clears local session state, and returns to the login screen — previously it navigated to a non-existent `/logout` route (404) and left the session valid.
|
|
419
|
+
- The "Invite User" dialog is now a real flow backed by `POST /users/invite` (loading state, error handling, valid roles, optional name) instead of a fake success toast.
|
|
420
|
+
|
|
421
|
+
**create-actuate-cms**
|
|
422
|
+
- The scaffolded admin page now wires `onVerifyTotp` (so MFA users can complete the second-factor step) and `onLogout`, matching the dev app.
|
|
423
|
+
|
|
424
|
+
**@actuate-media/plugin-email**
|
|
425
|
+
- `emailPlugin()` now registers its resolved provider with cms-core's email sender registry, so configuring email via the plugin is sufficient for password-reset, invite, and form-notification email.
|
|
426
|
+
|
|
427
|
+
- 05537a8: Replace the decorative AI settings tab with an honest, read-only status panel.
|
|
428
|
+
|
|
429
|
+
The previous AI tab shipped a fake API-key field (the key was never saved), a no-op "Verify" button, a misleading "encrypted at rest" claim, hardcoded zero usage metrics, and nine feature toggles that were read by nothing. None of it configured AI — that happens server-side via `@actuate-media/plugin-ai` and environment variables.
|
|
430
|
+
- `cms-core`: new admin-only `GET /ai/status` endpoint reporting `{ installed, configured, provider }`. It detects whether the AI plugin is installed and a provider is configured, and hints at which provider env var is present — never exposing key values.
|
|
431
|
+
- `cms-admin`: the AI tab now renders a truthful status panel (active / installed-but-unconfigured / not-installed) plus setup guidance, and no longer pretends to store credentials.
|
|
432
|
+
|
|
433
|
+
- 456e5f2: Fix client onboarding so a freshly scaffolded project actually has a working database.
|
|
434
|
+
|
|
435
|
+
**create-actuate-cms**
|
|
436
|
+
- Scaffolded projects now ship the canonical Prisma schema (all `actuate_*` models with `@@map`) and the full migration history, synced from `@actuate-media/cms-core` via `scripts/sync-prisma-assets.ts` and guarded by a drift test. Previously the scaffolder hand-maintained a divergent schema (14 of 33 models, no `@@map`) and shipped no migrations, so `prisma migrate deploy` in the build created no tables and core features (full-text search, i18n, API metrics, API keys, webhooks, notifications, etc.) were broken.
|
|
437
|
+
- The generated `prisma.config.ts` now warns when migrations would run against the pooled `DATABASE_URL` because `DIRECT_DATABASE_URL` is unset.
|
|
438
|
+
- The generated admin page now shows an actionable "run migrations" / backend-unreachable message instead of silently dropping to the login screen when the database has no tables or `/setup/status` fails.
|
|
439
|
+
- Next-steps + deployment docs now use `prisma migrate deploy` (applies the bundled migrations).
|
|
440
|
+
|
|
441
|
+
**@actuate-media/cms-core**
|
|
442
|
+
- `checkSetupRequired` now reports `migrationsRequired: true` (new `SetupStatus` field) when the database is reachable but the tables don't exist yet, and `POST /setup/create-admin` returns an actionable 503 in that case. New exported helper `isSchemaMissingError`.
|
|
443
|
+
- The admin middleware now returns an actionable HTML page (with remediation steps) when `CMS_SECRET` is missing, instead of a bare `500`.
|
|
444
|
+
|
|
445
|
+
- 90e80b6: Fix `plugin-email` test timeouts in CI by importing the email-sender registry from a lightweight subpath.
|
|
446
|
+
|
|
447
|
+
`emailPlugin()` previously imported `setEmailSender` from the full `@actuate-media/cms-core` barrel (handlers, middleware, the whole graph). Because the plugin's tests re-import the module on every `vi.resetModules()`, transforming that barrel repeatedly blew past the 5s test timeout on cold CI runners.
|
|
448
|
+
- `cms-core`: adds a new `@actuate-media/cms-core/email-sender` subpath export exposing `setEmailSender` / `getRegisteredEmailSender` / `resolveEmailSender` (the registry only depends on the config runtime).
|
|
449
|
+
- `plugin-email`: imports `setEmailSender` from that subpath instead of the barrel. No runtime barrel import remains.
|
|
450
|
+
|
|
451
|
+
## 0.38.0
|
|
452
|
+
|
|
453
|
+
### Minor Changes
|
|
454
|
+
|
|
455
|
+
- 008544f: Complete the MFA loop: account enrollment, login challenge, and backup-code recovery.
|
|
456
|
+
|
|
457
|
+
Previously the TOTP backend existed but had no user-facing plumbing — there was no
|
|
458
|
+
way to enroll from the admin, the login screen couldn't complete a second-factor
|
|
459
|
+
challenge, and backup codes were generated but never consumed anywhere (a lost
|
|
460
|
+
authenticator meant permanent lockout). The "Require MFA" policy was therefore
|
|
461
|
+
impossible to satisfy.
|
|
462
|
+
|
|
463
|
+
**cms-core**
|
|
464
|
+
- `POST /auth/totp/login` now accepts a one-time backup code as well as a TOTP code.
|
|
465
|
+
Matches are constant-time and single-use (the code is removed and remaining codes
|
|
466
|
+
re-encrypted); `login_success` audit records whether a backup code was used.
|
|
467
|
+
- New pure helpers `normalizeBackupCode` / `findBackupCodeMatch` in `auth/totp`.
|
|
468
|
+
- `GET /auth/me` now returns `totpEnabled` so the admin can show enrollment state.
|
|
469
|
+
|
|
470
|
+
**cms-admin**
|
|
471
|
+
- New `/profile` view (fixes the previously dead account-menu link) with an
|
|
472
|
+
**Account security** card: password reauth → QR code + manual key + downloadable
|
|
473
|
+
one-time backup codes → verify, plus a reauth-gated disable flow.
|
|
474
|
+
- The login screen now renders a **two-factor challenge** step (authenticator code
|
|
475
|
+
with a backup-code fallback). `AdminRoot`/`Login` gain an `onVerifyTotp` prop and
|
|
476
|
+
`onLogin` may now return `{ requiresTOTP, mfaPendingToken }`.
|
|
477
|
+
- The Security tab's "MFA not set up" warning now links to the enrollment flow.
|
|
478
|
+
- Adds `qrcode.react` for QR rendering.
|
|
479
|
+
|
|
480
|
+
## 0.37.0
|
|
481
|
+
|
|
482
|
+
### Minor Changes
|
|
483
|
+
|
|
484
|
+
- 06940a1: Add the Settings → Security tab as a real Security Center.
|
|
485
|
+
|
|
486
|
+
**cms-core**
|
|
487
|
+
- New `@actuate-media/cms-core/security-center` subpath export with pure, client-safe
|
|
488
|
+
helpers: `computeSecurityStatus`, `parseSecuritySettings`, `validateSecuritySettings`,
|
|
489
|
+
`validateIpRange`, `mfaEnableLockoutError`, `summarizeUsers`, `summarizeApiKeys`, and types.
|
|
490
|
+
- New API routes: `GET /security` (computed posture + cross-module summaries +
|
|
491
|
+
secret/encryption verdicts, never secret values), `PUT /security` (persists enforced
|
|
492
|
+
MFA + session policy with server-side lockout-safe MFA enabling, audited as
|
|
493
|
+
`security_settings_changed`), and `POST /security/sessions/revoke-all` (revokes the
|
|
494
|
+
caller's other sessions, audited).
|
|
495
|
+
- Concurrent-session enforcement now honors the stored `security.session.maxConcurrentSessions`
|
|
496
|
+
policy (config/default precedence preserved when unset; `0` = unlimited).
|
|
497
|
+
- Login responses include `mfaSetupRequired` when org-wide MFA is required but the account
|
|
498
|
+
has not enrolled past its grace window.
|
|
499
|
+
- `settings` global writes now audit nested `security` changes.
|
|
500
|
+
|
|
501
|
+
**cms-admin**
|
|
502
|
+
- New Security tab with a computed Security Status card, Authentication & MFA, Session
|
|
503
|
+
Policy (with revoke-other-sessions), Access & Request Protection, and Secrets/Encryption/Audit
|
|
504
|
+
cards. Wired to the real `/security` endpoints with source badges, dangerous-change
|
|
505
|
+
confirmations, lockout-safe MFA toggle, validation, and read-only states for non-admins.
|
|
506
|
+
Replaces the previous decorative security toggles.
|
|
507
|
+
|
|
508
|
+
## 0.36.0
|
|
509
|
+
|
|
510
|
+
### Minor Changes
|
|
511
|
+
|
|
512
|
+
- 3667ac2: Add the Settings → Appearance tab: a production-ready brand + admin-theme system.
|
|
513
|
+
|
|
514
|
+
**cms-core**
|
|
515
|
+
- New `@actuate-media/cms-core/appearance` subpath module with pure utilities:
|
|
516
|
+
logo-shape classification, WCAG contrast checks, admin accent-token
|
|
517
|
+
generation, and brand/favicon resolution (admin + public fallback chains).
|
|
518
|
+
- `GET /public/brand` (unauthenticated) resolves public logos + favicon
|
|
519
|
+
metadata for the public frontend; `GET /appearance` (authenticated) returns
|
|
520
|
+
stored appearance/brand settings plus resolved admin/public/favicon views and
|
|
521
|
+
per-asset URLs for editor previews.
|
|
522
|
+
- `PUT /globals/:slug` audit logging now records nested `appearance`/`brand`
|
|
523
|
+
changes.
|
|
524
|
+
|
|
525
|
+
**cms-admin**
|
|
526
|
+
- New Appearance tab (Branding, Admin Theme, Typography & Motion, live Brand
|
|
527
|
+
Preview) wired to the Media Center for logo/favicon uploads (assets referenced
|
|
528
|
+
by ID, never raw paths), with shape warnings, dark-logo warnings, accent
|
|
529
|
+
contrast validation, source badges, and read-only/permission states.
|
|
530
|
+
- `ThemeProvider` now applies color mode, accent color, sidebar density, content
|
|
531
|
+
font, and reduce-motion live, and feeds the resolved admin brand to the
|
|
532
|
+
sidebar logo (with site-initials fallback).
|
|
533
|
+
|
|
534
|
+
## 0.35.1
|
|
535
|
+
|
|
536
|
+
### Patch Changes
|
|
537
|
+
|
|
538
|
+
- b3f51f2: Fix "Global not found" error on the Settings page (and any global editor) before the first save. The authenticated `GET /globals/:slug` route now returns an empty object for a global that has no document yet — globals are upsert-on-write, so a never-saved slug is a valid empty state rather than a 404. The public `/public/globals/:slug` route keeps its config-gated 404 for genuinely undeclared globals.
|
|
539
|
+
|
|
540
|
+
## 0.35.0
|
|
541
|
+
|
|
542
|
+
### Minor Changes
|
|
543
|
+
|
|
544
|
+
- 58eff9b: Build a production-ready General Settings tab with a computed Site Index Status card.
|
|
545
|
+
|
|
546
|
+
**cms-core**
|
|
547
|
+
- Add `getSiteIndexStatus()` / `computeSiteIndexStatus()` (exported from the SEO module) that derives the effective crawl/index state (indexable / protected / blocked / partially_blocked / unknown) from the SAME effective SEO config the public renderer uses — environment, site URL, robots, sitemap, and last audit — plus a `robotsTxtBlocksEntireSite()` helper.
|
|
548
|
+
- Add `GET /seo/index-status` (ADMIN) returning the computed status, per-check rows, and actions.
|
|
549
|
+
- Audit `PUT /globals/settings` with a per-key before/after diff (`settings_changed`, including source + environment).
|
|
550
|
+
- Migrate the `GET /seo/meta/:documentId` preview to read robots defaults from the effective SEO config instead of the legacy `settings` global, so preview, public render, and General Settings share one source of truth.
|
|
551
|
+
|
|
552
|
+
**cms-admin**
|
|
553
|
+
- Replace the General tab with a fully wired implementation: Site Index Status card, Site Information (with config-managed source badges + live Site URL validation), Language & Region, and SEO & Robots Defaults — the SEO toggles now read/write the shared `/seo/config` (no duplicate store).
|
|
554
|
+
- Add dirty-state Save/Reset bar, validation summary, confirmation dialogs for dangerous indexing changes (production noindex on; non-production noindex off), loading skeletons, error/retry, and read-only gating for non-admins.
|
|
555
|
+
- Use semantic theme tokens (purple `brand` accent, status colors) so the tab matches the design system in light and dark modes.
|
|
556
|
+
|
|
557
|
+
## 0.34.0
|
|
558
|
+
|
|
559
|
+
### Minor Changes
|
|
560
|
+
|
|
561
|
+
- a6d6674: Forms PII/encryption, retention, audit, export, and MCP tools (PR8).
|
|
562
|
+
|
|
563
|
+
**cms-core**
|
|
564
|
+
- **Field-level encryption at rest**: submission values for fields flagged `encrypted` (or high-PII fields when `forms.encryptSensitiveFields` is enabled) are now encrypted with the `enc:v1:` AES-256-GCM envelope before persistence and transparently decrypted on every read path. Plaintext/legacy rows pass through unchanged; missing key on read fails closed.
|
|
565
|
+
- **PII retention**: `forms.retentionDays` is now wired into the cleanup cron, hard-deleting form submissions (and cascading their files) older than the window. Opt-in — disabled by default.
|
|
566
|
+
- **Audit logging**: viewing a single entry now logs `form.entry.viewed` and CSV export logs `form.entries.exported`, both with user id and IP, for PII-access traceability.
|
|
567
|
+
- **CSV export endpoint**: `GET /forms/entries/export?formId=…` streams a decrypted CSV of a form's submissions (bounded to 5000 rows) with formula-injection-safe cells. New exported helpers `exportFormEntries`, `encryptSubmissionData`, `decryptSubmissionData`, `fieldShouldEncrypt`.
|
|
568
|
+
|
|
569
|
+
**mcp-server**
|
|
570
|
+
- New `list_form_entries` and `get_form_entry` tools over the decrypted submissions inbox, both requiring an explicit `formId`/`entryId` (no enumeration) and documented as PII-sensitive. `get_form_entry` reads are audited server-side.
|
|
571
|
+
|
|
572
|
+
## 0.33.0
|
|
573
|
+
|
|
574
|
+
### Minor Changes
|
|
575
|
+
|
|
576
|
+
- 9f4192b: Add the Forms notifications + webhooks pipeline (PR6).
|
|
577
|
+
- **Email notifications**: on a clean submission, send admin alerts to the form's configured recipients and an optional submitter autoresponder through the configured `platform.email` adapter. Supports `{{placeholder}}` subject/message templates (HTML-escaped), a fields table that can exclude sensitive/PII fields, and a resolved `Reply-To` from a chosen field.
|
|
578
|
+
- **Per-form webhooks**: new `createFormWebhook` / `listFormWebhooks` / `updateFormWebhook` / `deleteFormWebhook` / `testFormWebhook` / `dispatchFormWebhooks` services. Secrets are stored encrypted and returned only once at creation; payloads are HMAC-SHA256 signed; delivery is SSRF-safe (`safeFetch`) with bounded retries + exponential backoff. Events: `form.submitted`, `form.spam_detected`, `form.entry.starred`, `form.entry.archived`.
|
|
579
|
+
- **API**: `GET/POST /forms/:id/webhooks`, `PATCH/DELETE /forms/:id/webhooks/:webhookId`, `POST /forms/:id/webhooks/:webhookId/test`, and `POST /forms/:id/notifications/test`.
|
|
580
|
+
- Side-effects fire non-blocking from the submission pipeline so a slow or failing channel never delays or breaks the public response. Spam submissions suppress email but still fire `form.spam_detected` webhooks.
|
|
581
|
+
- `EmailAdapter` send options gain optional `replyTo` / `from` (additive, non-breaking).
|
|
582
|
+
|
|
583
|
+
## 0.32.0
|
|
584
|
+
|
|
585
|
+
### Minor Changes
|
|
586
|
+
|
|
587
|
+
- 5a44d79: Add the public form submission pipeline (Forms PR5).
|
|
588
|
+
- New public, unauthenticated endpoints: `GET /public/forms/:slug` (embeddable
|
|
589
|
+
schema projection with server-only fields stripped) and
|
|
590
|
+
`POST /public/forms/:slug/submit`, plus CORS preflight (`OPTIONS`) support.
|
|
591
|
+
- Server-authoritative submission processing: per-field validation +
|
|
592
|
+
type-coercion against the active schema version, HTML sanitization of all
|
|
593
|
+
string values (stored-XSS defense), and dropping of keys not in the schema.
|
|
594
|
+
- Spam protection strategies: honeypot, time-to-submit, email/domain blocklist,
|
|
595
|
+
keyword blocklist, and origin domain allowlist. Spam is persisted but hidden
|
|
596
|
+
from the default inbox.
|
|
597
|
+
- File-upload validation (MIME allowlist + magic-byte sniffing + size cap) wired
|
|
598
|
+
to the configured storage adapter; metadata persisted to `FormSubmissionFile`.
|
|
599
|
+
- Sequential per-form entry numbers assigned in a transaction; client IPs are
|
|
600
|
+
hashed (never stored raw); attribution/UTM captured; submissions rate-limited
|
|
601
|
+
per IP+form and CAPTCHA-verified when configured.
|
|
602
|
+
- The legacy `POST /forms/:id/submit` route now runs through the same pipeline.
|
|
603
|
+
|
|
604
|
+
## 0.31.0
|
|
605
|
+
|
|
606
|
+
### Minor Changes
|
|
607
|
+
|
|
608
|
+
- ca3c1f6: Forms system — data model foundation (PR 1 of 9).
|
|
609
|
+
|
|
610
|
+
Adds the persistence layer and shared contracts for the full Forms workflow:
|
|
611
|
+
- New Prisma models: `FormSchemaVersion` (versioned field-schema snapshots so historical submissions always render against the schema they were captured with), `FormSubmissionFile` (file-upload metadata stored via the storage adapter, cascade-deleted with its submission), and `FormWebhook` (per-form signed outbound webhooks).
|
|
612
|
+
- Expanded `FormSubmission` with the submission-workflow columns: `entryNumber`, `schemaVersionId`, `normalizedData`, `senderName/Email/Phone`, `preview`, `unread`/`starred`/`archived`, `spamStatus`, attribution metadata (`ipAddressHash`, `userAgent`, `referrer`, `pageUrl`, `utm*`), `readAt`, `archivedAt`, and `updatedAt`. The migration is safe on populated databases (all additive/defaulted, `updatedAt` backfilled, existing rows marked read).
|
|
613
|
+
- New shared TypeScript contracts under `@actuate-media/cms-core` (`FormDefinition`, `FormField`, `FormFieldType`, `FormSchemaVersion`, `FormSubmission`, `FormSubmissionFile`, `FormNotificationSettings`, `FormSpamSettings`, `FormEmbedSettings`, `FormWebhookConfig`, query-param and stats types).
|
|
614
|
+
- New `forms` site-config section (`FormsConfig`): feature flag, allowed field types, spam defaults, file-upload limits, notification/webhook/autoresponder toggles, retention, PII encryption, and the public submission path.
|
|
615
|
+
|
|
616
|
+
No runtime behavior changes yet; subsequent PRs wire services, API routes, admin UI, public submission, notifications, and seed data on top of this model.
|
|
617
|
+
|
|
618
|
+
- 15d8c08: Forms system — core services + admin API routes (PR 2 of 9).
|
|
619
|
+
|
|
620
|
+
Adds the service layer and REST endpoints that back the Forms admin UI:
|
|
621
|
+
- **Forms service** (`forms/service.ts`): `listForms`, `getFormById`, `createForm`, `updateForm`, `duplicateForm`, `archiveForm`/`restoreForm`, `deleteForm` (blocks deletion when submissions exist unless forced), schema versioning (`saveFormSchema`/`getFormSchema` creates a new `FormSchemaVersion` on every save), per-form notification/embed settings, `getFormStats`, and `getFormsSidebarCounts`. Form definitions are stored via the standard document data-layer (inheriting versioning, cache invalidation, and audit logging). Schema validation enforces unique/valid keys, allowed field types, option presence, and the configured field cap.
|
|
622
|
+
- **Entries service** (`forms/entries.ts`): `listEntries` with search + form/unread/starred/archived/spam/date filters (archived and spam excluded by default), `getEntryWithSchema` (renders an entry against the schema version it was captured with), `getEntryCounts` (chip totals + per-form unread map), read/unread/star/archive mutations, `bulkUpdateEntries`, and `deleteEntry`. A normalization layer derives sender/preview from the payload for legacy rows.
|
|
623
|
+
- **API routes**: `GET/POST /forms`, `GET/PATCH/DELETE /forms/:id`, `POST /forms/:id/duplicate`, `GET/PUT /forms/:id/schema`, `GET/PUT /forms/:id/notifications`, `PUT /forms/:id/notify`, `GET/PUT /forms/:id/embed`, `GET /forms/stats`, `GET /forms/sidebar-counts`, `GET /forms/entries`, `GET /forms/entries/counts`, `GET/PATCH/DELETE /forms/entries/:entryId`, and `POST /forms/entries/bulk`. All require an authenticated write role and emit audit-log events.
|
|
624
|
+
|
|
625
|
+
Behavior change: `GET /forms` now returns the typed `FormDefinition` shape (including entry counters). `fields` is now the field array; the legacy `fields`-as-count value is available as `fieldCount`, and `submissions` mirrors `totalEntries` so existing admin views keep working until they migrate (PR 3).
|
|
626
|
+
|
|
627
|
+
Also fixes a pre-existing sitemap test that omitted the `type: 'page'` collection flag the SEO scoping now requires.
|
|
628
|
+
|
|
629
|
+
- 8891547: Scope the SEO surfaces to real front-end content and keep the admin out of search indexes.
|
|
630
|
+
- **cms-core**: The Content SEO list, audit, scan, internal-link candidates, **and the XML sitemap** now only include documents from collections explicitly typed `page` or `post` (visible and sitemap-eligible). Structural/utility collections (navigation menus, footers, tags, templates) — which have no `type` — and ad-hoc rows whose collection isn't declared in the config (e.g. benchmark/test data) are excluded, so a menu can no longer leak into the sitemap as an indexable URL. Adds the `frontEndContentCollectionTypes(config)` helper and threads a `collectionTypes` option through `gatherAuditEntities` / `runAndPersistAudit`; `processSeoScan` gains an `allowedCollections` option. The `POST /seo/scan` route now reuses `processSeoScan` (also fixing its case-sensitive `alt=`/`<h1` checks).
|
|
631
|
+
- **cms-admin**: `AdminRoot` now ensures a `noindex, nofollow` robots meta tag while the admin is mounted, so private admin UI is never indexed regardless of how the consumer renders the admin page.
|
|
632
|
+
|
|
633
|
+
## 0.30.0
|
|
634
|
+
|
|
635
|
+
### Minor Changes
|
|
636
|
+
|
|
637
|
+
- 2cd5d3a: SEO production-readiness (Important tier, batch 1 — backend security/access):
|
|
638
|
+
- **Read-endpoint access control.** `/seo/analysis`, `/seo/readability`, `/seo/internal-links`, `/seo/schema` and `/seo/meta` now enforce the target collection's `access.read` rule (and API-key collection scope), so a principal scoped to one collection can no longer pull SEO intelligence for documents in another. Adds a new exported `assertCollectionAccess(collection, operation, ctx, doc?)` helper.
|
|
639
|
+
- **AI audit logging.** Every successful `/ai/seo/*` generation (`generate-field`, `explain-issue`, `generate-schema`, `summarize`) now writes an `ai_seo_generate` audit-log event — these calls are billable and feed public meta/schema, so they need a forensic trail.
|
|
640
|
+
- **Prompt-injection + token-cost hardening.** Untrusted document/issue content interpolated into AI prompts is now length-clamped (2k chars) and fenced with explicit "treat as data, never instructions" delimiters across all SEO AI endpoints, including the title/description branches that previously sent unbounded content.
|
|
641
|
+
|
|
642
|
+
- 8d45b42: SEO production-readiness (Important tier, batch 2 — backend reliability/correctness):
|
|
643
|
+
- **Severity ordering.** Issue lists (`/seo/issues`, `/seo/overview`, AI summary) now rank by explicit risk (critical → warning → info) instead of an alphabetical `severity` sort that wrongly ranked `info` above `warning`.
|
|
644
|
+
- **Sitemap pagination.** Per-collection sitemaps no longer cap at 5,000 URLs and silently drop the rest. Collections larger than the 50k protocol limit are split across `?page=N` files referenced from the sitemap index (45k URLs/file). Sitemap-index `<loc>` values are now XML-escaped.
|
|
645
|
+
- **Consistent robots resolution.** `composePageMeta` now resolves robots through the shared `resolveRobotsDirectives`, honoring the `robotsPolicy` select field and a new `seo.robots.noIndexNonProduction` guard (plus `defaultNoIndex`/`defaultNoFollow`) so staging/preview deploys aren't indexed. Adds an optional `environment` input.
|
|
646
|
+
- **Audit reliability.** `runAndPersistAudit` refuses to start while another audit is RUNNING (prevents parallel runs duplicating `SeoIssue` rows), `gatherAuditEntities` is bounded with a `take` cap, and the expensive `/seo/scan` endpoint is now rate-limited and bounded.
|
|
647
|
+
|
|
648
|
+
- 90f53a6: Fix SEO Operations Center production-readiness blockers.
|
|
649
|
+
- **SSRF (`GET /seo/summary`)**: the link-health probe now uses `safeFetch`
|
|
650
|
+
(blocks private/loopback IPs, no redirect following), only probes same-origin
|
|
651
|
+
internal links, skips all probing when `NEXT_PUBLIC_SITE_URL` is unset, and is
|
|
652
|
+
gated to EDITOR+ — matching `/seo/link-health`.
|
|
653
|
+
- **`GET /llms.txt` leak**: now excludes soft-deleted documents and
|
|
654
|
+
internal/system collections (`__seo_config__`, `__*`, etc.) via the shared
|
|
655
|
+
`isInternalCollection` helper, so "deleted" and internal content can no longer
|
|
656
|
+
appear on a public surface.
|
|
657
|
+
- **`PUT /seo/content` data-layer bypass**: SEO field edits now route through the
|
|
658
|
+
new `updateDocumentSeoFields` action, which enforces the collection's
|
|
659
|
+
`access.update` rule, snapshots a `Version` row, recomputes the search index,
|
|
660
|
+
and fires the `afterUpdate`/cache-invalidate effects — while preserving the
|
|
661
|
+
undeclared SEO convention keys the generic field-access layer would strip.
|
|
662
|
+
Inbound SEO fields are now type- and length-validated.
|
|
663
|
+
- **cms-admin**: SEO read fetchers in `seo-service` now re-throw on `cmsApi`
|
|
664
|
+
errors so the error/retry UI works instead of rendering a misleading empty
|
|
665
|
+
fallback (e.g. a fake score of 0 / grade "Critical") on a backend failure.
|
|
666
|
+
|
|
667
|
+
### Patch Changes
|
|
668
|
+
|
|
669
|
+
- 1bd4a49: SEO production-readiness Polish tier — post-launch nice-to-haves from the SEO review.
|
|
670
|
+
|
|
671
|
+
cms-core:
|
|
672
|
+
- **Markdown-aware heading analysis** — `analyzeContent` and `countH1` now detect
|
|
673
|
+
Markdown ATX (`#`/`##`) and Setext (`===`/`---`) headings in addition to HTML
|
|
674
|
+
tags, so markdown-bodied posts are no longer wrongly flagged "No H1 found" /
|
|
675
|
+
"no subheadings".
|
|
676
|
+
- **Correct default OG image URL** — the auto-generated `og:image` now points at
|
|
677
|
+
`/api/cms/og.png` (where the endpoint is actually mounted) instead of `/og.png`,
|
|
678
|
+
which 404'd for social crawlers.
|
|
679
|
+
- **Consistent environment resolution** — `resolveSeoEnvironment()` centralises the
|
|
680
|
+
production gate, preferring `VERCEL_ENV` over `NODE_ENV` everywhere (preview
|
|
681
|
+
deploys report `NODE_ENV=production`), so `noIndexNonProduction` reliably
|
|
682
|
+
noindexes preview deployments.
|
|
683
|
+
- **Admin route boundary** — the CMS middleware matches `/admin` and `/api/cms` on
|
|
684
|
+
a path-segment boundary, so sibling public routes like `/administrators` or
|
|
685
|
+
`/admin-news` are no longer treated as admin routes.
|
|
686
|
+
- **AI status semantics** — SEO AI endpoints now return `503` when the provider is
|
|
687
|
+
installed-but-unconfigured (vs `501` when the plugin is genuinely missing).
|
|
688
|
+
- Removed a duplicated `gradeForScoreBand` helper in favour of the canonical
|
|
689
|
+
`gradeForSeoScore`.
|
|
690
|
+
|
|
691
|
+
cms-admin:
|
|
692
|
+
- SEO filter chips use accessible toggle-button semantics (`aria-pressed`) instead
|
|
693
|
+
of misused `role="tab"`.
|
|
694
|
+
- Crawl-settings toggles use the Radix `Switch` primitive and are disabled while a
|
|
695
|
+
save is in flight.
|
|
696
|
+
- Score progress bars expose an accessible name and `aria-valuetext`; drawers
|
|
697
|
+
without a description no longer trigger Radix's missing-description warning.
|
|
698
|
+
- Redirect CSV export defers object-URL revocation (and mounts the anchor) so the
|
|
699
|
+
download isn't cancelled before it starts.
|
|
700
|
+
- The SEO AI service treats both `501` and `503` as the graceful "AI unavailable"
|
|
701
|
+
state.
|
|
702
|
+
|
|
703
|
+
## 0.29.0
|
|
704
|
+
|
|
705
|
+
### Minor Changes
|
|
706
|
+
|
|
707
|
+
- a3b702f: Media Center foundation (phase 1):
|
|
708
|
+
- New media SEO scoring module (`calculateMediaSeoScore` + types) that grades assets on alt text, filename quality, dimensions, captions, and size.
|
|
709
|
+
- Additive `Media` schema fields (`originalFileName`, `caption`, `description`, `focusKeyword`, `tags`, `publicUrl`, `storageProvider`, `durationSeconds`, `thumbnailUrl`, `responsiveVariants`, `status`, `deletedAt`) plus a `MediaStatus` enum and supporting indexes, with the `0008_media_center` migration.
|
|
710
|
+
|
|
711
|
+
All fields are nullable or defaulted, so the change is additive and safe to apply to an existing database.
|
|
712
|
+
|
|
713
|
+
- 5fa84d3: Media library now reports real usage. `GET /media` joins the `MediaUsage` table and returns a `usedOn` array (page title + admin editor path) per asset, so the "orphaned" indicator is accurate instead of flagging every file. Adds `POST /ai/media-metadata`, which generates accessible alt text (and a derived title) for an image via the AI vision provider, with a graceful 501 when the AI plugin/provider isn't configured.
|
|
714
|
+
- 5fa84d3: SEO Operations Center — a full detect → prioritize → explain → route → fix → audit → generate workflow.
|
|
715
|
+
|
|
716
|
+
**cms-core**
|
|
717
|
+
- Additive `0009_seo` migration (mirrored to `apps/dev`): `SEOAuditRun`, `SEOIssue` (+ severity/status/category enums), `Redirect` extensions (`hits`, `lastHitAt`, `enabled`, `priority`, `preserveQuery`, `source`), `Redirect404Hit`, and `RedirectSuggestion`. All columns are nullable or defaulted, so it is safe to apply to an existing database.
|
|
718
|
+
- Pure, explainable scoring model (`seo/score.ts`): `calculatePageSeoScore` / `calculateSiteSeoScore` with documented weights and breakdowns.
|
|
719
|
+
- Persistent audit engine (`seo/audit-engine.ts`) with cross-run issue lifecycle (open / resolved / ignored / recurring) and a redirect engine (chain/loop detection, safe flatten, hit tracking, 404 logging, gated AI 404 recovery).
|
|
720
|
+
- New endpoints: `POST /seo/audit`, `GET /seo/audits[/:id]`, `GET /seo/issues[/:id]`, issue `resolve`/`ignore`/`reopen`/`fix`, `GET /seo/overview`, redirect CRUD + import/export + chains/loops/flatten/suggest, technical settings (`/seo/sitemap/*`, `/seo/crawl-settings`, `/seo/robots`), and approval-gated `/ai/seo/*` (generate-field, explain-issue, generate-schema, summarize) that degrade gracefully with 501 when `plugin-ai` is unconfigured.
|
|
721
|
+
- `/resolve` now returns Next.js-ready `metadata` plus validated JSON-LD; public `robots.txt` honors the admin-edited body.
|
|
722
|
+
|
|
723
|
+
**cms-admin**
|
|
724
|
+
- Rebuilt `SEO.tsx` into a five-tab Operations Center (Overview / Content / Technical / Redirects / Audit) with `?tab=` deep-link routing, dynamic badge counts, a typed `seo-service.ts` client, reusable token-only primitives and a focus-trapped drawer, editor/fix/copilot drawers, and full loading / empty / error / permission states.
|
|
725
|
+
|
|
726
|
+
**mcp-server**
|
|
727
|
+
- New SEO tools: `seo_overview`, `run_seo_audit`, `list_seo_audit_runs`, `list_seo_issues`, `get_seo_issue`, `set_seo_issue_status`, `list_redirect_rules`, `seo_redirect_overview`, `seo_redirect_chains`.
|
|
728
|
+
|
|
729
|
+
**create-actuate-cms**
|
|
730
|
+
- Scaffolded projects now ship a redirect-resolving `middleware.ts` backed by the database redirect table.
|
|
731
|
+
|
|
732
|
+
### Patch Changes
|
|
733
|
+
|
|
734
|
+
- 766b63c: Surface database-unavailable errors during first-run setup instead of masking them as "setup required". `checkSetupRequired()` and `createInitialAdmin()` now rethrow connection/initialization failures (Prisma `P1001`/`P1002`, `ECONNREFUSED`, `PrismaClientInitializationError`, etc.) so the `/setup/status` and `/setup/create-admin` endpoints return a 5xx for a DB outage rather than silently rendering the account-creation wizard and then failing with an opaque 400.
|
|
735
|
+
|
|
736
|
+
## 0.28.1
|
|
737
|
+
|
|
738
|
+
### Patch Changes
|
|
739
|
+
|
|
740
|
+
- fa54768: Accept SMTP.com's `SMTP_KEY` / `SMTP_CHANNEL` / `SMTP_EMAIL` env vars as
|
|
741
|
+
aliases for `SMTPCOM_API_KEY` / `SMTPCOM_CHANNEL` / `EMAIL_FROM`.
|
|
742
|
+
|
|
743
|
+
`resolveEmailConfigFromEnv()` now infers the `smtpcom` provider from either
|
|
744
|
+
naming scheme (the generic relay's `SMTP_HOST` still takes precedence when
|
|
745
|
+
present), and the `/health` env diagnostic recognises the same keys so a
|
|
746
|
+
correctly-configured SMTP.com deployment no longer warns about a missing
|
|
747
|
+
`RESEND_API_KEY`.
|
|
748
|
+
|
|
749
|
+
## 0.28.0
|
|
750
|
+
|
|
751
|
+
### Minor Changes
|
|
752
|
+
|
|
753
|
+
- bdeadb2: Resolve real author and related-posts data for post-type documents.
|
|
754
|
+
|
|
755
|
+
`GET /resolve` now returns a `post` object for post-type collections:
|
|
756
|
+
- `post.author` — `{ name, avatarUrl }` derived from the document's creator, so the Author Bio section shows the real author instead of a placeholder.
|
|
757
|
+
- `post.relatedPosts` — up to 3 recent published siblings in the same collection (`{ title, url, excerpt }`), so the Related Posts section links real content.
|
|
758
|
+
|
|
759
|
+
Both fields are additive and optional; existing consumers are unaffected.
|
|
760
|
+
|
|
761
|
+
- 1768a8a: Render the post type's template header on the public site so the live page matches the editor canvas and preview.
|
|
762
|
+
- `GET /resolve` now returns `template.header` (a normalized `PostHeaderConfig`) for post-type documents, resolved from the post type's `post-templates` doc at read time. The template stays the single source of truth, so editing it restyles every post of that type without re-saving each post.
|
|
763
|
+
- New `computeReadingTime()` export drives the header's "min read" figure from the post body (200 wpm, HTML stripped, min 1 minute) instead of the previous hardcoded "5 min read".
|
|
764
|
+
|
|
765
|
+
- d85e89e: Add a server-side aggregate `GET /posts` endpoint for the admin Posts area.
|
|
766
|
+
Filtering, sorting and pagination now run at the database level (one query plus
|
|
767
|
+
one count) and respect per-collection read scope, so the Posts list scales past
|
|
768
|
+
the old ~500-row-per-type client cap instead of pulling every document into the
|
|
769
|
+
browser. The admin `fetchPosts` service now calls this endpoint; its public
|
|
770
|
+
`Post` / `PostType` / `PostAuthor` shapes are unchanged.
|
|
771
|
+
|
|
772
|
+
### Patch Changes
|
|
773
|
+
|
|
774
|
+
- 5567daf: Post templates: seed sensible defaults and route the admin through the dedicated endpoint.
|
|
775
|
+
- A post type with no saved template now returns the default starting template (centered header + article body + related posts) instead of an empty one, so new posts and the template editor are never blank. Applied on the server (`GET /post-templates/:type`) and defended client-side.
|
|
776
|
+
- The admin post-editor service now reads/writes templates through the dedicated, audited `/post-templates/:type` endpoints (scoped + logged) instead of the raw `post-templates` collection API.
|
|
777
|
+
|
|
778
|
+
## 0.27.4
|
|
779
|
+
|
|
780
|
+
### Patch Changes
|
|
781
|
+
|
|
782
|
+
- c091125: Recognise Vercel Marketplace Upstash credentials. The Vercel Upstash (Redis/KV) integration provisions `KV_REST_API_URL` / `KV_REST_API_TOKEN`, not `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN`, so a correctly-provisioned store previously fell back to the in-memory rate limiter and the `/health` check kept warning.
|
|
783
|
+
|
|
784
|
+
The rate limiter (`createRateLimiter` / `resolveUpstashCredentials`), the platform-vercel rate-limit adapter, and the env health check now read the `KV_REST_API_*` names as a fallback (native `UPSTASH_*` names still win when both are present). No config change is needed for stores provisioned through the Vercel Marketplace.
|
|
785
|
+
|
|
786
|
+
## 0.27.3
|
|
787
|
+
|
|
788
|
+
### Patch Changes
|
|
789
|
+
|
|
790
|
+
- 6f05e8d: Make the email health check (`RESEND_API_KEY` entry in `validateEnvShape` / `/api/cms/health`) provider-aware. It now reports "Configured" when any supported email provider is set up via env (SMTP, SES, or SMTP.com), instead of always warning about a missing Resend key. Mirrors the env resolution in `@actuate-media/plugin-email`.
|
|
791
|
+
|
|
792
|
+
## 0.27.2
|
|
793
|
+
|
|
794
|
+
### Patch Changes
|
|
795
|
+
|
|
796
|
+
- 07f52af: Accept Prisma Accelerate / Prisma Postgres connection schemes (`prisma://`,
|
|
797
|
+
`prisma+postgres://`) in the `DATABASE_URL` env-shape validator. Previously the
|
|
798
|
+
`/health` and admin diagnostics endpoints flagged a valid Accelerate URL as an
|
|
799
|
+
invalid scheme, reporting the instance as unhealthy even though the runtime
|
|
800
|
+
connects successfully through the Accelerate transport.
|
|
801
|
+
|
|
802
|
+
## 0.27.1
|
|
803
|
+
|
|
804
|
+
### Patch Changes
|
|
805
|
+
|
|
806
|
+
- 4fb0097: Dashboard performance: cache the expensive SEO compute and reduce refetch churn.
|
|
807
|
+
- **cms-core:** `/stats` now caches its average-SEO-score computation per content-scope for a short TTL (60s). That score otherwise reads and scans the `data` JSON of up to 200 published documents on every dashboard load — the heaviest part of the endpoint. On a warm cache the read is skipped entirely; transient DB failures are not cached. Module-level so it survives across warm requests; a cold start recomputes once.
|
|
808
|
+
- **cms-admin:** `useApiData` gains an opt-in `cacheMs` option. When set, successful responses are cached in-memory by endpoint and reused on the next mount (cache-first), so views that remount frequently don't re-issue identical requests. `refetch()` always bypasses the cache. Default behaviour is unchanged for existing callers. The Dashboard enables a 30s cache on its four requests, so navigating away and back no longer re-fires them; first load and independent streaming are preserved.
|
|
809
|
+
|
|
810
|
+
## 0.27.0
|
|
811
|
+
|
|
812
|
+
### Minor Changes
|
|
813
|
+
|
|
814
|
+
- 91885de: Add a section-aware bridge so AI agents (e.g. in Cursor / VS Code) can build pages section-by-section through MCP, producing real, editable, publishable Page Editor sections rather than disposable markup.
|
|
815
|
+
- **cms-core**: new `@actuate-media/cms-core/page-sections` subpath exporting the flat Page Editor section registry, pure section-array helpers, and a deterministic `validateSectionContent`. This is now the single source of truth shared by the admin editor, the REST API, and MCP. Adds REST routes: `GET /page-sections/types`, `POST /page-sections/validate`, and section CRUD over a page document's `data.sections` (`GET|POST /page-sections/:collection/:id`, `PATCH|DELETE /page-sections/:collection/:id/:sectionId`, `POST /page-sections/:collection/:id/reorder`). Mutations go through the audited `updateDocument` path (versions, hooks, cache invalidation, live-preview push) and reuse collection scope checks. Writes are rejected on collections that don't declare a `sections` JSON field.
|
|
816
|
+
- **mcp-server**: nine new tools — `list_section_types`, `get_section_schema`, `validate_section_content`, `list_page_sections`, `add_page_section`, `update_page_section`, `reorder_page_sections`, `remove_page_section`, `toggle_page_section_visibility`.
|
|
817
|
+
- **cms-admin**: the Page Editor section registry and section helpers now re-export from `@actuate-media/cms-core/page-sections`; per-section publish validation is delegated to the shared `validateSectionContent`. No behavior change — internal refactor for a single source of truth.
|
|
818
|
+
|
|
819
|
+
- 91885de: Add a design-driven Posts template system that mirrors the Pages section editor.
|
|
820
|
+
- **cms-core**: tag section types with a `scope` (`page` | `post`), add post-oriented section types (`article-body`, `quote`, `author-bio`, `related-posts`, `gallery`), `PostHeaderConfig` / `PostTemplate` types, the `getSectionTypesForScope` helper, and new field types (`richText`, `boolean`, `relationship`, `gallery`). Adds `GET/PUT /post-templates/:type` API routes and a `?scope=` filter on `GET /page-sections/types`.
|
|
821
|
+
- **cms-admin**: new `PostSectionEditor`, `PostTemplateEditor` (with header panel), `PostPreview`, post section renderers, and a `post-editor-service` data layer. The Add Section gallery now filters by scope, and post type cards expose an "Edit template" action.
|
|
822
|
+
- **mcp-server**: scope-aware `list_section_types`, plus `get_post_template` and `save_post_template` tools so an external AI agent can read and build a post type's template.
|
|
823
|
+
|
|
824
|
+
### Patch Changes
|
|
825
|
+
|
|
826
|
+
- 91885de: Add the Posts admin area (All Posts + Post Types).
|
|
827
|
+
- `/posts` aggregates every collection marked `type: 'post'` (or
|
|
828
|
+
`admin.group: 'Posts'`) into a single sortable, filterable table
|
|
829
|
+
with type tabs, search, status/author filters, bulk actions
|
|
830
|
+
(publish / move to draft / delete), and pagination. Counts come
|
|
831
|
+
from the data layer, not constants.
|
|
832
|
+
- `/posts/types` shows a card grid per post-type collection with
|
|
833
|
+
accent border, icon, description, post count, field count, and
|
|
834
|
+
URL base — plus a dashed "+ New Post Type" card linking to setup
|
|
835
|
+
instructions.
|
|
836
|
+
- `/posts/new` lands on a type picker before opening the existing
|
|
837
|
+
per-collection editor.
|
|
838
|
+
- `/posts/types/new` offers a **Quick Post Type** gallery (Case
|
|
839
|
+
Studies, News, Portfolio, Events, Team) that pre-fills a tailored
|
|
840
|
+
field set, icon, and accent color, plus a manual form — both feed a
|
|
841
|
+
paste-ready `actuate.config.ts` snippet generator. Collections
|
|
842
|
+
remain code-defined because the read-only serverless filesystem
|
|
843
|
+
rules out runtime config writes.
|
|
844
|
+
- Sidebar collapses every post-type collection under a single
|
|
845
|
+
"Posts" parent with fixed `All Posts` / `Post Types` children.
|
|
846
|
+
- `/api/cms/collections` now surfaces `admin.description`, `icon`,
|
|
847
|
+
`color`, and `group` so the admin client can render type cards
|
|
848
|
+
and tabs without a separate fetch.
|
|
849
|
+
- New `admin.color` field on `CollectionDefinition` (accent token).
|
|
850
|
+
|
|
851
|
+
V1 deliberately skips an in-app Post Type editor / field builder —
|
|
852
|
+
the cards click through to the existing per-collection list and
|
|
853
|
+
new Post Types are added via the snippet generator. A follow-up PR
|
|
854
|
+
introduces a DB-overlay for visual metadata (icon / color /
|
|
855
|
+
description) editable from the admin UI.
|
|
856
|
+
|
|
857
|
+
- 4451a57: `/api/cms/stats` now filters the dashboard's recent activity feed,
|
|
858
|
+
total document count, per-collection counts, status counts, and SEO
|
|
859
|
+
score corpus to "user-facing CMS content" only.
|
|
860
|
+
- If the CMS config registers collections (the production shape),
|
|
861
|
+
those slugs are the allowlist. Anything else — benchmark fixtures,
|
|
862
|
+
orphan rows from removed collections, test data — is hidden.
|
|
863
|
+
- If `config.collections` is empty (dev consumers that ship
|
|
864
|
+
collections to the admin client only), the endpoint falls back to
|
|
865
|
+
a small denylist of internal slugs (`benchmarks`, `bench`).
|
|
866
|
+
|
|
867
|
+
This stops the benchmark suite's `benchmarks` collection from leaking
|
|
868
|
+
into the dashboard.
|
|
869
|
+
|
|
870
|
+
## 0.26.0
|
|
871
|
+
|
|
872
|
+
### Minor Changes
|
|
873
|
+
|
|
874
|
+
- 560d1de: Dashboard redesign + real-data backends.
|
|
875
|
+
|
|
876
|
+
`@actuate-media/cms-admin`:
|
|
877
|
+
- Sidebar now uses explicit Content and Settings group headers with nested
|
|
878
|
+
Pages children (Saved Sections, Templates). The collapse control moved
|
|
879
|
+
to the sidebar footer.
|
|
880
|
+
- Header hoists primary page actions (e.g. "+ New Post", "View Site")
|
|
881
|
+
into the top utility bar so the page hero is no longer the source of
|
|
882
|
+
truth for those affordances. The notification bell is wired to the
|
|
883
|
+
real `NotificationBell` component.
|
|
884
|
+
- Dashboard greeting picks the user's first name, and the "Delivery API"
|
|
885
|
+
- "Content Health" cards now read live data from new endpoints.
|
|
886
|
+
|
|
887
|
+
`@actuate-media/cms-core`:
|
|
888
|
+
- New `ApiRequestMetric` table (minute-bucketed counter + log-scale
|
|
889
|
+
latency histogram) and `recordApiRequest` instrumentation wired into
|
|
890
|
+
`handler-factory.ts`. Writes are fire-and-forget — request paths are
|
|
891
|
+
never blocked on the metric DB.
|
|
892
|
+
- New `ContentIssue` table + nightly `processContentHealthScan` cron
|
|
893
|
+
(`/api/cms/cron/content-health`) that runs a pure-logic scanner for
|
|
894
|
+
missing meta descriptions, missing alt text, outdated content
|
|
895
|
+
(>90 days), and broken internal links via HEAD probes.
|
|
896
|
+
- New endpoints `GET /api/cms/stats/api-delivery` and
|
|
897
|
+
`GET /api/cms/stats/content-health` return the dashboard's
|
|
898
|
+
aggregates with stable, never-undefined response shapes.
|
|
899
|
+
- Migration `0007_dashboard_metrics` adds both tables with the indexes
|
|
900
|
+
the new queries need. Safe to apply to populated DBs (no backfill).
|
|
901
|
+
|
|
902
|
+
`create-actuate-cms`:
|
|
903
|
+
- Scaffolded `vercel.json` now includes the nightly content-health
|
|
904
|
+
cron entry, and the deployment guide documents it.
|
|
905
|
+
|
|
906
|
+
## 0.25.1
|
|
907
|
+
|
|
908
|
+
### Patch Changes
|
|
909
|
+
|
|
910
|
+
- e72948a: Fix `compareToDummyHash` eager-init parity test: also warm Node's
|
|
911
|
+
libuv crypto worker pool before measuring. PR #170 awaited the
|
|
912
|
+
eager-init promise but missed that the first PBKDF2 call across the
|
|
913
|
+
entire process pays an additional ~100-150ms cold-start to spawn the
|
|
914
|
+
worker thread + initialise the C++ binding context. The first run on
|
|
915
|
+
`main` (run 26599207053) failed with e1=291ms vs e2=150ms — same ~2×
|
|
916
|
+
ratio as the historical bug, but caused by worker-pool warmup rather
|
|
917
|
+
than a real regression. The test now does a throwaway warmup call
|
|
918
|
+
after the eager-init wait so both measurements run on a hot worker
|
|
919
|
+
pool.
|
|
920
|
+
- 40d48da: Two flake fixes from the OSS maintenance pass:
|
|
921
|
+
- **`NotificationBell` SSE/REST race.** The `seenIdsRef` lookup index
|
|
922
|
+
was reconciled with `items` via `useEffect([items])`, which runs
|
|
923
|
+
after React commits. An SSE push that arrived between a REST
|
|
924
|
+
refresh resolving and the effect running would read a stale ref,
|
|
925
|
+
fall through the de-dup gate, and append a duplicate row. The ref
|
|
926
|
+
is now maintained synchronously at every items-mutation call site,
|
|
927
|
+
and the SSE handler also re-checks the latest snapshot inside the
|
|
928
|
+
`setItems` updater as a final safety net.
|
|
929
|
+
- **`compareToDummyHash` eager-init test determinism.** Exported a
|
|
930
|
+
test-only `_waitForDummyHashReady()` so the timing-parity test can
|
|
931
|
+
measure verify-only latency (rather than racing the eager hash
|
|
932
|
+
precompute on a cold module). Added a paired test that asserts the
|
|
933
|
+
eager-init promise resolves with no work pending on the consumer
|
|
934
|
+
side — catches future refactors that accidentally drop the
|
|
935
|
+
top-level invocation.
|
|
936
|
+
|
|
937
|
+
## 0.25.0
|
|
938
|
+
|
|
939
|
+
### Minor Changes
|
|
940
|
+
|
|
941
|
+
- a0f6eba: Rich `@`-mention picker for comments (last Phase 7 carry-over).
|
|
942
|
+
|
|
943
|
+
**`cms-core`**: New `GET /api/cms/users/search?q=<query>` endpoint —
|
|
944
|
+
authenticated (any role), case-insensitive `contains` matching against
|
|
945
|
+
`name` + `email`, capped at 10 results, returns only `{ id, name,
|
|
946
|
+
email }` (no role / hash / audit metadata). Empty / whitespace
|
|
947
|
+
queries return an empty array so the endpoint can't be used as a
|
|
948
|
+
directory-enumeration tool.
|
|
949
|
+
|
|
950
|
+
**`cms-admin`**: New `<MentionableTextarea>` component plus pure
|
|
951
|
+
helpers in `lib/mention-picker.ts` (`detectMentionContext`,
|
|
952
|
+
`insertMentionToken`, `searchMentionUsers`). Wired into the three
|
|
953
|
+
comment composer surfaces in `CommentSidePanel`: the new-thread
|
|
954
|
+
textarea, the reply input, and the in-place edit textarea. The
|
|
955
|
+
picker is keyboard-driven (Arrow Up / Down, Enter / Tab to commit,
|
|
956
|
+
Escape to close) with a Radix-style listbox + ARIA roles, and
|
|
957
|
+
gracefully handles slow responses (drops stale requests when typing
|
|
958
|
+
quickly).
|
|
959
|
+
|
|
960
|
+
**`mcp-server`**: New `search_users(query)` tool exposes the same
|
|
961
|
+
lookup so AI agents can resolve teammates by name fragment before
|
|
962
|
+
drafting a comment. Wire format is unchanged — the picker writes
|
|
963
|
+
`@<userId>` tokens that the existing `extractMentionedUserIds`
|
|
964
|
+
parser already understands, so no notification schema migration is
|
|
965
|
+
required.
|
|
966
|
+
|
|
967
|
+
No breaking changes.
|
|
968
|
+
|
|
969
|
+
## 0.24.0
|
|
970
|
+
|
|
971
|
+
### Minor Changes
|
|
972
|
+
|
|
973
|
+
- 4732184: Per-user SSE push channel for the notification bell (Phase 7 carry-over).
|
|
974
|
+
|
|
975
|
+
`@actuate-media/cms-core` exposes a new authenticated SSE endpoint:
|
|
976
|
+
|
|
977
|
+
GET /api/cms/notifications/stream
|
|
978
|
+
|
|
979
|
+
The stream pushes each `comment_reply`, `comment_mention`, and
|
|
980
|
+
`comment_resolved` notification the instant it's persisted to
|
|
981
|
+
`DocumentNotification`. The wire format is `event: ready` (once on
|
|
982
|
+
connect, with the subscribed `userId`) followed by `event:
|
|
983
|
+
notification` frames carrying the full DTO, plus a 25 s heartbeat
|
|
984
|
+
keepalive and a 4 min server-initiated close so `EventSource`
|
|
985
|
+
reconnects before platform max-duration caps fire.
|
|
986
|
+
|
|
987
|
+
`@actuate-media/cms-admin` switches `NotificationBell` over to an
|
|
988
|
+
`EventSource`-backed subscription with the existing REST endpoints as
|
|
989
|
+
a convergence backstop (the in-process bus is single-node by design;
|
|
990
|
+
the poller now runs every 60 s instead of 30 s). The new
|
|
991
|
+
`subscribeNotificationStream` helper is exported for any consumer that
|
|
992
|
+
wants to drive its own UI off the same channel.
|
|
993
|
+
|
|
994
|
+
Both packages bump `minor`: the cms-core change adds a new public
|
|
995
|
+
route and a new realtime export surface; the cms-admin change adds a
|
|
996
|
+
new `NotificationsApi.subscribe` member and exports the
|
|
997
|
+
`NotificationStreamHandle` / `NotificationStreamOptions` /
|
|
998
|
+
`EventSourceLike` types. No breaking changes — callers who don't pass
|
|
999
|
+
a custom `api` continue to work unchanged.
|
|
1000
|
+
|
|
1001
|
+
## 0.23.0
|
|
1002
|
+
|
|
1003
|
+
### Minor Changes
|
|
1004
|
+
|
|
1005
|
+
- 21db54b: Phase 7 PR F — MCP-parity audit stylistic cleanup.
|
|
1006
|
+
|
|
1007
|
+
This is the final follow-up to `docs/mcp-parity-audit.md`. After this
|
|
1008
|
+
PR the audit is fully retired — P0, P1, and stylistic notes are all
|
|
1009
|
+
addressed.
|
|
1010
|
+
|
|
1011
|
+
### `@actuate-media/cms-core`
|
|
1012
|
+
|
|
1013
|
+
Adds `GET /api/cms/collections`: a lightweight discovery endpoint
|
|
1014
|
+
sourced directly from `getActuateConfig()`. Returns
|
|
1015
|
+
`{ data: CollectionMeta[] }` with one entry per configured collection
|
|
1016
|
+
(`slug`, `labels`, `type`, `urlPrefix`, `hidden`, `fieldCount`,
|
|
1017
|
+
`fields[{name,type,required}]`). Unauthenticated by design — the
|
|
1018
|
+
response only exposes schema-shape metadata that is already public via
|
|
1019
|
+
`/openapi.json` — and the contract is intentionally tight (only
|
|
1020
|
+
`name` / `type` / `required` per field, no validators, hooks, or
|
|
1021
|
+
access closures leak).
|
|
1022
|
+
|
|
1023
|
+
### `@actuate-media/mcp-server`
|
|
1024
|
+
|
|
1025
|
+
Three audit-mandated stylistic cleanups:
|
|
1026
|
+
1. **AI-create-X collapse (6 tools → 3).** Removed `create_service`,
|
|
1027
|
+
`create_location`, `create_team_member`. Their behaviour is folded
|
|
1028
|
+
into `create_in_collection` via an optional `template` arg
|
|
1029
|
+
(`'service' | 'location' | 'team_member' | 'generic'`) that
|
|
1030
|
+
selects a built-in prompt-enrichment recipe plus a default
|
|
1031
|
+
collection slug. Kept `create_blog_post` and `create_case_study`
|
|
1032
|
+
as type-specific tools because their field-sets are distinctive
|
|
1033
|
+
enough to be standalone agent intents.
|
|
1034
|
+
|
|
1035
|
+
**Breaking change** (acknowledged — pre-1.0, audit-recommended):
|
|
1036
|
+
the three removed tool names will return "tool not found" on next
|
|
1037
|
+
release. Migration is mechanical:
|
|
1038
|
+
- `create_service({ category, ... })`
|
|
1039
|
+
→ `create_in_collection({ template: 'service', category, ... })`
|
|
1040
|
+
- `create_location({ cityState, ... })`
|
|
1041
|
+
→ `create_in_collection({ template: 'location', cityState, ... })`
|
|
1042
|
+
- `create_team_member({ name, role, ... })`
|
|
1043
|
+
→ `create_in_collection({ template: 'team_member', name, role, ... })`
|
|
1044
|
+
|
|
1045
|
+
2. **`audit_document` split.** Replaced with `audit_document_by_id`
|
|
1046
|
+
(requires `collection` + `id`) and `audit_inline_content` (requires
|
|
1047
|
+
`title` + `body`). Both forward to the same `/ai/audit-document`
|
|
1048
|
+
endpoint; the split exists so agents pick the right mode at the
|
|
1049
|
+
tool boundary instead of constructing a half-filled blob and
|
|
1050
|
+
discovering the "must provide either (collection,id) or body" rule
|
|
1051
|
+
via a server-side error.
|
|
1052
|
+
|
|
1053
|
+
**Breaking change** (audit-recommended): `audit_document` is
|
|
1054
|
+
removed. Migration: pick the explicit tool that matches the
|
|
1055
|
+
intent — `audit_document_by_id` for stored docs,
|
|
1056
|
+
`audit_inline_content` for ad-hoc text.
|
|
1057
|
+
|
|
1058
|
+
3. **`list_collections` reliability.** Now prefers the new
|
|
1059
|
+
`/api/cms/collections` endpoint as the primary source so the tool
|
|
1060
|
+
stays accurate when the OpenAPI generator drifts. Falls back to
|
|
1061
|
+
OpenAPI only on a real HTTP 404 (older CMS server) — the decision
|
|
1062
|
+
uses `ActuateClientError.status` (newly exported from the client),
|
|
1063
|
+
not a regex against the error message, so an unrelated server
|
|
1064
|
+
error that happens to contain "not found" in its body no longer
|
|
1065
|
+
triggers the fallback. Non-404 errors propagate.
|
|
1066
|
+
|
|
1067
|
+
**Return-shape change** (consistent contract): `data` is now
|
|
1068
|
+
ALWAYS a `CollectionSummary[]` array regardless of which path
|
|
1069
|
+
responded. The OpenAPI fallback path normalises into the same
|
|
1070
|
+
array shape that the new `/collections` endpoint emits, so
|
|
1071
|
+
callers no longer need to branch on `source` to know whether they
|
|
1072
|
+
got an array or an object map. The returned envelope is
|
|
1073
|
+
`{ data: CollectionSummary[], source: 'collections' | 'openapi' }`.
|
|
1074
|
+
|
|
1075
|
+
**Newly exported:** `ActuateClientError` (subclass of `Error`
|
|
1076
|
+
carrying `status: number`). Useful for any consumer that needs
|
|
1077
|
+
to branch on the HTTP status from a `request()` failure.
|
|
1078
|
+
|
|
1079
|
+
### Tests
|
|
1080
|
+
- mcp-server: 113 tests total (was 97 in PR E). 16 new — template
|
|
1081
|
+
routing for each of service / location / team_member, audit split
|
|
1082
|
+
validation, list_collections primary + fallback + 500-propagation.
|
|
1083
|
+
- cms-core: 4 new tests for the discovery endpoint covering field
|
|
1084
|
+
summaries, `hidden` flag, missing-config 500, and the
|
|
1085
|
+
no-leak-of-validators-or-hooks contract.
|
|
1086
|
+
|
|
1087
|
+
## 0.22.0
|
|
1088
|
+
|
|
1089
|
+
### Minor Changes
|
|
1090
|
+
|
|
1091
|
+
- abc7924: Phase 5.5 #1: tag-based edge caching for public document reads.
|
|
1092
|
+
- **`@actuate-media/client`**: `list`, `get`, `global`, `resolve`, and
|
|
1093
|
+
`listMedia` now automatically attach per-resource cache tags
|
|
1094
|
+
(`actuate:collection:<slug>`, `actuate:doc:<id>`,
|
|
1095
|
+
`actuate:global:<slug>`, `actuate:path:<path>`) alongside the existing
|
|
1096
|
+
wholesale `actuate` tag. New exported helpers `actuateTag(kind, slug)`
|
|
1097
|
+
and `actuateTags(kind, ...)` return the canonical tag set so a
|
|
1098
|
+
webhook handler can call `revalidateTag()` with the exact same
|
|
1099
|
+
strings the SDK uses for reads. The `tags` field on `defaultCache`
|
|
1100
|
+
remains opt-in; resource tags are merged on top of it and deduped.
|
|
1101
|
+
`revalidate: false` (no-store) bypasses tag emission entirely.
|
|
1102
|
+
- **`@actuate-media/cms-core`**: new top-level `cache?: CacheConfig`
|
|
1103
|
+
field on `ActuateCMSConfig`. When `cache.publicReads.enabled` is true
|
|
1104
|
+
the four read endpoints (`GET /collections/:slug`,
|
|
1105
|
+
`GET /collections/:slug/:id`, `GET /globals/:slug`, `GET /resolve`)
|
|
1106
|
+
emit `Cache-Control: public, s-maxage=<maxAge>, stale-while-revalidate=<swr>`
|
|
1107
|
+
for unauthenticated and API-key requests. Session-cookie requests
|
|
1108
|
+
always get `private, no-store`; preview-token resolve requests bypass
|
|
1109
|
+
the cache. New `cache.onInvalidate` hook fires after every
|
|
1110
|
+
create/update/delete and global update with the canonical tag set;
|
|
1111
|
+
failures are swallowed so cache invalidation can never roll back a
|
|
1112
|
+
successful write. The cache layer is off by default — existing
|
|
1113
|
+
consumers see no behaviour change until they opt in. See
|
|
1114
|
+
`docs/caching.md` for the full contract.
|
|
1115
|
+
|
|
1116
|
+
- d0a5275: Phase 5.5 #5 — Vercel Runtime Cache (layer 2) for public reads.
|
|
1117
|
+
|
|
1118
|
+
Adds an optional second cache layer between the CDN tier shipped in
|
|
1119
|
+
Phase 5.5 #1 and the database. When a CDN entry expires, the request
|
|
1120
|
+
reaches the function — and a runtime-cache hit lets the function
|
|
1121
|
+
return in ~10–20 ms instead of paying the full ~80 ms Postgres
|
|
1122
|
+
roundtrip.
|
|
1123
|
+
- New `CacheConfig.runtimeCache` config field — `{ adapter, ttl? }`.
|
|
1124
|
+
The adapter contract (`get` + `set` with TTL + tags) matches Vercel's
|
|
1125
|
+
`getCache()` from `@vercel/functions` so it can be passed through
|
|
1126
|
+
unchanged. Self-hosted projects can plug in any KV store (Upstash,
|
|
1127
|
+
in-memory, Redis) by satisfying the same interface.
|
|
1128
|
+
- New `cachedRead` helper in `packages/cms-core/src/cache/http.ts`.
|
|
1129
|
+
Wraps the four public read endpoints
|
|
1130
|
+
(`GET /collections/:slug`, `GET /collections/:slug/:id`,
|
|
1131
|
+
`GET /globals/:slug`, `GET /resolve`). Stamps responses with
|
|
1132
|
+
`X-Actuate-Cache: HIT|MISS`. Session-cookie requests bypass the
|
|
1133
|
+
layer entirely (per-user data must never enter a shared cache),
|
|
1134
|
+
matching the existing CDN-layer rule.
|
|
1135
|
+
- New `cacheKeyFor` helper — derives cache keys from `pathname` plus
|
|
1136
|
+
sorted query, so `?page=1&pageSize=20` and `?pageSize=20&page=1`
|
|
1137
|
+
share a cache entry while `?populate=author` cleanly separates
|
|
1138
|
+
populated/non-populated reads.
|
|
1139
|
+
- Adapter failures (`get` or `set` throwing) are logged and swallowed.
|
|
1140
|
+
Cache outages degrade silently to a direct DB read; user-visible
|
|
1141
|
+
requests never fail because the cache fell over.
|
|
1142
|
+
- Size guard: payloads > ~1.5 MB skip the `set` call to stay under
|
|
1143
|
+
Vercel's 2 MB runtime-cache item ceiling.
|
|
1144
|
+
|
|
1145
|
+
No public API changes for existing consumers. The new field is
|
|
1146
|
+
optional and the four read endpoints behave identically when it's
|
|
1147
|
+
unset.
|
|
1148
|
+
|
|
1149
|
+
### Patch Changes
|
|
1150
|
+
|
|
1151
|
+
- 1957416: Fix silent production regression introduced in PR #132 (`relationLoadStrategy: 'join'` queries on the dashboard recent-docs panel, document versions list, and `doc_get` bench scenario):
|
|
1152
|
+
- Add the `relationJoins` Preview-feature flag to `packages/cms-core/prisma/schema.prisma` and `apps/dev/prisma/schema.prisma`. Without it Prisma 7 rejects `relationLoadStrategy: 'join'` as an "Unknown argument" — the optimisation never actually fired, the dashboard recent-docs panel silently returned empty (`safeFindMany` swallowed the error), the versions panel returned 500, and `/api/bench` reported 100% errors for `doc_get`.
|
|
1153
|
+
- Stop the `safeCount` / `safeFindMany` / `safeGroupBy` helpers in `api/handlers.ts` from swallowing all errors. They still return the empty value (`0` / `[]`) to keep the request path alive when an optional Prisma model is missing — but now log unexpected errors to `console.warn` so the next time a query is silently failing in production it shows up in Vercel + Sentry logs.
|
|
1154
|
+
- New regression test (`schema-preview-features.test.ts`) pins both `relationJoins` and `fullTextSearchPostgres` on the schema so this class of bug can't recur.
|
|
1155
|
+
|
|
1156
|
+
`apps/dev`: regenerated Prisma client (no source changes — the new flag enables the existing `relationLoadStrategy: 'join'` calls).
|
|
1157
|
+
|
|
1158
|
+
- 93a910a: Phase 5.5 #2 + #3 — cache-miss latency pass.
|
|
1159
|
+
- `populateRelations` / `validateRelations` now batch every relation
|
|
1160
|
+
fetch into a single `RelationLookup.fetchByIds(allCollections, allIds)`
|
|
1161
|
+
call instead of issuing one DB round-trip per target collection.
|
|
1162
|
+
Documents that link into N collections now cost 1 round-trip on
|
|
1163
|
+
populate, not N.
|
|
1164
|
+
- Dashboard recent-docs and version-list queries opt into Prisma 7's
|
|
1165
|
+
`relationLoadStrategy: 'join'`, folding the `createdBy` / `updatedBy`
|
|
1166
|
+
/ `changedBy` user joins into one SQL round-trip. Same shape applied
|
|
1167
|
+
to the benchmarks Prisma provider's `getDocument` scenario so
|
|
1168
|
+
`/api/bench` measures the new geometry.
|
|
1169
|
+
- `apps/dev` API routes pin `preferredRegion = 'iad1'` so the deployed
|
|
1170
|
+
Vercel Function co-locates with the Prisma Postgres tier the
|
|
1171
|
+
Marketplace integration provisions in the same region. Override
|
|
1172
|
+
documented in `docs/deployment.md`.
|
|
1173
|
+
|
|
1174
|
+
- 0f50407: Security: bump `sanitize-html` from 2.17.3 to 2.17.4 to fix a default XSS via raw-text `<xmp>` passthrough (GHSA-vjp9-mfvj-w4qg, CRITICAL). The advisory only fires when content is rendered with the default `allowedTags`; cms-core's bundled defaults are unaffected, but the patch closes the entire class of bypasses for any downstream caller that uses the bundled sanitizer.
|
|
1175
|
+
|
|
1176
|
+
## 0.21.0
|
|
1177
|
+
|
|
1178
|
+
### Minor Changes
|
|
1179
|
+
|
|
1180
|
+
- 1675eac: Add component-aware blocks — Phase 4 foundation slice.
|
|
1181
|
+
|
|
1182
|
+
Ships the new `@actuate-media/component-blocks` package, which reads a directory of React component source files and emits a typed `Manifest` describing every exported component's props (`string` / `number` / `boolean` / `enum` / `union` / `object` / `array`). Same-file type aliases are resolved automatically. Includes a CLI (`actuate-component-blocks extract --root <dir> --out <file>`) and a programmatic `extractManifest()` API.
|
|
1183
|
+
|
|
1184
|
+
Adds the `componentBlock` field type to `@actuate-media/cms-core`, which consumes a manifest and validates stored block values — rejecting unknown components, missing required props, wrong-type values, and out-of-set enum picks. Field-level `allow` and `defaultComponent` options are supported.
|
|
1185
|
+
|
|
1186
|
+
This is the foundation slice; admin form generation, migration-diff CLI, and discriminated-union pickers ship in follow-up PRs. See `docs/component-blocks.md`.
|
|
1187
|
+
|
|
1188
|
+
## 0.20.0
|
|
1189
|
+
|
|
1190
|
+
### Minor Changes
|
|
1191
|
+
|
|
1192
|
+
- 8f2d3f8: Redesign the admin Dashboard and fix the recurring "content cut off on the left" layout bug.
|
|
1193
|
+
|
|
1194
|
+
**Dashboard (`@actuate-media/cms-admin`)**
|
|
1195
|
+
- New `Dashboard.tsx` matching the production design: time-of-day greeting, hero action row (New Post / View Site), horizontally-scrollable quick actions, five stat cards, two-column main grid (Recent Activity + Publishing Queue + Content Health), and a four-tile Content Delivery footer (Total Documents, Scheduled, Form Responses, Active Webhooks).
|
|
1196
|
+
- All data derived from the existing `/stats` and `/health` endpoints — no additional network round-trips. Recent activity, publishing queue, content health, and delivery tiles are computed client-side via `useMemo` so the backend never blocks the frontend.
|
|
1197
|
+
- Fully responsive: stat cards collapse from 5 → 3 → 2 columns at `lg` / `sm` / mobile breakpoints, the main grid stacks below `lg`, and quick actions scroll horizontally on narrow screens.
|
|
1198
|
+
- Deterministic per-author avatar colors (hash-based) so the activity feed is stable across renders without state or extra fetches.
|
|
1199
|
+
|
|
1200
|
+
**Layout cutoff fix (`@actuate-media/cms-admin`)**
|
|
1201
|
+
- Added `lg:shrink-0` to the desktop Sidebar wrapper. Without it, on viewports between `lg` (1024px) and ~1280px the flex sidebar column could compress below the Sidebar's intrinsic 256px width, causing dashboard content to overflow into and visually overlap the sidebar. Combined with the existing `min-w-0` on the main column, the row now reliably renders as `[256px Sidebar][1fr Main]` at every desktop width.
|
|
1202
|
+
- Dashboard is wrapped in `max-w-[1400px] mx-auto w-full` so on ultra-wide displays the content stays in a comfortable reading column instead of stretching edge-to-edge.
|
|
1203
|
+
|
|
1204
|
+
**Stats endpoint extension (`@actuate-media/cms-core`)**
|
|
1205
|
+
- `GET /stats` now returns `webhookCount` and `webhookActiveCount`, calculated via two cheap `safeCount` calls in the existing `Promise.allSettled` batch. The dashboard's "Active Webhooks" tile uses these instead of issuing a second request, keeping page load to a single stats fetch.
|
|
1206
|
+
|
|
1207
|
+
- 0ac9024: Inline AI co-author HTTP endpoint + MCP tool (Phase 2).
|
|
1208
|
+
|
|
1209
|
+
Adds `POST /api/cms/ai/coauthor` with four sub-actions (`rewrite`,
|
|
1210
|
+
`expand`, `compress`, `proofread`) that wrap the plugin-ai writing
|
|
1211
|
+
primitives behind a single endpoint. Rate-limited under the same
|
|
1212
|
+
bucket as content generation (20 calls/hour/user). Missing provider
|
|
1213
|
+
configuration surfaces as HTTP 501 instead of 500.
|
|
1214
|
+
|
|
1215
|
+
Exposed to AI agents via the MCP tool `coauthor_text` with the same
|
|
1216
|
+
input shape, so the admin UI and AI agents can drive identical
|
|
1217
|
+
text-transformation workflows.
|
|
1218
|
+
|
|
1219
|
+
Tests: 5 endpoint tests covering each action + validation errors.
|
|
1220
|
+
|
|
1221
|
+
- 5d18097: Wire per-type SEO defaults into the admin Settings panel — the last loose end from Slice A (SEO Completeness).
|
|
1222
|
+
|
|
1223
|
+
**`cms-core`**
|
|
1224
|
+
- New `seo/config-store` module persists admin edits in a single reserved-slug document (`__seo_config__`) and merges them on top of the static `actuate.config.ts` `seo` block at runtime, field-by-field.
|
|
1225
|
+
- New endpoints `GET /seo/config` and `PUT /seo/config` (admin-only). Saving uses partial-update semantics so editing the site-wide section never wipes per-collection overrides (and vice versa).
|
|
1226
|
+
- `/sitemap.xml`, `/sitemaps/:slug.xml`, `/robots.txt`, `/og.png`, and `/resolve` now load the merged config per request via `loadEffectiveConfig()`, so admin changes are live immediately with no redeploy.
|
|
1227
|
+
- Empty strings in PUT payloads are treated as "unset" (fall back to static config) — the panel is purely additive.
|
|
1228
|
+
|
|
1229
|
+
**`cms-admin`**
|
|
1230
|
+
- New **Settings → SEO** tab backed by `<SEOConfigPanel />`. Edits site-wide defaults (siteUrl, siteName, OG image, Twitter handle, change-freq, priority, robots toggles, AI-bot blocking) and per-collection defaults (Schema.org type, archive path, sitemap priority/changefreq, noindex/nofollow, exclude-from-sitemap).
|
|
1231
|
+
- Static `actuate.config.ts` values render as input placeholders so admins always see what they're overriding.
|
|
1232
|
+
|
|
1233
|
+
12 new unit tests cover the merge semantics, unset behaviour, DB roundtrip, upsert (no duplicate rows), and DB-error fallback.
|
|
1234
|
+
|
|
1235
|
+
- 313a23f: Add Slice F — AI quality pass.
|
|
1236
|
+
|
|
1237
|
+
`plugin-ai` exposes a new `auditDocument()` composer that combines SEO,
|
|
1238
|
+
readability (Flesch-Kincaid / Gunning Fog / SMOG averaged + remapped to a
|
|
1239
|
+
0-100 score), accessibility, and freshness scoring into a single
|
|
1240
|
+
composite grade with prioritized actionable issues. The composer is
|
|
1241
|
+
deterministic — it never calls an LLM — so it's cheap enough to invoke
|
|
1242
|
+
on every save or debounced keystroke.
|
|
1243
|
+
|
|
1244
|
+
`cms-core` adds two HTTP endpoints:
|
|
1245
|
+
- `POST /ai/audit-document` — runs the composite audit against a stored
|
|
1246
|
+
document (`{ collection, id }`) or ad-hoc content. Honors field-level
|
|
1247
|
+
access control when loading from the DB.
|
|
1248
|
+
- `POST /ai/suggest-internal-links` — scans published documents for
|
|
1249
|
+
phrases the supplied content could link to, returning ranked
|
|
1250
|
+
anchor-text + target-URL suggestions. Optionally scopes to a single
|
|
1251
|
+
collection.
|
|
1252
|
+
|
|
1253
|
+
`mcp-server` exposes both endpoints as MCP tools (`audit_document` and
|
|
1254
|
+
`suggest_internal_links`) so AI agents can grade drafts and propose
|
|
1255
|
+
internal links before publish.
|
|
1256
|
+
|
|
1257
|
+
Docs: `docs/ai-quality-audit.md`.
|
|
1258
|
+
|
|
1259
|
+
## 0.19.0
|
|
1260
|
+
|
|
1261
|
+
### Minor Changes
|
|
1262
|
+
|
|
1263
|
+
- 39f5941: Slice E — Preview & Publishing Polish.
|
|
1264
|
+
|
|
1265
|
+
**Draft preview links**
|
|
1266
|
+
- Preview tokens now accept a configurable `ttlSeconds` (clamped to
|
|
1267
|
+
`[60, 2,592,000]`, default 7 days). The old 5-minute hard-coded TTL
|
|
1268
|
+
was too short for sharing with clients.
|
|
1269
|
+
- `POST /preview/token` now verifies the document exists before
|
|
1270
|
+
issuing a token, includes the doc's `slug` in the response so the
|
|
1271
|
+
admin can build a friendly URL, and records the issuing user in the
|
|
1272
|
+
JWT `sub` claim for audit logs.
|
|
1273
|
+
- `GET /resolve?preview=<token>` drops the `status: PUBLISHED` filter
|
|
1274
|
+
when a valid token is presented, returning draft content at the
|
|
1275
|
+
document's real URL. The response includes a `preview: { active,
|
|
1276
|
+
expiresAt }` block so frontends can render a "preview mode" banner.
|
|
1277
|
+
Tokens are bound to a single `(collection, documentId)` — using one
|
|
1278
|
+
against a different URL returns `403`.
|
|
1279
|
+
|
|
1280
|
+
**Scheduled publishing**
|
|
1281
|
+
- New `POST /collections/:slug/:id/schedule` endpoint sets
|
|
1282
|
+
`scheduledAt` and/or `scheduledUnpublishAt` (validated as future
|
|
1283
|
+
ISO 8601 timestamps with `unpublish > publish`) and flips status to
|
|
1284
|
+
`SCHEDULED`. The existing scheduling cron picks the doc up at the
|
|
1285
|
+
scheduled time.
|
|
1286
|
+
- New `DELETE /collections/:slug/:id/schedule` cancels a pending
|
|
1287
|
+
schedule. `SCHEDULED` docs revert to `DRAFT`; `PUBLISHED` docs with
|
|
1288
|
+
a pending unpublish stay live.
|
|
1289
|
+
|
|
1290
|
+
**Admin UI**
|
|
1291
|
+
- New **Share preview** toolbar button on `DocumentEdit` opens a
|
|
1292
|
+
dialog to generate signed preview URLs with selectable TTL
|
|
1293
|
+
(1 hour / 1 day / 7 days / 30 days) and a copy-to-clipboard control.
|
|
1294
|
+
- New **Schedule** button in the Status panel opens a datetime picker
|
|
1295
|
+
for setting both publish and (optionally) unpublish times. Active
|
|
1296
|
+
schedules render an inline blue pill with the times. "Reschedule"
|
|
1297
|
+
and "Cancel schedule" supported.
|
|
1298
|
+
- New exported components: `SharePreviewLinkDialog`,
|
|
1299
|
+
`SchedulePublishDialog`.
|
|
1300
|
+
|
|
1301
|
+
**Tests & docs**
|
|
1302
|
+
- 20 new tests: preview adapter TTL clamping, JWT round-trip, secret
|
|
1303
|
+
isolation; `/preview/token` + `/resolve?preview=` happy/sad paths;
|
|
1304
|
+
schedule create/cancel/validation paths.
|
|
1305
|
+
- New `docs/preview-and-scheduling.md` walkthrough; `docs/api-reference.md`
|
|
1306
|
+
expanded with the new endpoints.
|
|
1307
|
+
|
|
1308
|
+
## 0.18.0
|
|
1309
|
+
|
|
1310
|
+
### Minor Changes
|
|
1311
|
+
|
|
1312
|
+
- 4433301: Phase 2 Slice D — Content Relations & References.
|
|
1313
|
+
- **cms-core**: runtime support for the `relationship` field type — referential validation and opt-in depth-1 population.
|
|
1314
|
+
- **Validation**: every create/update verifies that referenced IDs point at live (non-deleted) documents in the declared target collection(s). Broken references fail with a precise error (`Relation field "author" references document "team-x42" which does not exist in collection "team".`). Updates only re-validate fields the caller actually touched.
|
|
1315
|
+
- **Population**: pass `?populate=author,relatedPosts` (or `*`) on `GET /collections/:slug/:id` and `GET /resolve` to expand stored IDs into `{ id, collection, title, slug, status, data }` summaries. Batches by target collection (≤ 1 query per collection per request); soft-deleted refs fall back to raw IDs.
|
|
1316
|
+
- **Presets**: new `relationField()` and `relatedDocsField()` helpers. Built-in collection presets now wire `posts.relatedPosts → posts`, `case-studies.services → services` (the `locations.servicesOffered → services` relation continues unchanged).
|
|
1317
|
+
- **Multi-target**: `relationTo: ['posts', 'case-studies']` continues to work — validator accepts a doc if it lives in any target.
|
|
1318
|
+
- Public API: `validateRelations`, `populateRelations`, `collectRelationRefs`, `parsePopulateParam`, `buildRelationLookup`, plus types `RelationLookup`, `RelationSummary`, `PopulateOptions`.
|
|
1319
|
+
- **plugin-ai**: the document content generator now skips `relationship` fields when building the LLM prompt (the model can't know your document IDs) and accepts ID strings only on output. Prevents the AI from inventing broken references that the validator would then reject.
|
|
1320
|
+
- **23 new tests** in `cms-core/src/__tests__/fields/relations.test.ts` covering ref collection, validation, population, populate-param parsing, multi-target relations, and array-nested relations. Full suite: 620 cms-core tests passing.
|
|
1321
|
+
- **Docs**: new `docs/relations.md` (declaration, validation, populate, admin UI, common patterns), `?populate=` documented in `docs/api-reference.md` for both `/collections/:slug/:id` and `/resolve`.
|
|
1322
|
+
- **Admin UI**: the existing `RelationshipField` picker continues to work — search, multi-select, status badges, "Create new" shortcut. No changes required.
|
|
1323
|
+
|
|
1324
|
+
## 0.17.0
|
|
1325
|
+
|
|
1326
|
+
### Minor Changes
|
|
1327
|
+
|
|
1328
|
+
- e6a18b5: Phase 2 Slice C — AI Content Authoring for any collection.
|
|
1329
|
+
- **plugin-ai**: new `generateDocumentContent()` helper that maps a prompt onto a collection's field schema and returns structured field data ready to persist. Supports tone, target audience, brand voice profile, and existing-content context. Coerces LLM output to declared field types (numbers, booleans, dates, select options, array items) and drops fields the schema doesn't declare.
|
|
1330
|
+
- **cms-core**: new `POST /collections/:slug/ai-create` endpoint — the non-page counterpart to `/page-builder/create`. Generates a complete document for any collection, persists as draft, optionally publishes via a follow-up update when the caller has admin scope. Refuses page-builder collections (use `/page-builder/create` instead). Hard caps prompt at 4000 chars and shares the per-user 20/hour AI rate-limit bucket. Returns a `warning` field when create succeeds but publish is denied.
|
|
1331
|
+
- **mcp-server**: six new tools — `create_in_collection` (generic) plus per-type sugar tools `create_blog_post`, `create_case_study` (with `clientName` / `industry` hints), `create_service` (`category` hint), `create_location` (`cityState` hint), and `create_team_member` (`name` / `role` hints). Each per-type tool targets the conventional collection slug shipped by the cms-core collection presets, with an explicit `collection` override for sites using custom slugs.
|
|
1332
|
+
- 19 new tests (10 unit tests in plugin-ai for the content generator, 9 integration tests in cms-core for the API endpoint covering auth, validation, persistence, publish flow, and the page-builder refusal path).
|
|
1333
|
+
- Docs: updated `docs/api-reference.md` with the new endpoint, `docs/mcp.md` and `packages/mcp-server/README.md` with the new tools and example agent prompts.
|
|
1334
|
+
|
|
1335
|
+
## 0.16.0
|
|
1336
|
+
|
|
1337
|
+
### Minor Changes
|
|
1338
|
+
|
|
1339
|
+
- 9377c39: **Post-type Polish (Phase 2, Slice B)** — one-line content types for the eight patterns nearly every marketing site needs, plus matching page-builder starter templates.
|
|
1340
|
+
- **Collection presets** — `blogPostCollection()`, `caseStudyCollection()`, `portfolioCollection()`, `serviceCollection()`, `locationCollection()`, `teamMemberCollection()`, `faqCollection()`, `testimonialCollection()`. Each returns a complete `CollectionDefinition` with sensible fields, admin defaults, `seo.defaultSchemaType` set to the right Schema.org type (BlogPosting / Article / CreativeWork / Service / LocalBusiness / Person / FAQPage / Review), archive paths for post-type collections, sitemap priorities, and admin grouping. All accept `CollectionPresetOverrides` (extraFields, seo merge, label / slug / urlPrefix overrides).
|
|
1341
|
+
- **Field presets** — reusable bundles you can spread into any collection: `titleField`, `slugField`, `excerptField`, `richContentField`, `featuredImageField`, `tagsField`, `publishedDateField`, `authorField`, `quoteField`, `ctaField`, `seoFields`, `locationFields`, `geoFields`, `priceFields`.
|
|
1342
|
+
- **Four new page-builder templates** — `case-study` (hero / metrics / challenge / solution / results / quote / CTA), `portfolio` (cover / info strip / description / gallery / CTA), `service` (hero with CTA / description / benefits / pricing / FAQ), `team-member` (avatar hero / bio / contact links). Joins the existing `blank`, `landing`, `about`, `contact`, `location`, and `blog-post` templates.
|
|
1343
|
+
- **Per-collection template suggestion** — `GET /api/cms/page-templates?collection=:slug` now reorders the response so suggested templates bubble to the top, annotates each template with `suggested` + `suggestionRank`, and returns a `suggestion` object pointing at the primary template ID. `getTemplatesForCollection('posts')` exposes the mapping in code.
|
|
1344
|
+
- **33 new tests** (16 unit field-preset + 17 unit collection-preset + suggestion lookup). Full suite: 588 passed.
|
|
1345
|
+
- **Docs updated** — `docs/configuration.md` adds the Collection Presets, Field Presets, and Page-builder Templates per Type sections.
|
|
1346
|
+
|
|
1347
|
+
## 0.15.0
|
|
1348
|
+
|
|
1349
|
+
### Minor Changes
|
|
1350
|
+
|
|
1351
|
+
- 00ffcf7: **SEO Completeness (Phase 2, Slice A)** — production-grade SEO surfaces wired into the core CMS with zero extra plugins.
|
|
1352
|
+
- **New `composePageMeta()` helper** — One-shot SEO composer: pass a resolved document, its collection, and the CMS config and get back a Next.js-ready `metadata` object, computed JSON-LD (Article / BlogPosting / LocalBusiness / Service / etc.), pre-rendered `<meta>` / `<script>` HTML, and the canonical URL. Auto-detects Schema.org type from collection slug (`posts → BlogPosting`, `locations → LocalBusiness`, `services → Service`, …) and respects per-collection `defaultSchemaType` / `defaultRobots` / `defaultOgImage` overrides.
|
|
1353
|
+
- **`/resolve` now returns `meta`, `jsonLd`, and `jsonLdHtml`** — SSR consumers (Next.js apps, AI agents, MCP clients) get a ready-to-render meta tag block and JSON-LD `<script>` tag without re-deriving anything from raw doc data.
|
|
1354
|
+
- **Public sitemap + robots + OG routes** — `GET /sitemap.xml`, `GET /sitemaps/:slug.xml`, `GET /robots.txt`, and `GET /og.png` are served automatically from the CMS handler once `seo.siteUrl` is set. Each route can be disabled individually (`seo.sitemap.disabled`, `seo.robots.disabled`, `seo.ogImage.disabled`).
|
|
1355
|
+
- **Per-collection SEO config** — `CollectionDefinition.seo` now accepts `defaultSchemaType`, `defaultRobots`, `defaultOgImage`, in addition to the existing `sitemapPriority` / `sitemapChangeFreq` / `excludeFromSitemap` / `archivePath`.
|
|
1356
|
+
- **Site-wide SEO config** — New top-level `seo` block on `ActuateCMSConfig` for `siteUrl`, `siteName`, `defaultOgImage`, `twitterHandle`, `organization`, `robots`, `sitemap`, and `ogImage` defaults.
|
|
1357
|
+
- **AI-bot blocking** — `seo.robots.blockAIBots: true` adds disallow rules for GPTBot, ChatGPT-User, ClaudeBot, Claude-Web, anthropic-ai, Bytespider, CCBot, and Google-Extended.
|
|
1358
|
+
- **27 new tests** covering `composePageMeta()` (15) and the public SEO routes (12).
|
|
1359
|
+
- **Updated docs** — `docs/seo.md` adds the zero-config-surfaces section and `composePageMeta` walkthrough; `docs/api-reference.md` documents the new public routes and the enriched `/resolve` response.
|
|
1360
|
+
|
|
1361
|
+
## 0.14.0
|
|
1362
|
+
|
|
1363
|
+
### Minor Changes
|
|
1364
|
+
|
|
1365
|
+
- e87471d: Phase 1: AI access. API key authentication (act*sk*\* bearer tokens) with scope-based authorization across collections, globals, media, and the page builder. New /api-keys admin endpoints + UI for creating, listing, and revoking keys with password reauth. CSRF is skipped for API-key requests. New /page-builder/create one-shot endpoint that generates an AI page and persists it as a document in a single call. New @actuate-media/mcp-server package exposes the CMS to AI agents (Claude, Cursor, ChatGPT, etc.) via the Model Context Protocol — full CRUD, globals, media, page generation, and one-shot create tools. Adds 44 new tests covering the API key auth path, scope enforcement, CSRF skip, and MCP client/tools.
|
|
1366
|
+
|
|
1367
|
+
## 0.13.0
|
|
1368
|
+
|
|
1369
|
+
### Minor Changes
|
|
1370
|
+
|
|
1371
|
+
- 9cf5d10: Engineering audit — medium and low items.
|
|
1372
|
+
|
|
1373
|
+
**New features**
|
|
1374
|
+
- `validateEnvShape()` (`diagnostics/env.ts`): runtime check that `CMS_SECRET`, `CMS_ENCRYPTION_KEY`, `DATABASE_URL`, `BLOB_READ_WRITE_TOKEN`, `UPSTASH_REDIS_*`, `RESEND_API_KEY`, and `CRON_SECRET` are not just present but well-formed (length, hex, placeholder detection). Surfaced via `/api/cms/health` so misconfiguration is caught at deploy time, not at first runtime use. Adds an `unhealthy` status when any required var is malformed.
|
|
1375
|
+
- `createLogger(scope)` (`diagnostics/logger.ts`): structured per-scope logger that respects `ACTUATE_LOG_LEVEL` (`silent`/`error`/`warn`/`info`/`debug`) and `ACTUATE_LOG_FORMAT=json` for log-aggregator ingestion. Defaults to `warn` in production, `info` elsewhere.
|
|
1376
|
+
- `getActuateConfig()` / `setActuateConfig()` (`config/runtime.ts`): typed accessor for the per-process CMS config, replacing scattered `(globalThis as any).__actuateConfig` casts. Documents the single-process / set-once contract in detail.
|
|
1377
|
+
|
|
1378
|
+
**Hardening**
|
|
1379
|
+
- `/media/upload` now requires a valid `Content-Length` header before parsing the body, closing a chunked-encoding bypass that allowed unbounded memory consumption (audit M7).
|
|
1380
|
+
- In-memory rate limiter (`createInMemoryRateLimiter`) now has a 10,000-key LRU cap with periodic expired-entry sweeping. Prevents unbounded `Map` growth under enumeration / IP-spoofing (audit M1).
|
|
1381
|
+
- Upstash rate-limiter outages now emit a throttled `rate_limit.degraded` audit log entry in addition to the existing `console.error`, giving operators a durable record of when the limiter was failing open (audit M2).
|
|
1382
|
+
|
|
1383
|
+
**Schema**
|
|
1384
|
+
- `AuthConfig` now properly types `maxConcurrentSessions`, `passwordPolicy`, and `oauth.{providers, allowSelfSignup}` — these were already consumed at runtime but cast through `any`.
|
|
1385
|
+
- `ActuateCMSConfig.redirects.allowedExternalHosts` is now part of the public type.
|
|
1386
|
+
|
|
1387
|
+
**Bug fixes**
|
|
1388
|
+
- Removed dead `config.plugins.forms.hooks` access in the form-submission handler — `plugins` is an array, not an object keyed by name, so this path always evaluated to `undefined` and was masked by the `as any` cast.
|
|
1389
|
+
- `/api/cms/health` now derives the response status from env validation in **both** the success and the DB-failure paths (Bugbot review on PR #41, Medium). The early-return on `db()` failure previously hardcoded `status: 'degraded'`, so a deploy with a broken DB _and_ a malformed `CMS_SECRET` would report `degraded` instead of the more accurate `unhealthy` — defeating the M6 goal of catching env misconfig at `/health` time.
|
|
1390
|
+
- `validateEnvShape()` "ok" messages no longer disclose exact secret lengths (Bugbot review on PR #41, Low). The previous wording — `Configured (42 chars).` — leaked configured `CMS_SECRET` and `CRON_SECRET` lengths via the unauthenticated `/health` endpoint, narrowing brute-force search space with no operational benefit. Lengths still appear in error/warn messages because those only surface on misconfiguration that the operator must fix.
|
|
1391
|
+
- Removed dead exported `log` singleton from `diagnostics/logger.ts` (Bugbot review on PR #41, Low). Every consumer uses `createLogger(scope)`; the convenience export was never imported.
|
|
1392
|
+
- Removed unreachable `isPlaceholder(encKey)` branch in `validateEnvShape()` (Bugbot review on PR #41, Low). After the `length === 64` and `HEX_RE` checks pass, the value is guaranteed pure hex characters, but every pattern in `PLACEHOLDER_PATTERNS` requires non-hex letters — so the branch could never fire. The `HEX_RE` failure message already reads "Looks like a placeholder" verbally, which covers the common case (e.g. `aes256-local-dev-key-change-in-prod`).
|
|
1393
|
+
- `/api/cms/health` now consults all three legitimate `CMS_SECRET` sources via a wrapped `EnvSource` passed to `validateEnvShape()` — `process.env.CMS_SECRET`, the legacy `process.env.CMS_SESSION_SECRET`, and `getActuateConfig()?.secret` (Bugbot review on PR #43, Medium). Previously the validator only inspected `process.env.CMS_SECRET`, so a deploy that configured the secret via `actuate.config.ts` got the contradictory response `secretConfigured: true` AND `status: "unhealthy"` — false alarm that would trip Kubernetes readiness probes / load-balancer health checks. The validator's `'missing'` message now also names the config-file path so the diagnostic matches `getSessionSecret()`'s own behavior.
|
|
1394
|
+
|
|
1395
|
+
**Documentation**
|
|
1396
|
+
- `README.md`: filled in with project overview, repo layout, dev quickstart, Windows long-path setup instructions, env-var reference, and release flow.
|
|
1397
|
+
- `.cursor/rules/security.mdc`: corrected to say `sanitize-html` (matches code) instead of `isomorphic-dompurify`.
|
|
1398
|
+
- `.cursor/rules/architecture.mdc`: documents the `getActuateConfig()` accessor and forbids direct `globalThis` reads.
|
|
1399
|
+
|
|
1400
|
+
**Tests** (+ 80 net new tests; 525 total passing across cms-core)
|
|
1401
|
+
- `__tests__/auth/oauth.test.ts` (29): PKCE primitives, state JWT round-trip, nonce binding, provider mismatch rejection, password-protected account refusal, self-signup gating, OAuthAccount-based linking, session creation.
|
|
1402
|
+
- `__tests__/auth/reset.test.ts` (17): single-use tokens, expiry, deactivated-user rejection, password policy enforcement, atomic update + session revocation, replay protection.
|
|
1403
|
+
- `__tests__/diagnostics/env.test.ts` (12): shape validation across every required and optional env var, plus 3 parametrized regression cases asserting "ok" messages do not include exact lengths for `CMS_SECRET`, `CMS_ENCRYPTION_KEY`, or `CRON_SECRET`.
|
|
1404
|
+
- `__tests__/diagnostics/logger.test.ts` (7): level filtering, JSON output, prefix formatting.
|
|
1405
|
+
- `__tests__/security/rate-limit.test.ts` (+2): LRU cap eviction and most-recently-used preservation.
|
|
1406
|
+
- `__tests__/api/health.test.ts` (5): `/health` returns `unhealthy` (not `degraded`) when env is broken AND db throws; `/health` returns `degraded` when only db throws and env is fine; `/health` JSON contains no `\d+\s*chars?` pattern (length-leak regression); `/health` does not report `unhealthy` when the secret is supplied via `CMS_SESSION_SECRET` (legacy env); `/health` does not report `unhealthy` when the secret is supplied via `actuate.config.ts → secret` (config-file path).
|
|
1407
|
+
|
|
1408
|
+
**Internal**
|
|
1409
|
+
- `turbo.json`: explicit `outputs: []` for `@actuate-media/update-server#build` (it runs `tsc --noEmit`, no artifacts).
|
|
1410
|
+
- Migrated `console.*` calls in `rate-limit`, `audit`, and `oauth` to the new structured logger.
|
|
1411
|
+
- Replaced `@ts-ignore` with `@ts-expect-error` on the streaming `RequestInit` cast.
|
|
1412
|
+
|
|
1413
|
+
## 0.12.0
|
|
1414
|
+
|
|
1415
|
+
### Minor Changes
|
|
1416
|
+
|
|
1417
|
+
- 370fa95: Security hardening (engineering-audit High items):
|
|
1418
|
+
- **SSRF DNS-rebinding defense**: `safeFetch` now resolves the hostname before fetching and rejects when any A/AAAA record lands in a private range. Previously the URL was only validated as written, so an attacker controlling DNS for `evil.tld` could return a public IP at validation time and `127.0.0.1` at fetch time.
|
|
1419
|
+
- **SSRF IP canonicalization rewrite**: replaced regex-based hostname matching in `validateWebhookUrl` with a proper IP canonicalizer (`security/ip-canon.ts`). Now blocks every encoding `getaddrinfo` accepts — decimal (`http://2130706433`), octal (`http://0177.0.0.1`), hex (`http://0x7f.0.0.1`), short-form (`http://127.1`), IPv4-mapped IPv6 (`http://[::ffff:127.0.0.1]`), bracketed IPv6, and the carrier-grade NAT range (`100.64.0.0/10`). Cloud-metadata hostnames (`metadata.google.internal`, `*.internal`) are also rejected explicitly.
|
|
1420
|
+
- **AES-256 key validation**: `encryptField` / `decryptField` now throw `InvalidEncryptionKeyError` at the call site if `CMS_ENCRYPTION_KEY` isn't exactly 64 hex chars. Previously a misconfigured key (e.g. the placeholder `aes256-local-dev-key-change-in-prod`) only surfaced as an opaque WebCrypto error the first time a field was written — late, in production, hard to diagnose.
|
|
1421
|
+
- **JWT payload shape validation**: `verifySession` and OAuth `verifyState` now assert the decoded payload has the expected fields (`userId`, `role`, `sessionId`) and strip extra attacker-supplied claims before returning. Throws `InvalidSessionPayloadError` / `InvalidOAuthStateError` on malformed payloads.
|
|
1422
|
+
- **Login user-enumeration timing defense**: when the email doesn't exist OR the account is OAuth-only, the login handler now still runs PBKDF2 against a dummy hash before returning the error. Previously the response-time delta let attackers enumerate valid emails.
|
|
1423
|
+
- **PBKDF2 strength bump**: hash iterations raised from 100,000 → 600,000 (OWASP / NIST 2023+ guidance for PBKDF2-HMAC-SHA256). The hash format embeds the iteration count, so old 100k hashes still verify; `verifyPassword` reads the count from the stored string. Login opportunistically re-hashes weak stored hashes after a successful sign-in.
|
|
1424
|
+
- **Cron endpoints**: added `/api/cms/cron/publish`, `/api/cms/cron/cleanup`, `/api/cms/cron/seo-scan` matching the documented Vercel cron paths. Auth is via `Authorization: Bearer ${CRON_SECRET}` (constant-time compare, fail-closed when the env var is unset). Cleanup hard-deletes expired sessions, old audit logs (90d), trashed documents (30d), and used password reset tokens (1d).
|
|
1425
|
+
|
|
1426
|
+
New tests: 99 added across `auth/password`, `auth/session`, `security/encrypted-fields`, `security/ssrf`, `security/safe-fetch`, and `cron/`.
|
|
1427
|
+
|
|
1428
|
+
### Patch Changes
|
|
1429
|
+
|
|
1430
|
+
- 370fa95: Security follow-ups from the Bugbot review of PR #40 (engineering-audit High items):
|
|
1431
|
+
- **SSRF — malformed IP literals now fail closed.** `canonicalizeHostname()` previously returned `{isHostname: false}` for IP-shaped strings that failed to parse (e.g. `::1::1`, `300.0.0.1`) without setting `ipv4`/`ipv6`. `validateWebhookUrl` and `safeFetch.resolveAndCheck` then treated that as "valid public IP, no DNS lookup needed" and let the request through. The canonicalizer now exposes an explicit `isValidIp` flag and the SSRF callsites reject malformed IP literals up front.
|
|
1432
|
+
- **Login timing — dummy PBKDF2 hash is now eager.** `compareToDummyHash` previously initialized the dummy hash lazily on first call, so the first unknown-email login after a cold start was ~1s slower than subsequent ones, partially defeating the timing-side-channel defense added in the original High fix. The dummy hash is now precomputed at module load (top-level Promise) and `compareToDummyHash` awaits it.
|
|
1433
|
+
- **Cron auth — constant-time compare no longer leaks secret length.** `isAuthorizedCronRequest` compared the bearer token to `CRON_SECRET` after an early `if (a.length !== b.length) return false`, which made the response time depend on the submitted token's length. Replaced with HMAC-SHA256 of both inputs (always 32 bytes) compared via `node:crypto.timingSafeEqual` against a per-process random key.
|
|
1434
|
+
- **Cron endpoints — accept GET (Vercel Cron) in addition to POST.** Vercel Cron sends GET requests by default. The original implementation registered the cron paths as POST-only, so every Vercel-scheduled invocation would have returned 404/405 and silently skipped. Endpoints are now registered for both GET and POST so Vercel Cron and self-hosted POST schedulers (k8s CronJob, GH Actions `curl -X POST`, EventBridge) both work.
|
|
1435
|
+
- **Cron `modelExists` correctly rejects null delegates.** The guard used `typeof db[name] === 'object'`, which is `true` for `null` (the historical `typeof null === 'object'` quirk). With `{ session: null }` the guard returned true and the subsequent `db.session.deleteMany(...)` threw a TypeError that the outer try/catch only papered over. Now explicitly checks `delegate !== null`.
|
|
1436
|
+
- **Cron SEO scan H1 detection is case-insensitive.** The image-missing-alt scan uses `/gi`, but the H1-presence check used `content.includes('<h1')`, so documents with valid `<H1>` headings were falsely flagged as missing. Replaced with `/<h1\b/i.test(content)` to match the HTML spec and the rest of the function's behavior.
|
|
1437
|
+
- **Cron SEO scan alt-text check is case-insensitive.** Sibling bug to the H1 fix above: the `<img>` regex was `/gi` (case-insensitive), but the `alt=` substring test inside the filter was case-sensitive (`img.includes('alt=')`), so `<IMG ALT="text">` matched the regex and was then falsely counted as missing alt text. Replaced with `/\balt\s*=/i.test(img)` so all uppercase / mixed-case attribute spellings are recognized.
|
|
1438
|
+
|
|
1439
|
+
New tests:
|
|
1440
|
+
- `security/ssrf.test.ts` — 9 cases covering malformed IPv4/IPv6 literals, embedded-null hostnames, oversize labels, and `validateWebhookUrl`/`resolveAndCheck` rejecting them.
|
|
1441
|
+
- `auth/password.test.ts` — regression assertion that the first `compareToDummyHash` call is not significantly slower than later calls.
|
|
1442
|
+
- `cron/cron.test.ts` — 5 cases covering varying-length `CRON_SECRET` attempts (constant-time guarantee), plus 1 regression for null model delegates, 4 parametrized cases for case-insensitive H1 detection, and 5 parametrized cases for case-insensitive alt-text detection.
|
|
1443
|
+
- `api/cron-routes.test.ts` — 12 cases covering GET (Vercel Cron path) and POST for all three cron endpoints, plus 401 paths.
|
|
1444
|
+
|
|
1445
|
+
## 0.11.2
|
|
1446
|
+
|
|
1447
|
+
### Patch Changes
|
|
1448
|
+
|
|
1449
|
+
- 6d2ea6e: Two fixes that emerged while stabilizing the e2e suite:
|
|
1450
|
+
- **cms-core**: `createRateLimiter()` now respects `ACTUATE_DISABLE_RATE_LIMIT=1` and returns a permissive limiter that always allows requests. This is for test harnesses where every test shares the same client IP and exhausts the strict per-IP buckets within the first few login attempts. Production never sets this var; the deploy guide and Vercel templates omit it. Previously the env var bypass only existed at the per-handler `checkRateLimitAsync` wrapper in `handlers.ts`, so the global API rate limit in `handler-factory.ts` (which uses `rateLimiter.check()` directly) ignored it and broke retries with HTTP 429.
|
|
1451
|
+
- **cms-admin**: `<SEO />` view crashed with "M.filter is not a function" the first time it rendered. The `/seo/link-health` endpoint returns `{ truncated, checked, issues: [...] }` (an object), but `useApiData<any[]>` typed it as an array and the `linkHealth = linkHealthData ?? []` coercion silently put the wrapper object into the `linkHealth` variable. Calls like `linkHealth.filter(l => l.status === 404)` then threw on every render — including when the user was on the (default) Pages tab, because Radix `<Tabs.Content>`'s children are evaluated eagerly even when the tab is unmounted. Fixed the typing and switched to `linkHealthData?.issues ?? []`.
|
|
1452
|
+
|
|
1453
|
+
## 0.11.1
|
|
1454
|
+
|
|
1455
|
+
### Patch Changes
|
|
1456
|
+
|
|
1457
|
+
- 22c314b: Add `ACTUATE_DISABLE_RATE_LIMIT=1` env-var escape hatch for test
|
|
1458
|
+
harnesses. Production never sets it; the wired bypass lives at the
|
|
1459
|
+
single `checkRateLimitAsync` chokepoint in `api/handlers.ts`.
|
|
1460
|
+
|
|
1461
|
+
Why: the e2e Playwright suite shares a single `login:unknown` rate
|
|
1462
|
+
limiter bucket across all 12 specs (the dev server doesn't set
|
|
1463
|
+
`X-Forwarded-For`, so `getClientIp()` returns `'unknown'`). The strict
|
|
1464
|
+
5-attempts-per-15-min login limiter exhausts after the first three
|
|
1465
|
+
specs, causing every subsequent test to fail with "Too many login
|
|
1466
|
+
attempts." This was masked for months by an unrelated `pnpm
|
|
1467
|
+
format:check` CI failure that gated the whole `e2e` job.
|
|
1468
|
+
|
|
1469
|
+
The bypass is keyed off an explicit env var rather than `NODE_ENV ===
|
|
1470
|
+
'test'` because some Next.js build steps run with `NODE_ENV=test` and
|
|
1471
|
+
we want the rate limiter exercised by unit tests that don't opt in.
|
|
1472
|
+
|
|
1473
|
+
## 0.11.0
|
|
1474
|
+
|
|
1475
|
+
### Minor Changes
|
|
1476
|
+
|
|
1477
|
+
- 6db988e: Major engineering quality + integration upgrades.
|
|
1478
|
+
|
|
1479
|
+
**Security & reliability (cms-core)**
|
|
1480
|
+
- Audit log: fixed runtime crash from `orderBy: createdAt` → `timestamp`; never throws on insert; allows null `details`.
|
|
1481
|
+
- IPv4 + IPv6 + CIDR allowlist with canonical comparison; reject `unknown` IPs against non-empty allowlists.
|
|
1482
|
+
- Centralised trusted client IP extraction via `x-vercel-forwarded-for` / `x-real-ip` / `x-forwarded-for` (with explicit `trustProxy`).
|
|
1483
|
+
- Atomic Upstash `INCR` + `EXPIRE NX` rate limiter, fail-open with logging.
|
|
1484
|
+
- Encryption at rest for OAuth tokens, TOTP secrets, backup codes, and webhook secrets via `CMS_ENCRYPTION_KEY`.
|
|
1485
|
+
- Hardened TOTP login: `mfa_pending` JWT + per-IP/per-user rate limit + request fingerprint binding (no userId leak between steps).
|
|
1486
|
+
- Reauth required for TOTP setup/disable; revoke other sessions on TOTP changes.
|
|
1487
|
+
- OAuth state nonce bound to a cookie; `allowSelfSignup` flag; never silently link to password-protected accounts.
|
|
1488
|
+
- SSRF-safe `safeFetch()` for webhooks and `/seo/link-health`; concurrency cap + URL count cap; private IPs/loopback/link-local rejected.
|
|
1489
|
+
- Magic-byte + MIME validation + DOMPurify SVG sanitisation on `/media/upload`; tightened WebP/AVIF/GIF/SVG detectors.
|
|
1490
|
+
- Webhook + plugin hook dispatch moved outside the DB transaction so a rollback never fires events for a doc that no longer exists.
|
|
1491
|
+
- AI: per-user rate limit + token/cost budget + input length cap + balanced-brace JSON extractor + `validateTree` after every step + secret redaction in audit prompts.
|
|
1492
|
+
- CSRF exemption tightened to exact-prefix match on the rewritten path (no more `pathname.includes`).
|
|
1493
|
+
- `refreshSession`: validate session row exists & is active before re-issuing.
|
|
1494
|
+
- Public SEO endpoints require auth; `/llms.txt` filters PUBLISHED + non-deleted.
|
|
1495
|
+
- `/forms`, `/forms/:id/submissions`, `/search/global` users restricted to EDITOR+.
|
|
1496
|
+
- Open-redirect host allowlist for `/redirects`; folder routes validate scope on move/delete; password-reset email enumeration blocked.
|
|
1497
|
+
- `/script-tags/resolve` returns proper public cache headers.
|
|
1498
|
+
|
|
1499
|
+
**Integration fixes (consumer feedback)**
|
|
1500
|
+
- **`createServerSiteClient(prisma, config)`** — new server-only Site client that reads from Prisma directly. Use in Server Components, `generateMetadata`, route loaders. Avoids the `Invalid URL` crash that `createSiteClient()` throws under bare Node, and skips the self-fetch overhead.
|
|
1501
|
+
- **`createSiteClient()`** now auto-resolves `baseUrl` from `NEXT_PUBLIC_SITE_URL` or `VERCEL_URL` when not explicitly provided.
|
|
1502
|
+
- **`/public/globals/:slug`** defaults to public when no `access.read` is set (was a 403 footgun).
|
|
1503
|
+
- **`cmsApi`** auto-bootstraps the `actuate_csrf` cookie before non-GET requests; `AdminRoot` also primes it on mount when a session exists. Fixes the silent 403 every consumer hit when signing in via a custom Server Action.
|
|
1504
|
+
- **AdminShell** routes collections with `type: 'page'` through `<PageBuilder>` instead of `<DocumentEdit>`. Built-in admin paths (`/users`, `/media`, etc.) are reserved before collection routes so a user-defined `users` collection can't shadow the Users view.
|
|
1505
|
+
- **`BlockCatalog`** now reads custom blocks from `globalThis.__actuateConfig.pageBuilder.blocks` automatically. Also accepts an explicit `customBlocks` option and an `includeCore: false` flag.
|
|
1506
|
+
- **Page Builder envelope** unified at the top level — `layout` and `pageSettings` are accepted on POST/PUT and returned at top-level on GET. Storage shape is `data._layout` / `data._pageSettings`; both wire shapes accepted on input for backwards compatibility.
|
|
1507
|
+
- **`/media/upload`** now uses the configured platform storage adapter (e.g. `platform-vercel`) when set; honors `token`/`prefix` options. Falls back to direct `@vercel/blob` import for legacy installs.
|
|
1508
|
+
- **BlockEditor** field types fleshed out: real TipTap editor for `richText`, working media picker for `media`, JSON editor with validation for `json`, plus `slug`, `date`, and an array editor for primitive arrays.
|
|
1509
|
+
- **`pageBuilder.previewTheme`** now applied to the canvas via scoped CSS variables.
|
|
1510
|
+
- OpenAPI: documented `/public/globals/:slug` and `/resolve`.
|
|
1511
|
+
|
|
1512
|
+
**DX**
|
|
1513
|
+
- `.env.example` documents required formats for `CMS_ENCRYPTION_KEY` (64-hex), `BLOB_READ_WRITE_TOKEN`, and `RESEND_*`.
|
|
1514
|
+
- Tests: 266 passing (was 230); regression tests for IP allowlist, client-ip, redact, secret-storage, audit, upload-magic, server-site, custom block discovery.
|
|
1515
|
+
|
|
1516
|
+
## 0.10.4
|
|
1517
|
+
|
|
1518
|
+
### Patch Changes
|
|
1519
|
+
|
|
1520
|
+
- 58bf2e6: Add site-wide SEO robots defaults with per-page index/follow override controls.
|
|
1521
|
+
|
|
1522
|
+
## 0.10.3
|
|
1523
|
+
|
|
1524
|
+
### Patch Changes
|
|
1525
|
+
|
|
1526
|
+
- e2fabee: Add AI-friendly deployment diagnostics, align generated schemas with first-run admin features, and harden setup guidance for design-first deployments.
|
|
1527
|
+
|
|
1528
|
+
## 0.10.2
|
|
1529
|
+
|
|
1530
|
+
### Patch Changes
|
|
1531
|
+
|
|
1532
|
+
- b66800c: Align admin-facing API response shapes with cms-admin consumers, add Vercel Blob image configuration, and harden admin views against partial data.
|
|
1533
|
+
|
|
1534
|
+
## 0.10.1
|
|
1535
|
+
|
|
1536
|
+
### Patch Changes
|
|
1537
|
+
|
|
1538
|
+
- b224105: Fix post-release developer experience regressions in public site globals, scaffolded Prisma wiring, and seed/populate database initialization.
|
|
1539
|
+
|
|
1540
|
+
## 0.10.0
|
|
1541
|
+
|
|
1542
|
+
### Minor Changes
|
|
1543
|
+
|
|
1544
|
+
- 266b657: Add first-class public site helpers, CLI init/populate workflows, and generated frontend boilerplate for globals, document resolution, and media normalization.
|
|
1545
|
+
|
|
1546
|
+
## 0.9.0
|
|
1547
|
+
|
|
1548
|
+
### Minor Changes
|
|
1549
|
+
|
|
1550
|
+
- c3c55c7: Add AI-powered page generation pipeline with prompt-to-page workflow.
|
|
1551
|
+
|
|
1552
|
+
**cms-core**: AI pipeline types (GenerationContext, GenerationStep, StepResult, GenerationResult), prompt template builders for structure/content/SEO/accessibility steps, deterministic accessibility auto-fix engine (auditAccessibility, fixAccessibility), and API endpoints for /page-builder/generate, /page-builder/generate-block, /page-builder/audit-a11y, and /page-builder/fix-a11y.
|
|
1553
|
+
|
|
1554
|
+
**plugin-ai**: Multi-step generatePage() pipeline that runs structure → content → SEO → accessibility steps sequentially via AIProvider, with validation retry, graceful fallback, progress callbacks, and token tracking. Single-block generateBlockContent() for context-aware content generation.
|
|
1555
|
+
|
|
1556
|
+
**cms-admin**: AI Generate Dialog with prompt input, tone selection, step toggles, business context fields, real-time generation progress, and step-by-step review with accept/regenerate flow. AI Block Assist button for one-click content generation per block. AI button integrated into the builder toolbar.
|
|
1557
|
+
|
|
1558
|
+
- 2ef3505: Add SEO analysis and design scoring for the page builder. cms-core gains `analyzeSEO()` (content extraction, readability, 15+ SEO checks, per-block hints), `analyzeDesign()` (6-category scoring for visual hierarchy, spacing consistency, content density, color usage, image quality, and mobile readiness), and semantic block intelligence with smart suggestions. cms-admin replaces the placeholder SEO and Design tabs in the page builder ContextPanel with fully functional `BuilderSEOPanel` (score ring, checks, readability grid, SERP/social previews, inline editing) and `DesignScorePanel` (score ring, category progress bars, expandable checks, severity-ranked suggestions).
|
|
1559
|
+
- 2269f10: Add page templates and saved sections for the page builder. cms-core gains 6 built-in page templates (blank, landing, about, contact, location, blog-post) with full container tree structures, plus CRUD API endpoints for both page templates and saved sections with tree validation, auto-seeding, and audit logging. cms-admin adds a TemplatePicker modal for new page creation, a SavedSections CRUD management view with category filtering, and admin routing for the new views.
|
|
1560
|
+
|
|
1561
|
+
## 0.8.0
|
|
1562
|
+
|
|
1563
|
+
### Minor Changes
|
|
1564
|
+
|
|
1565
|
+
- e19430e: Add page builder foundation: container tree types, Zod validation schema, tree manipulation utilities, unified block catalog with 10 core types and named variants, and Prisma models for page templates and saved sections. New `pageBuilder` config option on `ActuateCMSConfig`. New `./page-builder` export path.
|
|
1566
|
+
|
|
1567
|
+
## 0.7.0
|
|
1568
|
+
|
|
1569
|
+
### Minor Changes
|
|
1570
|
+
|
|
1571
|
+
- b5de8dd: Add password reset flow for admin login. Users click "Forgot Password?" on the login page, enter their email, and receive a time-limited reset link via the configured email adapter (Resend). The reset token is hashed before storage and expires after 1 hour. On successful reset, all existing sessions are revoked.
|
|
1572
|
+
- New PasswordResetToken Prisma model with migration 0005_password_reset_tokens.
|
|
1573
|
+
- POST /auth/forgot-password and POST /auth/reset-password API endpoints with rate limiting.
|
|
1574
|
+
- Core auth module (reset.ts) with token generation, email dispatch, and password update logic.
|
|
1575
|
+
- ForgotPassword and ResetPassword admin views with full form validation.
|
|
1576
|
+
- AdminRoot routes /forgot-password and /reset-password in the unauthenticated shell.
|
|
1577
|
+
|
|
1578
|
+
## 0.6.0
|
|
1579
|
+
|
|
1580
|
+
### Minor Changes
|
|
1581
|
+
|
|
1582
|
+
- 54c7a3b: Add Script Tags management system for injecting tracking codes, analytics, and custom scripts into public site pages. Admins create tags with HTML/JS code, choose placement (head, body open, body close), set scope (entire site, parent pages + descendants, or exact URLs), and assign priority order. Includes new ScriptTag database model, CRUD + public resolve API endpoints, admin list and editor views, and updated scaffolder site layout with automatic tag rendering.
|
|
1583
|
+
|
|
1584
|
+
## 0.5.0
|
|
1585
|
+
|
|
1586
|
+
### Minor Changes
|
|
1587
|
+
|
|
1588
|
+
- c08cf8b: Add layout variants system for customizable per-page headers, footers, and other layout regions. Layout regions are defined in config, stored as regular collection documents, and assigned to pages with automatic parent-to-child URL path inheritance. The /resolve endpoint returns resolved layout variants alongside page data. Admin UI includes layout assignment on document edit, collection grouping in sidebar, and default layout settings.
|
|
1589
|
+
|
|
1590
|
+
### Patch Changes
|
|
1591
|
+
|
|
1592
|
+
- 80aa939: Add scaffold site rendering layer with catch-all route, block components, layout components, and critical config wiring fixes.
|
|
1593
|
+
- Fix API route template to pass config to handleActuateAPI (enables /resolve and layout regions)
|
|
1594
|
+
- Fix admin page template to pass real config to AdminRoot (enables layout sidebar and blocks field)
|
|
1595
|
+
- Fix /resolve endpoint to handle root path (/) by looking up "home" or "index" slug
|
|
1596
|
+
- Add blocks-first page fields, header/footer collections, and layout config to scaffold (conditional on blocks plugin)
|
|
1597
|
+
- Generate catch-all page route with dynamic SEO metadata and first-boot welcome page
|
|
1598
|
+
- Generate BlockRenderer, HeroBlock, CTABlock, TextBlock, ImageTextBlock components
|
|
1599
|
+
- Generate SiteHeader and SiteFooter layout components with graceful fallbacks
|
|
1600
|
+
|
|
1601
|
+
## 0.4.0
|
|
1602
|
+
|
|
1603
|
+
### Minor Changes
|
|
1604
|
+
|
|
1605
|
+
- f2cf7d7: Redesign Dashboard and Pages views: extended /stats API with statusCounts, formCount, avgSeoScore, and author names; new /seo/summary endpoint; recharts donut chart for content overview; SEO performance panel; folder color badges and SEO score column in Pages view.
|
|
1606
|
+
|
|
1607
|
+
## 0.3.1
|
|
1608
|
+
|
|
1609
|
+
### Patch Changes
|
|
1610
|
+
|
|
1611
|
+
- 0621037: Make Dashboard collection-aware instead of hardcoding Posts/Pages labels.
|
|
1612
|
+
|
|
1613
|
+
**cms-core:**
|
|
1614
|
+
- `/stats` API now returns `collectionCounts` (per-collection document counts via groupBy)
|
|
1615
|
+
- Recent documents include `collection` field in response
|
|
1616
|
+
|
|
1617
|
+
**cms-admin:**
|
|
1618
|
+
- Dashboard reads `config.collections` to derive stat card labels, counts, routes, and quick actions
|
|
1619
|
+
- "Edit" links use `/{doc.collection}/{doc.id}` instead of hardcoded `/posts/{id}`
|
|
1620
|
+
- "New" quick actions are generated from configured collections
|
|
1621
|
+
- Falls back to generic "Documents" / "Pages" labels when no config is provided
|
|
1622
|
+
|
|
1623
|
+
## 0.3.0
|
|
1624
|
+
|
|
1625
|
+
### Minor Changes
|
|
1626
|
+
|
|
1627
|
+
- 4e3a5eb: RC2-RC4 and hardening release.
|
|
1628
|
+
|
|
1629
|
+
**cms-core:**
|
|
1630
|
+
- Replace isomorphic-dompurify with sanitize-html (eliminates Vercel ESM crash)
|
|
1631
|
+
- Remove cms-core from serverExternalPackages, move to transpilePackages
|
|
1632
|
+
- Add GET /api/cms/health endpoint with model probing and diagnostics
|
|
1633
|
+
- Harden db() Proxy with try/catch on property access and rawDb()
|
|
1634
|
+
- Harden safeCount/safeFindMany against Proxy throws
|
|
1635
|
+
- Add startup validation for missing CMS_SECRET (503 instead of 500)
|
|
1636
|
+
- Bypass auth on /stats when secret is missing (returns zero-data)
|
|
1637
|
+
- Loosen NextConfigLike types (experimental/images/webpack as unknown)
|
|
1638
|
+
- Implement login anomaly detection (new IP, brute force, unusual hours)
|
|
1639
|
+
- Implement webhook DNS resolution with private range checking
|
|
1640
|
+
- Implement TOTP reauth verification
|
|
1641
|
+
- Add Zod validation for secret in defineConfig (min 32 chars)
|
|
1642
|
+
- Read CMS_CORE_VERSION from package.json at runtime instead of hardcoding '0.1.0'
|
|
1643
|
+
|
|
1644
|
+
**cms-admin:**
|
|
1645
|
+
- Ship pre-compiled CSS (dist/actuate-admin.css) with all 48 responsive classes
|
|
1646
|
+
- New export: @actuate-media/cms-admin/styles/precompiled.css
|
|
1647
|
+
- Add CSS containment (`contain: layout style`) on .actuate-admin
|
|
1648
|
+
- Fix ShortcutHelp modal using React state instead of DOM manipulation
|
|
1649
|
+
- Add fixed-position layout isolation wrapper to AdminRoot
|
|
1650
|
+
- Add useApiData retry limits (maxRetries=3) with exponential backoff
|
|
1651
|
+
- Dashboard shows health-aware "Database Setup Required" banner
|
|
1652
|
+
|
|
1653
|
+
**cli:**
|
|
1654
|
+
- Add `actuate db:init` command (injects CMS models into Prisma schema)
|
|
1655
|
+
- Add `actuate db:status` command (checks model presence and DB connectivity)
|
|
1656
|
+
|
|
1657
|
+
**create-actuate-cms:**
|
|
1658
|
+
- Embed full CMS schema (9 models + enum) in generated schema.prisma
|
|
1659
|
+
- Add @actuate-media/cli to scaffolded devDependencies
|
|
1660
|
+
- Use pre-compiled CSS import in scaffolded admin layout
|
|
1661
|
+
|
|
1662
|
+
## 0.2.3
|
|
1663
|
+
|
|
1664
|
+
### Patch Changes
|
|
1665
|
+
|
|
1666
|
+
- Fix /stats endpoint: use Promise.allSettled so missing Prisma models return 0 instead of 500. Dashboard now shows a soft warning instead of a red error banner when stats are unavailable. Add !important to font-family on .actuate-admin to prevent consumer site fonts from cascading into the admin panel.
|
|
1667
|
+
|
|
1668
|
+
## 0.2.2
|
|
1669
|
+
|
|
1670
|
+
### Patch Changes
|
|
1671
|
+
|
|
1672
|
+
- Loosen withActuateCMS types: accept minimal config objects without requiring full CollectionDefinition (labels, fields). Replace Partial<ActuateCMSConfig> with WithActuateCMSOptions and NextConfigWithActuate with NextConfigLike that aligns with Next.js's NextConfig shape. Consumers no longer need `as unknown` casts.
|
|
1673
|
+
|
|
1674
|
+
## 0.2.1
|
|
1675
|
+
|
|
1676
|
+
### Patch Changes
|
|
1677
|
+
|
|
1678
|
+
- Fix Turbopack conflict: remove cms-core from transpilePackages since it's already in serverExternalPackages. Turbopack forbids a package in both lists. Also add a safety filter so any overlap between the two lists is resolved in favor of serverExternalPackages.
|
|
1679
|
+
|
|
1680
|
+
## 0.2.0
|
|
1681
|
+
|
|
1682
|
+
### Minor Changes
|
|
1683
|
+
|
|
1684
|
+
- 682dac7: Fix consumer integration issues discovered during MaidPro.co deployment
|
|
1685
|
+
- **cms-core**: API handlers gracefully degrade when CMS Prisma models are missing (returns 501 instead of 500)
|
|
1686
|
+
- **cms-core**: Accept CMS secret via `actuate.config.ts` (`secret` field) in addition to `CMS_SECRET` env var
|
|
1687
|
+
- **cms-core**: `withActuateCMS` now accepts `Partial<ActuateCMSConfig>` (no more forced type casts)
|
|
1688
|
+
- **cms-core**: `withActuateCMS` auto-injects all `@actuate-media/*` packages into `transpilePackages`
|
|
1689
|
+
- **cms-core**: Expanded Prisma schema fragment with all CMS models for consumer merge
|
|
1690
|
+
- **cms-admin**: Removed duplicate Tailwind import from admin styles (consumers already load Tailwind)
|
|
1691
|
+
- **platform-vercel**: Email adapter accepts both `defaultFrom` and `from` for sender address
|
|
1692
|
+
- **plugin-email**: Email plugin accepts both `defaultFrom` and `from` for consistency
|
|
1693
|
+
|
|
1694
|
+
### Patch Changes
|
|
1695
|
+
|
|
1696
|
+
- 3fd38a2: Fix all 13 integration issues discovered during MaidPro.co deployment
|
|
1697
|
+
|
|
1698
|
+
**cms-core:**
|
|
1699
|
+
- Add .js extensions to all ESM relative imports (58 files)
|
|
1700
|
+
- Replace workspace:\* with versioned deps in all packages
|
|
1701
|
+
- Add sideEffects:false for proper tree-shaking
|
|
1702
|
+
- Subpath exports (/config, /api, /middleware, /next) avoid barrel side effects
|
|
1703
|
+
- Stats/search routes use safeCount/safeFindMany for missing Prisma models
|
|
1704
|
+
- Ship prisma/cms-schema.prisma with all required CMS models
|
|
1705
|
+
- Accept both CMS_SECRET and CMS_SESSION_SECRET with clear error messages
|
|
1706
|
+
- Fix hardcoded /admin paths in OAuth flow (now uses getAdminPath())
|
|
1707
|
+
- Loosen withActuateCMS and createCMSMiddleware parameter types
|
|
1708
|
+
- Auto-inject transpilePackages and serverExternalPackages in withActuateCMS
|
|
1709
|
+
- Unify PluginDefinition with ActuatePlugin interface
|
|
1710
|
+
|
|
1711
|
+
**cms-admin:**
|
|
1712
|
+
- Scope all 40+ CSS theme variables to .actuate-admin instead of :root
|
|
1713
|
+
- ThemeProvider applies dark class to .actuate-admin, not document.documentElement
|
|
1714
|
+
- Scaffold uses (admin)/(site) route groups for full CSS/script isolation
|
|
1715
|
+
- Sidebar branding supports custom logo via config.admin.branding
|
|
1716
|
+
|
|
1717
|
+
**platform-vercel:**
|
|
1718
|
+
- Email adapter accepts both `from` and `defaultFrom`
|
|
1719
|
+
|
|
1720
|
+
**All plugin/platform packages:**
|
|
1721
|
+
- Fix ESM .js extensions in compiled output
|
|
1722
|
+
- Replace workspace:\* with ^0.1.0 for publishable deps
|