Apostrophe 4.33.0: Free and improved version history, in-context field editing, and AI connectors in core #5637
BoDonkey
announced in
Release Notes
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Hello Apostrophe Community!
Apostrophe 4.33.0 brings three major improvements to the editing and development experience. Version history, called Document Versions in Apostrophe, has moved from a Pro extension into the open source core. It's also been improved to show a more detailed view of changes. String and rich text fields can now be edited directly on the page, wherever your JSX, Nunjucks or Astro templates render them. Core also gains a shared, provider-agnostic AI engine for building AI-powered features across Apostrophe. This release also includes important improvements to JSX, logging, and security, so we recommend updating promptly.
Version history: now open source and part of core
document-versions.mp4
Document version history, previously the Pro
@apostrophecms-pro/document-versionsextension, is now open source and built into Apostrophe core as@apostrophecms/document-versions. There is nothing extra to install and no license required. It is on for pages and pieces by default, and can be configured per type with theversionsoption.It has also been reworked to show a much clearer view of what’s changed. The new interface makes it much easier to see what changed in each version, who made the change, and when. Drafts are now recorded as well as published versions, including whether AI was involved, and images and files have their own version history in the media manager. See the Document Versions guide for a full tour. Accessing this feature will work the same for editors, but experience once they open the Document Versions modal looks quite different.
If your project uses the extension, remove it when you upgrade. If you upgrade both, the extension will print a polite warning and do nothing since the feature is now in core. Your existing history is converted automatically when the site first starts. Some configuration options, REST API routes and methods have changed, so see the migration guide for the full steps, including notes for rolling deployments.
In-Context Field Editing
Areas have always been editable right where they appear on the page. Now ordinary schema fields can be too. This change makes it much easier for developers to deliver a true in-context visual editing experience for schema fields. Instead of opening the editor modal for a hero to change a headline, an editor clicks the headline on the page and types over it. Existing
stringfields work this way, as do fields of the newrichTexttype, and custom field types can opt in.This is a template choice, not a schema change. When a template renders a field with the new field tag, that field becomes editable where it appears. Fields rendered the usual way behave exactly as before, and every field stays in the editor modal and the REST API. The one exception is Astro: because it receives its data before its templates run, each field it edits in place also needs
wysiwyg: truein the schema.For example, take a widget with an ordinary
stringfield:Rendering it with
Fieldin the widget's JSX template makes the headline editable right on the page:The Nunjucks equivalent is
{% field data.widget, 'headline' with { tag: 'h1' } %}, and in Astro it is<AposField doc={widget} name="headline" tag="h1" />, withwysiwyg: trueadded to the schema field.In edit mode, the editor takes on the page's own typography and takes up no more room than the text it replaces, so editing feels like typing on the page rather than filling in a form. Outside edit mode, only the value is rendered, so the tag is safe to use anywhere. See the inline editing guide for the details.
The new
richTextschema field type also means rich text no longer has to live in a widget inside an area. It uses the same editor, sanitization rules, and configuration as the rich text widget, so any customizations you've already made apply automatically.AI Connectors in Core
Apostrophe now includes
apos.ai, a provider-agnostic API for AI text and image generation. Features can be written once against a shared interface, while switching between Anthropic, OpenAI, Google, OpenAI-compatible services, or a local Ollama is handled through configuration rather than feature-specific integrations.This connector engine is opt-in: no provider or API key is configured by default. It also provides shared infrastructure for tool calling, permissions, background jobs, retries, testing, and content extraction, giving current and future AI features a consistent way to work with Apostrophe content.
Our Pro modules are the first to use it:
richTextfields. Automatic translation still supports traditional providers likedeeplas well.package.jsonto get it) now generates throughapos.aiand no longer requires Automatic Translation. Existing projects should review the migration notes in the module README.JSX Across Our Documentation and Starter Kits
When we introduced JSX templates in Apostrophe 4.31.0, we said we would update our documentation over time to show JSX as the preferred authoring path. The documentation now leads with JSX, and our starter kits use JSX templates as well, so we recommend starting new projects with JSX. Nunjucks remains fully supported, and the two can coexist in the same project, so existing sites can keep their templates as they are or migrate one at a time.
Security
This release fixes 22 responsibly disclosed security vulnerabilities across core and several supporting packages, three of them rated high severity. We recommend all users upgrade promptly. This release addresses:
sanitize-html, Import/Export, CAPTCHA verification, the SQL database adapters anduploadfsFull details and reporter credits are in the changelog below and in the published GitHub security advisories.
A few of these fixes change behavior you may be relying on:
viewRolenow applies to logged-out visitors, as was always intended. In particular, users (viewRole: 'admin') reached through a relationship, such as the author of an article, will no longer load for logged-out visitors in templates or public APIs. Use a separate piece type, such as an "author" type, to represent people publicly. See ourpublic-demorepository for an updated example of this.apos.http.bigUploadMiddleware()now requires a logged-in user by default. A route that genuinely accepts anonymous uploads must pass its ownauthorizecallback orauthorize: false.replicateClusters, revoke or rotate that token due to token exposure. However, it should be noted that this configuration did not actually work prior to this release. So it is unlikely that it was in practical use.Additional Improvements
Faster, more predictable undo and redo: Undo now reverses the edit itself instead of replaying your whole editing session and re-rendering the page. The page no longer flashes or jumps, the change is scrolled into view and briefly outlined, each undo is a small, fast save, and your typing history survives the page being refreshed around it.
Structured logging, everywhere: Apostrophe's structured logger now covers boot messages, warnings, runtime errors, long-running tasks, and diagnostics from modules and supporting libraries such as
uploadfsandsanitize-html. Development output remains readable and colorized, while production defaults to one JSON object per line. A new top-levellogoption inapp.jsconfigures logging for the entire process, including multisite projects. Developers using custom loggers should review the changelog below for changes to event metadata.Node.js 22.12 or Newer Required
Apostrophe now requires Node.js 22.12 or newer, since it relies on the ability to
require()ES modules that arrived in that release. If you are on an earlier Node.js 22 release, upgrade Node.js before you update Apostrophe.How to Update
Update your projects with
npm updateand let us know what you think on our roadmap.The “stable” option
Everything above reflects our standard “latest” releases. Customers also have the option of following our “stable” release channel. The difference is simple: stable releases receive new features one full quarter later, but receive bug fixes and security fixes at the same time as “latest.” For this to be an effective way to increase stability, customers still need to actively evaluate “latest” so that they have a meaningful head start on what is coming in “stable.” Customers interested in following this path should reach out for complete information on how to point their dependencies to “stable.”
Everything we ship as “latest” is 100% ready for production. The “stable” series is an option for those with a large investment in training, automated testing, etc. It allows delayed implementation of new features without falling behind on fixes.
🚀 Happy coding!
Apostrophe 4.33.0
Adds
Added
apos.ai, a provider-agnostic AI API for text generation with tool calling, image generation and background jobs. Feature code is written once, against one normalized surface: switching between Anthropic, OpenAI, Google or any OpenAI-compatible service is a configuration change, not a rewrite. It is opt-in: no provider and no key are configured out of the box.@apostrophecms/ai: the engine, with normalized request and result shapes, a tool registry, an agent loop, effort levels, retries, a permission seam and a mock mode for tests.@apostrophecms/ai-adapter-anthropic: Anthropic (Claude) support, via the Messages API. An entry may setworkspaceId(or exportAPOS_ANTHROPIC_WORKSPACE_ID) to send theanthropic-workspace-idheader, required with an identity-linked API key that is not scoped to a single workspace.@apostrophecms/ai-adapter-openai: OpenAI support, via the Responses and Images APIs.@apostrophecms/ai-adapter-openai-compatible: support for any Chat Completions service (Groq, Mistral, OpenRouter, Ollama, vLLM and others) with no adapter code of your own.@apostrophecms/ai-adapter-google: Google (Gemini) support, text and images through one API.AI actions check permissions through
apos.ai.can(req, ...)rather thanapos.permission.can()directly. The signature is the same, with AI policy on top: user accounts and permission groups are denied to the AI outright, for every action including reading, whatever the user's own permissions allow.Supporting changes, useful on their own:
bus: trueis a pure event carrier. It is never rendered, and itseventis emitted onapos.busin exactly one browser tab, then dismissed. The options object may be passed in place of the message.reporting.isCanceling(), acancelroute and acanceledstatus), anexpireAfteroption that expires job records from the database,userIdownership that restricts the status and cancel routes, anotifications: falseoption for callers with their own progress transport, and the error of a failed job recorded on its document.apos.schema.extract()returns the content of a doc or widget as a flat array of text and image items with dot paths, selected by the newextractablepolicy on field types and schema fields.extract(), defaulting to a walk of their own schema. Rich text and image widgets contribute their content directly, andextractableis accepted as a widget type option.Added a
richTextschema field type, so rich text is no longer available only as a widget in an area. ArichTextfield is edited with the same editor, sanitized with the same rules, and indexed for search the same way as the content of a rich text widget.Rich text is still configured in exactly one place, the
@apostrophecms/rich-text-widgetmodule: itsdefaultOptions, itstoolsand its methods governrichTextfields too, so a project that has customized rich text gets the same customizations in schema fields without doing anything. To support this, the editor was factored out ofAposRichTextWidgetEditor.vueinto a new, reusableAposRichTextEditor.vue, used by both the widget editor and the newAposInputRichText.vuefield. The widget editor keeps its name, props, events and markup, so nothing changes for existing rich text widgets or for projects that have overridden either component.Permalinks are stored as placeholders in a
richTextfield, just as in a rich text widget. They are replaced with real URLs when rendered with{% field %}(orFieldin JSX, orAposFieldin Astro). To use arichTextfield without placing it on the page in a WYSIWYG way, callapos.modules['@apostrophecms/rich-text-widget'].renderRichText(req, html)on the markup.Added the
{% field %}custom tag, which outputs one schema field of a document, widget, array item or object and lets the user edit it in place, right where it appears on the page:If the field is an area,
{% field %}is exactly{% area %}: same markup, same editor, samewithclause. Otherwise the field type must offer an on-page editor, which today meansrichTextandstring. Any other type throws an exception naming the field and its type, rather than displaying something the user cannot edit.A single-line string is rendered and edited inline:
Name: {% field data.page, 'name' %}keeps its place on the line when the editor arrives, rather than becoming a block of its own. The tag is chosen by the field type: astringis aspan, while astringwithtextarea: trueand rich text are adiv. Nothing is printed after the closing tag, not even a newline, so a field can be followed immediately by punctuation.Outside of edit mode only the value is rendered, so the tag is safe to use on any page: rich text renders as it does in a widget, permalinks and all, and a string is escaped as text, with the line breaks of a
textarea: truestring preserved. In edit mode the editor is mounted in place, inheriting the page's own typography and taking up no more room than the markup it replaced, so nothing on the page moves when editing begins. Edits are saved exactly as an area on the page saves them, patching one field at a time. Read-only fields, and fields of a document other than the one the page is about, are displayed but not editable, just like an area. Hovering near a field outlines it and shows the same breadcrumb trail a widget has.The
withclause acceptstag(overriding the tag the field type chose),class(orclassName),style,attrs, andedit: falseto render a field that is never editable in place.Field types opt in with a
wysiwygproperty, and can customize the rest:wysiwyg: true: this type can be edited in place.wysiwygComponent: the editor component, by conventionAposWysiwygInputplus the capitalized type name.wysiwygRender(req, field, value): the markup for the value; escaped text by default.wysiwygTag(field): the tag the value is rendered as when the template does not say;divby default.wysiwygModifiers(field): extraapos-wysiwyg-field--*classes, so one type can be styled differently depending on its configuration.wysiwygIcon: the icon that opens the breadcrumb trail,pencil-iconby default. An individual field can override it with its ownwysiwygIconproperty.Supporting changes, useful on their own:
apos.area.renderAreaTag()andapos.schema.renderFieldTag()carry out the work of the two tags and can be invoked directly, and thewithclause is now parsed by one shared implementation. The breadcrumb trail is now defined once, as a set of SCSS mixins in@apostrophecms/ui, shared by the widget and field trails.AposRichTextEditoraccepts aninlineprop, which drops the padding and spacing the editor wears in a widget or a modal.Fields can now be edited in place from JSX templates and from external front ends such as Astro, not only from Nunjucks. JSX templates get a
Fieldhelper alongsideArea, which is the{% field %}tag with the samewithclause:External front ends receive what they need to display and edit a field under
_wysiwygFieldson the document, widget, array item or object the field belongs to.@apostrophecms/apostrophe-astrorenders it with its newAposFieldcomponent.When using Astro, a non-area field must be explicitly declared as WYSIWYG, by setting
wysiwyg: trueon the field or by adding it to thewysiwygFieldsarray option of the module. A name inwysiwygFieldsthat is not in the schema throws at startup. Nunjucks and JSX need no such flag, since they run inside Apostrophe. An external front end receives its data before its templates run, so it has to say in advance which fields it will render in place; annotating every field of every document on a page would add substantially to the response.A visitor who cannot edit a field is sent only what it takes to display it, so a page served to the public carries no editors, icons or patch keys. Fields rendered in place also carry the editor only for the document the page is actually about (the piece on a show page, otherwise the page itself); other documents rendered on the page, such as the pieces of an index page, navigation, and the global doc, are emitted as their values only.
A field rendered in place now names its definition with
data-field-idinstead of carrying a copy of it indata-field, since each doc type and widget type already ships its schema to the browser.Supporting change, useful on its own:
apos.schema.wysiwygFieldData(req, object, field, with)returns everything needed to render a field in place and edit it.Document Versions is now open source and ships in core as
@apostrophecms/document-versions, on for pages and pieces by default and configured per type with theversionsoption. It provides a history of drafts and publications with the editor who saved each one and whether AI was involved, a list of what every version changed field by field, those changes marked on the document where they render, restore from a version's own entry, and Undo Publish through the history. Existing version records from@apostrophecms-pro/document-versionsare converted on upgrade. See the migration guide.Added the
directionoption to@apostrophecms/login(ltrorrtl) to force the text direction of the login page and related pages (password reset, additional login requirements) regardless of the direction of the current locale. When unset, the login page keeps following the locale. Projects that overrideTheAposLogin.vuekeep the<html dir>fix but lose the root direction class; projects that overridelogin.htmlwithout extendingdata.outerLayoutmust set thedirattribute themselves.The video widget now has an in-context empty state you can paste a video URL into.
Changes
Undo and redo on the page now reverse the edit itself, rather than replaying every edit made since the page was loaded and then rendering the whole page again. While editing, this means:
Two things are deliberately unchanged. A rich text editor in a modal keeps its own undo history, since there is no page history there to join. And code that reports edits the old way, with a bare patch, still works exactly as it did, undone by replaying the history and rendering the page again.
For developers:
context-editednow accepts{ patch, inverse, target }, whereinverseis the patch that takes the edit back andtargetdescribes what was edited (widgetId,anchorIdorpatchKey). A bare patch, as emitted until now, is still accepted.Logging is now fully structured. Every diagnostic Apostrophe emits (boot and cluster notices, fatal startup errors, deprecations, runtime error catches, and long-running tasks, which now report a start, periodic progress and a summary instead of a line per item) is a typed event rather than a raw
consolecall. Program output such as task help, listings and reports is unchanged.logoption configures logging for the whole process from its first line, withformat, a customlogger,messageAsandfilter. When present it is the entire configuration, and the legacy@apostrophecms/logand@apostrophecms/utillogging options are ignored, with a startup warning listing them.formatdefaults toauto: colorized pretty output in development, one JSON object per line in production.format: 'legacy'pins the output shape of earlier releases.APOS_LOG_FORMAT,APOS_FILTER_LOGS,NO_COLORandFORCE_COLORset or override this per process.apos-listeningevent, drawn as a startup banner in development and kept by the default filter in production.require('apostrophe/logger')provides the same logger as a standalone factory, usable before or entirely without anaposobject.aposobject, includinguploadfs,express-cache-on-demandandsanitize-html, now receive a logger from Apostrophe and join the pipeline.apos.util.logand related methods, an object in final position is now treated as the event data regardless of the number of arguments, so its keys become queryable fields. Previously only a call of exactly(message, object)was read that way.debug,info,warnanderror, and its first argument is always an event type. There is deliberately nologmethod: calling one throws an error naming the four severities.'<module>: <event-type>'prefix; read themoduleandtypefields instead.apos.util.warnDevno longer prefixes a warning icon.stackis now the error's own stack string, rather than an array of trimmed lines with the first one dropped.Errors passed to
next(err)by Express middleware are now logged as structuredrequest-errorevents and answered with JSON for the API, external front ends and any client not preferring HTML, and with plain text for browser navigations, instead of Express's raw stack on stderr and its HTML error page.Notifications now expire at the database level, rather than lingering forever when nothing dismisses them and being re-sent on every admin page load. The lifetime is set by the new
expireAfteroption of the@apostrophecms/notificationmodule, in seconds, defaulting to86400(one day); set it to0for the previous behavior. A one-time migration clears the existing backlog.Reduced the size of the logged-in admin UI JavaScript bundle by roughly 200KB minified (about 90KB gzipped) with no change in behavior.
lodashand@paralleldrive/cuid2are no longer bundled into the admin UI; the few functions the browser needed from them now come from small, dependency-free implementations inapostrophe/lib/beneath.js. Server-side id generation (apos.util.generateId) still uses cuid2. Becausebeneath.jsis an ES module that is also loaded on the server, Apostrophe now requires Node.js 22.12 or newer (forrequire(esm)), and the package'senginesfield has been updated accordingly.A relationship field with an array
withTypenow fails at startup, instead of passing validation and throwing on every query that loads it.The orphaned document type warning at startup now says "collection" rather than "mongodb collection", so it no longer misreports the database on SQLite and other non-MongoDB projects.
Fixes
Ctrl/Cmd+ZandCtrl/Cmd+Shift+Zwhile a modal is open, which rolled back page changes the editor could not see. Native text undo works inside dialogs again, and page-level undo and redo work as before when no modal is open.hrefattributes. Previously they were emitted as<a href=/contact>rather than<a href="/contact">, so a URL containing a space became two attributes. This affects both rich text widgets andrichTextfields.relationshipSuggestionSort(most recently updated first, by default), which now applies only to the suggestions listed for an empty input.::beforecheckmark on every crumb.{% field %}leaving the browser's copy of its widget out of date, so that copying, cutting, duplicating or opening that widget's editor could quietly undo the user's typing.{% field %}occasionally becoming uneditable until the next full page load, while areas on the same page stayed editable.apos.notification.dismiss()no longer creates a stray database document when the notification it names is already gone.retryUntilUniqueno longer retries an_idcollision thatfixUniqueErrorhandlers cannot resolve.apos.migration.eachnow closes its cursor when the iterator throws, instead of leaving it open.Security
apos.http.bigUploadMiddleware()now requires a logged-in user by default, and theaposBigUploadprotocol it implements has been hardened (CWE-400, CWE-306, CWE-770, GHSA-86wm-68pq-5jwq).The middleware accepted an optional
authorizecallback, but a route that did not supply one processedstart,chunkandendrequests from anyone. Since the protocol allocates server-side upload state and writes chunks to uploadfs before the route's own handler runs, any permission check made by the route was made too late. An unauthenticated request could create upload records, store chunk data and drive filesystem work on a site using such a route. Specifically:req.userunless the route supplies its ownauthorizecallback. Authorization runs before the request body is parsed, so an unauthorized request no longer reaches multer or leaves a temporary file behind. A route that genuinely accepts anonymous big uploads may passauthorize: falseto opt out, and is then responsible for its own protection against abuse. Onlyfalseopts out: any other non-function value now throws at startup, so a misspelled or undefined variable cannot quietly leave a route open.bigUploadMaxChunksoption (default 10000, allowing a 40GB file at the client's 4MB chunk size), and the number of files may not exceed the newbigUploadMaxFilesoption (default 10). The count drives a loop over uploadfs both when assembling a file and when cleaning it up; a declared count ofNumber.MAX_SAFE_INTEGERleft cleanup running effectively forever, and because expired uploads are cleaned up at the start of every new one, one such record stalled every later upload on the site. Cleanup also bounds the chunk count and the number of files it reads back from an existing record, so a record written before this release cannot hang it.end, the temporary files already assembled for it are now removed rather than left behind.endrequest no longer crashes the process. A request naming an upload id that does not exist sent its response and then passednullto the background cleanup routine, producing an unhandled rejection, which by default terminates Node. As a backstop, the middleware no longer returns a promise to Express, which ignores it; anything that rejects on the way to the route is now logged and answered with a 500.chunkandendis now laundered to a string. As an object it reached the MongoDB selector as a query operator, soaposBigUpload[id][$ne]=selected an arbitrary upload in progress rather than the caller's own (CWE-943). Uploads additionally record the user that started them and are only readable by that same user.start,chunkandendnow report a rejected request with its own status code (400 or 404) instead of a blanket 500, a 500 carries the underlying error and stack to the log rather than an empty event, and a refused request is logged without a stack.Thanks to Kai Zhi and Bp0lr for reporting the vulnerability, and to Bp0lr for contributing additional hardening.
The rich text widget's CSV-to-table upload route accepted uploads from anyone, including logged-out visitors, and never removed the temporary file it staged on disk, whether the upload was rejected or accepted. Repeated requests could fill the system temporary directory. The route now requires the same permission as uploading attachments (checked before anything is written to disk), limits uploads to a single file of at most
csvTableMaxSizebytes (a new rich text widget option, 10MB by default), and always removes the temporary file when the request completes (CWE-459, CWE-400, CWE-862, GHSA-qhcq-9pm2-c9w9). Thanks to Bp0lr for reporting the vulnerability.The
projectquery builder now discards field names containing__proto__,constructororprototypesegments, as defense in depth against a denial of service in the SQLite and PostgreSQL adapters. See@apostrophecms/db-connectbelow for details (CWE-1321, CWE-400, GHSA-j5rq-xfvr-p969). Thanks to Daniel Coles and Jace for reporting the vulnerability.The page REST API now enforces view permissions when fetching a single page, when fetching the full page tree (
all=1) and when autocompleting page titles. Previously these requests skipped view permission checks, so users could read pages they were not allowed to view: with apublicApiProjectionconfigured, anonymous visitors could see the projected fields ofloginRequiredpages, and logged-in contributors and editors could read pages whose type has aviewRoleabove their role. The page tree still includes pages the user can view but not edit. Fields restricted withviewPermissionare now also removed from single-page responses, as they already are for pieces. In addition, aviewRoleset on a page or piece type now applies to anonymous site visitors too, as was always intended; before this change it only restricted logged-in users, a bug that should not have been relied upon (CWE-285, CWE-862, GHSA-2j32-q6rx-h844). Thanks to Anisetti Chaitanya Eshwar Prasad for reporting the vulnerability.Reordering a page among its siblings under the same parent now requires permission to create pages under that parent, just like moving a page to a new parent or inserting a page there. Previously that check only applied when a page moved to a different parent. As a result, a user who could edit one page nested under a restricted parent (for example a page type with
editRole: 'admin') could reorder it and so change the rank, and therefore the navigation order, of that parent's other children, which they had no permission to edit. Reordering within the archive is still allowed (CWE-862, GHSA-2jrp-qc93-h2j8). Thanks to Daniel Coles for reporting the vulnerability.Confirming the current password in
@apostrophecms/settingssubforms, including the password change form (PATCH /api/v1/@apostrophecms/settings/password), was not throttled, so someone holding an authenticated session but not the password could guess it without limit and then replace it. These confirmations are now throttled according to thethrottleoptions of the@apostrophecms/loginmodule, just like logging in. Attempts are counted per user in a separate namespace, so mistakes in the settings dialog never lock the user out of the login form, and simultaneous guesses are held to the same limit as sequential ones (CWE-307, GHSA-653j-g8j7-gh54). Thanks to thota murari for reporting the vulnerability.The
:_id/localesREST route of piece types and pages did not apply the public API check used by the other read routes, andapos.doc.getLocales()did not apply document-level view permissions. As a result, a caller could learn which locales exist for documents they are not allowed to view, including draft locales of document types restricted byviewRolefor logged-in users. The route now requires the same public API access as its sibling routes, and only the locale versions of a document that the current user is permitted to view are returned (CWE-862, CWE-200, GHSA-gqh3-7856-rjjg). Thanks to Santosh Kumar Puppala and thota murari for reporting the vulnerability.Hardened the single-use token that moves a logged-in session to a locale served from another hostname. The token was generated with a non-cryptographic ID generator, stayed valid for an hour, could be redeemed on any hostname, and could be redeemed more than once by simultaneous requests. It is now 256 bits from a cryptographically secure source, expires after 60 seconds, is accepted only on the hostname it was minted for, and is consumed atomically. The redirect that strips it from the URL is sent with
Referrer-Policy: no-referrerandCache-Control: no-store. The session is adopted under a freshly generated session id, and an invalid token no longer wipes the visitor's existing session.@apostrophecms/passport-bridgenow mints its cross-locale tokens through the same code (CWE-598, CWE-384, GHSA-hhvr-8m24-qqr3).In addition, the
@paralleldrive/cuid2dependency has been updated to version 3, which draws on the platform's cryptographically secure random number generator rather thanMath.random. This strengthens every identifier and token Apostrophe generates with it, including login bearer tokens and password reset tokens. The format of generated ids is unchanged.Thanks to Anisetti Chaitanya Eshwar Prasad for reporting the vulnerability.
The browser-side
apos.http.parseQueryhelper, which the admin UI runs on the current page's query string, let parameter names containing__proto__,constructororprototypesegments modifyObject.prototype. A crafted link could therefore alter the behavior of the admin UI for a logged-in user who opened it. Such parameters are now ignored, and the parser only descends into the result's own properties. The dot-path setter used to replay undo and redo in areas is hardened the same way (CWE-1321, CWE-79, GHSA-m2m5-rw3w-cwmj). Thanks to LoGiCaL__ for reporting the vulnerability.The
exist-in-localeroute of the@apostrophecms/i18nmodule only checked that the user was logged in. It then reported which of the requested documents existed in a given locale and mode, without checking whether the user was allowed to view them. A low-privilege user could therefore learn whether restricted documents, including drafts, existed in a given locale. The route now reports only documents the user is permitted to view. It rejects draft mode for users who cannot view drafts, and it rejects modes other thandraftandpublished(CWE-862, CWE-200, GHSA-vmxh-77cw-65j7). Thanks to thota murari for reporting the vulnerability.The notification REST API's
PATCHandDELETEroutes and theclear-eventroute did not check who was asking, so anyone who knew a notification's_idcould dismiss it, delete it or clear its event, even without logging in. These routes, the single-notificationGETroute and the server-sidedismissmethod now require a logged-in user and act only on that user's own notifications. A notification belonging to someone else is left alone, the same way a notification that no longer exists is (CWE-862, CWE-639, GHSA-vwwx-px9w-crrc). The practical risk was low: notification ids are randomly generated and are only ever sent to the notification's own recipient, and there is no known way for anyone else to obtain one. Thanks to K Shanmukha Srinivasulu Royal for reporting the vulnerability.A doc type's
viewRoleoption did not apply to the general public (logged-out visitors), only to logged-in users below the required role. As a result, the public could load@apostrophecms/userdocs, which haveviewRole: 'admin', through a relationship, e.g. via a piece type's REST APIpublicApiProjectionor a template.viewRolewas always intended to apply to the public as well, and this was a bug that should not have been relied upon. The public, and any user without a recognized role, now rank belowguest, soviewRole(as well aseditRoleandpublishRole) restricts them as intended. This may change what your site displays: if your templates or public APIs show users reached through a relationship (for instance as the author of an article), those users will no longer be loaded for logged-out visitors. Use a separate piece type, such as an "author" type, to represent people publicly (CWE-863, GHSA-xf6w-q65w-4f2w).Pro Modules
@apostrophecms-pro/advanced-permission3.1.0This module provides more granular control over content permission. It allows the creation of custom groups with proscribed abilities and the ability to assign users to as few or as many groups as desired. Users and groups can also be assigned per-document and per-locale permissions allowing for fine control of who can edit and publish any document. Explore our documentation to discover how this extension can enhance your project. Then, effortlessly integrate it through our new Apostrophe Workspaces. For further details or inquiries, feel free to contact us or visit our pricing page.
Changes
@apostrophecms-pro/automatic-translation1.6.0This bundle allows automatic translation of documents (pages and pieces) when localizing content. It comes with two translation providers: Google Cloud Translation and DeepL, and supports custom providers. Explore our documentation to learn how this extension can enhance your project. Once you're ready, obtain a license and install it through Apostrophe Workspaces. For further details or inquiries, feel free to contact us or visit our pricing page.
Adds
@apostrophecms-pro/automatic-translation-llm, shipped in this bundle. It translates through the provider already configured for the Apostrophe core AI engine (apos.ai), so a project that has configured AI once needs no translation-specific AI client, key or model. Enable it inapp.jsand select it withprovider: 'llm'. The routing optionseffort,providerandmodelare all optional and inherit the core AI configuration. The provider module is inert in library mode (enabled: falseon the main module): it does not register and does not require a configured AI stack. See the README.richTextfield type) are now translated, and convert back as HTML exactly as the content of a rich text widget does. Previously they were extracted and sent, but the translated markup came back through the plain text conversion, which decodes HTML entities, so an escaped<b>returned as a real tag and corrupted the surrounding markup. Opt a field out withextractable: [ 'notranslate' ], as for any other field.extractionVersion(an integer, currently1), a capability marker for modules integrating with the extraction API. Check it instead of feature-sniffing internal methods.enabled: falsethe module registers no providers and no UI, but keeps the extraction API available to other modules.Changes
apos.schema.extract) and itsextractablefield policy. The module requires a core version providing it and refuses to start otherwise. Translation extracts the items taggedtext, excluding those carrying the newnotranslatetag.translate: true/falseflags (at the field, field type, widget module and area widget configuration levels) are deprecated. They keep working through an automatic startup mapping (true→extractable: [ 'text' ],false→extractable: [ 'notranslate' ]) with a once-per-boot deprecation warning. Migrating toextractableis highly recommended; see the migration guide in the README.string(or another text-carrying type) is enough, and fields of an enabled type no longer needtranslate: trueof their own. Previously both the type and every field required explicit flags, so fields that silently never translated will start translating. Review your custom types after updating, and tag any field that must stay untranslated withextractable: [ 'notranslate' ].translate: false, now thenotranslatetag) remain visible to other consumers of content extraction, since the legacy flag only ever scoped translation. Useextractable: falseto hide content from every consumer.htmlwidget now opts out of translation viaextractable: [ 'notranslate' ]instead of the legacy flag. To re-enable it, overrideextractablein your project rather than settingtranslate: true; a startup warning flags that combination.extractTextis nowextractText(req, schema, values), andextractTranslationMetanow takesreqas its first argument. Thereq-less signatures are deprecated: they use an anonymous task request and warn on every call. The recursive signatureextractText(schema, values, fields, valuePath, schemaPath)is also deprecated but keeps working, with a single deprecation warning.getTranslationTextmethod, and a translation type registered with its owngetTextviaaddTranslationType, are consulted exactly as before. Each hook logs a once-per-type deprecation warning; see the migration guide in the README for moving to the coreextractmethod.applyFieldTextnow refuses a field it cannot write back safely (a missing value path, a failing conversion, or a target holding a non-string value such as a whole widget), leaving the document untouched and logging a structuredapply-field-skippedwarning instead of corrupting content or crashing the run. The number of refused fields is recorded onaposTranslationMeta.skippedFields(when non-zero) and reported asskippedFieldsin theafterTranslateevent payload.inputitems of theafterTranslateevent, now carry thelabelof the field they were extracted from, when it has one. The publicextractTextresult does not include it, so its shape still matches the legacy engine's.path, since such an item cannot be applied back to the document.stack, rather than an array of trimmed lines nested in the reported error.Fixes
homepagefield topackage.json, pointing at the public extension page.@apostrophecms-pro/cypress-tools1.0.0-beta.30Automated functional browser tests are an important part of quality assurance for enterprise websites and web applications. Cypress is an industry-standard, open-source library for carrying out automated functional browser tests. This module provides a collection of conveniences for testing ApostropheCMS sites and applications within Cypress. Contact us or visit our new pricing page to learn more.
Fixes
API.mdships in the package, the link is now relative and resolves wherever the README does.homepageinpackage.jsonpointed at the same private repository. It now points at the public extension page.@apostrophecms-pro/data-set2.1.0This module provides an easy avenue for low-code data presentation. It creates a data-set piece for the import of comma-separated values (CSV) files. A separate widget allows for the dynamic selection of columns, and display of the data as an HTML table, or in other templates provided by the developer. Contact us or visit our pricing page to learn more.
Changes
@apostrophecms-pro/doc-template-library2.3.0This module solves the "blank page problem" for developers and product managers, and makes it faster for editors to create content. doc-template-library allows for the configuration of default widgets and pre-populated content on piece or page templates, and to re-use existing layouts. Explore our documentation to discover how this extension can enhance your project. Then, effortlessly integrate it through our new Apostrophe Workspaces. For further details or inquiries, feel free to contact us or visit our pricing page.
Changes
@apostrophecms-pro/document-versions3.0.0Changes
@apostrophecms/document-versionsmodule in Apostrophe core. It has no features: installed alongside a core version that includes the module, it only logs a warning that it can be removed, and with an older core version it refuses to start. See the migration guide.@apostrophecms-pro/import-export-translation1.1.0The purpose of this module is to create a specialized JSON file containing the text of the site, which can be downloaded and manually edited into a different language before being imported into a new locale. Upon import, pages from the default locale will be localized into the new locale substituting the translated text.
Changes
stringare now extracted by default, where the legacy engine requiredtranslate: trueon the type. See the README.@apostrophecms-pro/automatic-translationis too old to extract through the Apostrophe core. This is checked via that module'sextractionVersioncapability marker; at least version1is required.@apostrophecms-pro/automatic-translation, instead of using its deprecated request-less signature.skippedFieldscount: individual fields the translation library refused to write back rather than risk corrupting a document. Each refusal is logged as a structuredimport-field-skippedwarning naming the document and entry, replacing the formerimport-document-errorlog with its guessed "Invalid text data" message, and the rest of the document imports normally.@apostrophecms-pro/automatic-translationis now a structured log entry, and is no longer silent in production.stack, rather than an array of trimmed lines undertrace.Fixes
homepageinpackage.jsonpointed at a private repository, so tools that surface it, including the extension page on apostrophecms.com, offered a link that 404s. It now points at the public extension page.@apostrophecms-pro/multisite4.6.0This module lets you have many ApostropheCMS websites running on a single codebase in a single Node.js process. Each has its own database, users, media uploads, etc. Sites can be created and managed via a dashboard site. Explore our documentation to discover how this extension can enhance your project. For further details or inquiries, feel free to contact us or visit our pricing page.
Adds
sites(site)configuration function now receives a second argument,{ logger }, bound to the site it is configuring, so you can log from a function that runs before the site has anaposobject.Changes
logoption of the rootapp.js(format,logger,filter) configures multisite, the dashboard and every site at once.LOG_LEVELandVERBOSEkeep working as aliases for the severity filter. A site can still take over delivery by returning{ log: { logger } }fromsites(site). See the Logging section of the README.multisite.destroy()now destroys the sites, then the dashboard, then the logger, so a shared transport is torn down once and only after everything that writes to it.Fixes
apostrophe.@apostrophecms-pro/multisite-dashboard1.8.1This extension creates the new default multisite dashboard with infinite scroll, search functionality, and the ability to save templates. This extension requires that the project also have the
@apostrophecms-pro/multisiteextension installed and configured. Explore our documentation to discover how this extension can enhance your project. For further details or inquiries, feel free to contact us or visit our pricing page.Changes
Fixes
@apostrophecms-pro/multisiteextension that used an outdated slug, which now returns a server error.homepagefield topackage.json, pointing at the public extension page.@apostrophecms-pro/section-template-library1.1.0Accelerate content creation and maintain design consistency by turning your best widgets into reusable section templates. Content teams can instantly insert components without recreating layouts from scratch. Explore our documentation to discover how this extension can enhance your project. Then, effortlessly integrate it through our new Apostrophe Workspaces. For further details or inquiries, feel free to contact us or visit our pricing page.
Changes
@apostrophecms-pro/seo-assistant2.0.0The SEO Assistant module generates SEO page metadata automatically through the use of AI. This extension requires that the project also have the
@apostrophecms/seoextension installed. For further details or inquiries, feel free to contact us or visit our pricing page.Breaking changes
apos.ai), configured once for the whole project, instead of carrying its own OpenAI client. Theopenaidependency is gone. Configure a provider on@apostrophecms/ai; see "Migrating from 1.x" in the README.@apostrophecms-pro/seo-assistant-openaiis retired. It still loads, so an existingapp.jsentry does not break the boot, but it does nothing except report that it should be removed. ItsapiKeyandmodeloptions no longer have any effect.registerProvider,getProvider,hasProvider,getProviderManager,addCurrentProvider,getCurrentProviderManagerand thecurrentProviderproperty are gone, along with theprovideroption that named a provider module. Custom providers become either an AI engine provider adapter or a method override on this module.generatePromptnow returns{ system, messages }, the options the AI engine is called with, rather than an array of chat messages. The page context and the editor's instruction now travel as one user message instead of two consecutive ones, which no provider accepts.SEO_ASSISTANT_DEVis removed. UseAPOS_AI_MOCK=1instead, which answers every AI call offline through the engine's own pipeline.Adds
effort,provider,modelandreasoningoptions steer generation, using the AI engine's own vocabulary. All are optional; without them the assistant follows the engine's routing.providerwithout amodeleach stop the boot with the configuration to paste.Changes
apos.schema.extract). This requires a core version providing it; the module refuses to start otherwise.@apostrophecms-pro/automatic-translationextension is no longer required, and theenabled: falseworkaround it needed can be removed from your project configuration.seoTitleandseoDescriptionfields are now taggedextractable: [ 'seo' ], which is how they are kept out of the page context. Any field carrying that tag is excluded from the context.body.contentorrows.cellrather than the barecontentorcell.stack, rather than an array of lines with the first one dropped.Free Modules
@apostrophecms/ai-helper1.0.0-beta.13This module enhances Apostrophe with AI-driven helpers. Currently this module offers:
See the extension page for more info.
Changes
consolecalls, so they flow through Apostrophe's log pipeline. Task listings and reports still print as program output.Security
Hostheader. This allowed a logged-in user to make the server send requests to hosts of their choosing. These images are now read directly from uploadfs, and no HTTP request is involved. The accept and variation routes also let a user act on AI images generated by other users, and the accept, list and delete routes did not check whether the user may edit any content. Users can now only accept images they generated themselves or make variations of them, and all of the AI image routes require the same permissions as generating an image. The package now has a mocha test suite (CWE-918, CWE-862, CWE-639, GHSA-527g-ff96-x6wj). Thanks to Daniel Coles for reporting the vulnerability.@apostrophecms/anchors1.1.1This Anchors module adds a wrapping element with an anchor linking target around all widgets. Developers may customize or opt-out individual widget types.
Fixes
package.json: the license badge and "Give us a star on GitHub!" link pointed at the archived standalone repository, and the missingbugsfield has been added. No code changes.@apostrophecms/apostrophe-astro1.16.0This module integrates ApostropheCMS into your Astro application.
Adds
Added the
AposFieldcomponent, which displays one schema field of a document, widget, array item or object and lets the user edit it right where it appears on the page, the wayAposAreadoes for areas:The value is rendered by Apostrophe, so rich text arrives with its permalinks resolved, a string arrives escaped, and a field type added by a module renders as that module specifies. A single-line string is inline:
Name: <AposField doc={person} name="name" />.keeps its place on the line, and its full stop, when the editor arrives.tagoverrides the tag the field type chose,classis added to the classes it asks for rather than replacing them,styleandattrsare passed through,edit={false}never offers editing, and any other prop reaches the editor as its options.Apostrophe only sends the editing information for fields that ask for it, with
wysiwyg: trueon the field or thewysiwygFieldsoption of the module, since it cannot tell from the data which fields a template renders in place. While developing,AposFieldwrites a note to the terminal when it displays a field that did not ask, naming the field and the option to set. A field that was not annotated is still displayed, just not editable, so a page never breaks over it.Changes
[apostrophe-astro]prefix and honorsNO_COLOR.Fixes
@apostrophecms/cache-redis1.1.1Fixes
@apostrophecms/favicon1.1.4This module allows users to edit the "favicon" (browser tab icon) of the site via the global settings of the site. As such, it pairs well with the Apostrophe palette and multisite modules.
Fixes
@apostrophecms-pro/paletteextension, pointing to the built-in global styles feature instead where an equivalent exists.package.json: the license badge pointed at the retired standalone repository,repository.directorywas misspelled aspackages/favison, and the missingbugsfield has been added. No code changes.@apostrophecms/form1.5.5Allow ApostropheCMS editors to build their own forms. They can then place any form in one or more content areas across the website.
Fixes
@apostrophecms-pro/paletteextension, pointing to the built-in global styles feature instead where an equivalent exists.@apostrophecms-pro/advanced-permissionpointed at a private repository; it now links to the public extension page. No code changes.Security
URLSearchParamsand sent as a form-encoded POST body, and tokens that are not strings are rejected without being verified (CWE-88, GHSA-44qr-rrjg-2cqq). Thanks to Anisetti Chaitanya Eshwar Prasad for reporting the vulnerability.uploadLimitsoption. The file size limit defaults to the attachment module'smaxSizeoption if set, otherwise 20MB. Upload errors and aborted requests now stop the submission instead of processing it with incomplete data. The form widget's browser code now sends the form data before the files, which the server requires (CWE-400, CWE-770, GHSA-89mh-mm8c-mv7f). Thanks to Anisetti Chaitanya Eshwar Prasad for reporting the vulnerability.@apostrophecms/form-submission-google1.0.3This module adds an additional form submission option to Apostrophe Forms. It allows website managers to configure individual forms to submit to a specific Google Docs spreadsheet.
Fixes
package.json: the license badge pointed at the Blog module's license rather than this module's own, and the missingbugsfield has been added. No code changes.@apostrophecms/i18n-static1.2.0This module makes it possible to edit the translations of static text strings found in templates through the ApostropheCMS admin UI.
Changes
consolecalls, so they flow through Apostrophe's log pipeline. Task listings and reports still print as program output.@apostrophecms/import-export3.7.0This module enables import and export of pages and pieces, with or without related documents such as files, images and other related types.
Changes
consolecalls, so they flow through Apostrophe's log pipeline. Task listings and reports still print as program output.Fixes
_iderror.package.json:@apostrophecms/import-export-xlsxwas linked to its archived repository, andpackages/was missing from thehomepagefield. No code changes.Security
The
importExportImportroutes of@apostrophecms/import-export-pageand@apostrophecms/import-export-piece-typenow authorize the chunked upload before it is processed (CWE-400, CWE-306, GHSA-86wm-68pq-5jwq).Both routes passed no
authorizecallback toapos.http.bigUploadMiddleware(), so an unauthenticated request could complete theaposBigUploadstart,chunkandendsteps — creating upload state, storing chunk data and assembling a temporary file — beforeimport()reached its ownreq.usercheck and rejected the import. Each route now makes that same check up front, and unauthenticated requests are refused before any upload state exists.The middleware in
apostropheitself now requires a logged-in user by default and bounds the client-declared chunk count, so this module is protected even without the explicit callback; the callback keeps the routes safe when installed alongside an olderapostrophe.Thanks to Kai Zhi and Bp0lr for reporting the vulnerability, and to Bp0lr for contributing additional hardening.
When extracting an uploaded gzip archive, the check meant to keep tar entries inside the extraction directory only looked for a literal
../in each entry name, so names such as..or./..still pointed outside of it. Entry paths are now resolved and must land strictly inside the extraction directory (absolute names are skipped too), and a failed write now fails the import cleanly and stops reading the archive rather than risking an unhandled error that stops the process (CWE-22, GHSA-97wv-p4xx-c7mg). Thanks to Arpit Jain for reporting the vulnerability.When importing with a
:keycolumn to update existing documents, the key column's name and value were used as-is in the database query that finds the document to update. Because gzip archives are parsed with EJSON and CSV cells are JSON-parsed, a crafted file could supply a MongoDB query operator instead of a plain value (or an operator such as$whereas the column name) and overwrite existing documents it never actually identified. Key values must now be plain strings or numbers and key column names may not start with$or contain.; rows that break these rules are reported as failed and nothing is updated or inserted for them (CWE-943, CWE-284, GHSA-jqw5-w6h3-44g6). Thanks to carfeii for reporting the vulnerability and proposing a fix, and to Anisetti Chaitanya Eshwar Prasad for independently reporting it via CSV import.@apostrophecms/import-export-xlsx1.2.0This module improves @apostrophecms/import-export by adding the
xlsxformat.Changes
consolecalls, so they flow through Apostrophe's log pipeline. Task listings and reports still print as program output.Fixes
@apostrophecms/login-hcaptcha1.3.0This login verification module adds a hCaptcha check when any user logs into the site.
Adds
defaultAdminLocale, otherwise the locale of the login page). The newhcaptcha.hloption of@apostrophecms/loginforces a specific language.Security
URLSearchParamsand sent as a form-encoded POST body, and tokens that are not strings are rejected without being verified (CWE-88, GHSA-44qr-rrjg-2cqq). Thanks to Anisetti Chaitanya Eshwar Prasad for reporting the vulnerability.@apostrophecms/login-recaptcha1.2.1This login verification module adds a reCAPTCHA check when any user logs into the site.
Security
URLSearchParamsand sent as a form-encoded POST body, and tokens that are not strings are rejected without being verified (CWE-88, GHSA-44qr-rrjg-2cqq). Thanks to Anisetti Chaitanya Eshwar Prasad for reporting the vulnerability.@apostrophecms/openapi-generator1.1.0This module automatically generates custom API documentation and client SDKs for your ApostropheCMS project. It discovers all routes—including Pro module endpoints—and creates comprehensive OpenAPI 3.1 specs with zero configuration.
Changes
consolecalls, so they flow through Apostrophe's log pipeline. Task listings and reports still print as program output.@apostrophecms/passport-bridge1.6.1apostrophe-passportworks together withpassport-google-oauth20,passport-gitlab2and similar passport strategy modules to let users log in to Apostrophe CMS sites via Google, Gitlab and other identity providers. This feature is often called federation or single sign-on.Fixes
directionoption of@apostrophecms/login.Security
@apostrophecms/redirect1.7.1Manages site redirects for Apostrophe.
Fixes
package.json: the license badge pointed at the Blog module's license rather than this module's own. No code changes.@apostrophecms/scheduled-publishing1.0.6This module allows you to schedule publishing of your pieces (which includes pages) on specific dates.
Fixes
@apostrophecms/seo1.7.0Comprehensive SEO module providing meta field management and Schema.org structured data generation (JSON-LD) for all pages and pieces.
Changes
consolecalls, so they flow through Apostrophe's log pipeline. Task listings and reports still print as program output.Fixes
@apostrophecms/sitemap1.5.1The Apostrophe Sitemap module generates XML sitemaps for websites powered by ApostropheCMS.
Fixes
perLocale: true) now uses the scheme (http/https) of the site's configuredbaseUrlfor locales with their own hostname (separateHost: trueor ahostnamelocale option), instead of always defaulting tohttp. Previously,<loc>andxhtml:link hreflangURLs for such locales could be emitted ashttp://even when the site and that host are served exclusively over HTTPS.@apostrophecms/vite1.2.0This extension provides Vite integration for ApostropheCMS projects, enabling module bundling and hot module replacement (HMR) during development.
Changes
stackis now the stack string itself, rather than an array of trimmed lines.public/folder that are referenced from CSS, such as web fonts loaded withurl('/modules/...'), are now emitted at their original path instead of as a content-hashed copy underassets/. As a result,apos.asset.url('/modules/...')in a template now matches the URL requested by the built CSS, so<link rel="preload">tags for web fonts work as intended rather than downloading each font twice. Cache busting is still provided by the release directory. Note that Vite still inlines assets smaller than 4 KB into the CSS, so such small fonts should not be preloaded.Fixes
assetsDirbuild option, which was misspelled and silently ignored.@/alias documentation.Utilities
@apostrophecms/cli3.7.1The Apostrophe CLI is a cross-platform starting point for creating and configuring ApostropheCMS projects, providing a simple boilerplate generator and wrapping other useful functions into an easy to use command line tool.
Changes
createaction.@apostrophecms/db-connect1.1.0Adds
expireAfterSeconds) to the PostgreSQL and SQLite adapters. As in MongoDB, a background task removes expired documents every 60 seconds.EJSON(Extended JSON) is now exported, so Apostrophe modules can serialize documents without a direct driver dependency.Fixes
next(), and when afor awaitloop exits throughbreak,returnor a throwing body. Previously each abandoned cursor kept a connection checked out for the life of the process.sort()now orders numbers numerically instead of as text, so10no longer sorts before2, and the children of a page with ten or more of them stay in rank order.Security
Object.prototypeor other built-ins shared by the whole Node.js process. Since theprojectquery builder of the REST API accepts field names from logged-in users with editing access, a single request could delete a built-in method such ashasOwnPropertyand break the site for every user until restart. Field paths containing__proto__,constructororprototypeare now ignored, and the helpers only traverse properties a document actually has, never inherited ones. As defense in depth, theprojectquery builder now also discards field names containing those segments. The default MongoDB adapter was not affected (CWE-1321, CWE-400, GHSA-j5rq-xfvr-p969). Thanks to Daniel Coles and Jace for reporting the vulnerability.apostrophecms-openapi1.1.1The official OpenAPI 3.1 specification for the ApostropheCMS REST API. Explore endpoints interactively, mock the API for rapid prototyping, or generate type-safe SDKs in your preferred language.
Fixes
boring1.1.2Changes
eslint-config-apostrophe6.1.0An ESLint configuration for ApostropheCMS core and official modules.
Adds
eslint-config-apostrophe/strictentry point that promotesno-consoleto an error in server-side code, for projects ready to enforce it.Changes
no-consoleis now a warning for server-side code, where diagnostics belong in Apostrophe's log pipeline rather than on the console. It was already an error in browser and test code.ui/written as.mjsis now recognized as browser code, like its.jsneighbors.express-cache-on-demand1.1.0Express middleware providing "on demand" caching that kicks in only when requests arrive simultaneously.
Adds
loggeroption. Apostrophe passes one, so the unsupported-response error now joins its log pipeline.launder1.7.2Fixes
LICENSE.mdfile. The license type was always correctly stated as MIT. Thanks to Leonardo Cardozo Vargas for pointing out the omission.sanitize-html2.18.0This module provides a simple HTML sanitizer with a clear API.
Adds
loggeroption: pass any console-shaped object, withdebug,info,warnanderrormethods, and sanitize-html's own diagnostics are delivered to it rather than to the console, so an application with its own logging pipeline can route them. Missing methods, and no option at all, fall back to the console.Changes
Fixes
allowedSchemesByTagis now applied tosrcsetandimagesrcsetURLs. Previously the per-tag lookup used the attribute name instead of the tag name, so these attributes always fell back to the globalallowedSchemesand ignored a tag-specific scheme allowlist.iframetag was escaped, a change in behavior caused by an upstream change inhtmlparser2. This "fallback markup" is now preserved without escaping, and is still fully sanitized according to the same rules as the original input. Thanks to sumitjhacodes for the fix.Security
metawas allowed together with itshttp-equivandcontentattributes, the destination URL of a<meta http-equiv="refresh" content="0;url=...">was never checked againstallowedSchemes, because it is embedded incontentrather than being an attribute of its own. Sojavascript:,data:and other disallowed destinations passed through. The refresh URL is now extracted the way browsers do it, allowing for the different spellings, separators, quoting and letter case ofurl=, and checked againstallowedSchemes(orallowedSchemesByTag.meta). If it is rejected, or the content cannot be parsed as a refresh, thecontentattribute is removed.contenton othermetaelements is unchanged. The default configuration does not allowmetaand was not affected (CWE-79, CWE-601, GHSA-cv27-6wvh-8x7j). Thanks to adrbogacz for reporting the vulnerability.animate,animateColor,animateMotion,animateTransform,set) when they retarget a URL attribute such ashrefcompared the full tag name, so a namespace-prefixed spelling likesvg:animatewas not recognized when such tags were allowed (for example withallowedTags: false). In XML serializations such as XHTML or standalone SVG, the prefixed element is a real animation element and could retarget a link to ajavascript:URL after sanitization. The element andattributeNameare now matched by their local names, ignoring any prefix (CWE-79, CWE-184, GHSA-374f-7chj-9948). Thanks to Kai Aizen (SnailSploit) for reporting the vulnerability.noscriptis listed innonTextTags, the discarded region could end too early. Browsers with scripting enabled treat<noscript>content as raw text up to the first</noscript>, but the underlying parser treats it as markup, so an end tag for an enclosing element inside<noscript>closed it implicitly and the rest of its content was emitted as ordinary sanitized markup. The discard region now continues until the point where a browser would end the<noscript>element, while implied closes of othernonTextTagssuch as<option>behave as before (CWE-79, CWE-436, GHSA-x3q4-9hxx-gx8m). Thanks to joaquiniglesiaslug for reporting the vulnerability.sluggo1.0.1Changes
uploadfs1.28.0uploadfscopies files to a web-accessible location and provides a consistent way to get the URLs that correspond to those files. uploadfs can also resize, crop and autorotate uploaded images. uploadfs includes S3-based, Azure-based, GCS-based and local filesystem-based backends and you may supply others.Adds
loggeroption, documented in the README. Apostrophe passes one, so uploadfs diagnostics now join its log pipeline.Security
When the Azure backend was configured with a SAS token (
sas: true),getUrl()returned a base URL that still carried the SAS token as its query string, so the credential was included in public attachment URLs (CWE-522, CWE-200, GHSA-hhph-8536-gfq5).getUrl()now strips the query string from the container URL, so public URLs contain no credentials and are well formed. The top-levelsasoption is now also honored whenreplicateClustersis not used, as documented.The practical impact is expected to be very limited, because this configuration could not have worked in production:
sas: trueoption was ignored, so a SAS token configured that way was treated as an account key and could not authenticate. Only asas: truesetting insidereplicateClusterstook effect, and only when that was the first cluster listed.cdnoption never had the token in their URLs.If you ever deployed uploadfs with a SAS token in
replicateClusters, revoke or rotate that token.Thanks to Kimi Security Team for reporting the vulnerability.
uploadfs did not validate the paths passed to its methods, so an application that passed an untrusted path to
copyIn,copyOut,streamOut,remove,enableordisablecould read, write, delete or change the permissions of files outside the uploads folder when using the local backend. Apostrophe itself builds these paths from generated ids and sanitized file names, but uploadfs now enforces this on its own: paths containing..segments are refused with an error for every backend (this also stops keys that cloud SDKs would normalize into another bucket or container), and the local backend additionally verifies that every filesystem path it touches resolves to a location insideuploadsPath(CWE-22, GHSA-gmfx-5g6x-rr72). Thanks to loulu1ou for reporting the vulnerability.All reactions