Repository navigation
v4.5.0
Upgrading from 4.4 takes four steps, in this order.
-
Update the console first. This release answers with API contract 5
(X-Api-Contract-Version: 5), and a console that does not accept contract 5 refuses to run
against it. Deploy barakoBrew 1.6.0 or later before the API. -
Update any app using barako-client to
@baryodev/barako-client0.3.1 or later. Switching
tenant no longer returns a refresh token, and 0.3.1 keeps the one it already holds. -
Stop the API, then run the two migrations. A running API keeps a transaction open, and
4.4 refuses to start once the new table exists, so both run with the API stopped:psql "$DATABASE_URL" -v ON_ERROR_STOP=1 --single-transaction -f migrations/4.5.0/refresh-token-hash-index.sql psql "$DATABASE_URL" -v ON_ERROR_STOP=1 --single-transaction -f migrations/4.5.0/email-sent-emails.sql -
Start 4.5.0.
To roll back to 4.4, stop 4.5.0 and run the rollbacks before starting the older image:
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 --single-transaction -f migrations/4.5.0/rollback-email-sent-emails.sql
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 --single-transaction -f migrations/4.5.0/rollback-refresh-token-hash-index.sql
After a rollback, anyone who signed in or refreshed on 4.5.0 signs in again, since 4.4 looks refresh
tokens up by a value 4.5.0 no longer stores. docs/upgrading-to-4.0.md has the full sequence.
Module versions that move in this release: BarakoCMS.Abstractions 4.5.0, BarakoCMS.Import 4.4.0,
BarakoCMS.Portability 4.4.0, and 4.3.1 of BarakoCMS.Analytics.Umami, BarakoCMS.Diagnostics,
BarakoCMS.Email.Resend, BarakoCMS.ExternalAuth, BarakoCMS.FeatureFlags, BarakoCMS.Files,
BarakoCMS.Forms and BarakoCMS.Pwa.
Breaking
- Changing a connector's address kept its stored credentials.
PUT /api/connectors/{slug}
keeps a secret the request leaves out, which is how the console saves an edit without showing the
token. That now holds only while the base URL keeps its scheme, host and port (compared with the
host lowercased and in punycode, and the default port made explicit). When any of those change,
every stored secret has to be sent again or cleared in the same request, and the update answers 400
with one error per missing secret, namedsecrets.<Key>. A path change on the same origin keeps
the secrets as before. A request definition whose path template resolves to a different scheme,
host or port from its connector (an absolute or//URL) is now refused when it is composed.
A connector'sprobePathhas to start with a single/and hold no backslash, sohealth,
//host/andhttps://host/are answered 400, and the test button refuses a stored probe path
that resolves to another origin. These requests used to succeed, soX-Api-Contract-Version
moves to 5, the same bump the platform role change makes, and the barakoBrew console has to
accept contract 5 before it runs against this release. - An upload whose bytes do not match its declared type is now refused.
POST /api/files
answers 400 for a type that only starts with an allowed one (image/pngx) and for a file whose
content is not the declared format, such as a PNG sent asimage/jpeg. Both used to be stored.
ApiContract.Versionmoves to 5. - A portability import wrote types and entries that the create endpoints refuse (#933).
POST /api/portability/importnow runs each content type through the checksPOST /api/content-types
runs, lifecycle included, and refuses a field declared twice. A new type keeps the bundle's
lifecycle, where it used to drop it. A stored type keeps its own when the bundle has none, and a
bundle that changes it, or adds one to a type with entries, is refused. On a stored type the
import refuses a required field with no default when the type has entries, and a bundle that
raises, lowers or leaves out a non-Public field;PUT /api/content-types/{name}/fields/{field}/sensitivityis how that changes. A bundle type matches a
stored type under create's name normalisation, soBlog Postupdatesblog-post. Each entry goes
through the same write path asPOST /api/contents, now shared asIContentCreator: a field the
importing caller may not see is dropped, the entry is validated against its type as the bundle
leaves it (required fields, value types, references, unique slugs), the type's lifecycle hooks
run, and the entry starts in the type's initial lifecycle state. The import runs in one database
transaction with each entry written before the next is checked, so validation and lifecycle hooks
see the entries before it in the bundle: journal entries are numbered in sequence and a page's
parent from the same bundle resolves. Export now writes each entry'sid, and a reference to
another record of the bundle is pointed at that record as imported, whatever the order. A
reference to anything outside the bundle has to exist in the target. A type that an earlier import
stored with no display name or with field names that are not PascalCase no longer imports until it
is fixed. The import is all or nothing: any refusal answers 400 naming each item as
contentTypes[i]orcontents[i], nothing is written, and a dry run answers the same. A bundle
holds at most 5,000 entries (Portability:MaxImportRecords) and 500 types. A bundle that imported
before and breaks one of these rules is now refused, soApiContract.Versionmoves to 5, the same
bump as the platform role change.IContentBatchRunnerruns such a batch.ISensitivityService
andIContentValidatorServicegain an overload that takes the content type definition, with a
default implementation that throws. BarakoCMS.Abstractions 4.5.0, BarakoCMS.Portability 4.4.0.
POST /api/import/contentmoves onto the same path and the same transaction: a field the caller
may not see is dropped instead of stored, the type's lifecycle hooks run and see earlier rows, and
a request holds at most 5,000 records (Import:MaxRecords), refused with 400 past that. Its
response shape andcontinueOnErrorare unchanged. BarakoCMS.Import 4.4.0. - Only a platform administrator changes a user's global roles.
POST /api/users/{id}/roles
andDELETE /api/users/{id}/roles/{roleId}change the roles a user holds in every tenant. They now
answer 403 unless the caller'smanage_user_membershipcomes from one of the caller's own global
roles, so an Admin whose role comes from a tenant membership can no longer use them and manages
roles inside that tenant through/api/tenants/members. Removing SuperAdmin now takes a
SuperAdmin, and removing it from the last user who holds it answers 409. A role carrying*,
manage_roles,manage_tenants,manage_usersormanage_email_settingsis now granted only by
a SuperAdmin, through either/api/users/{id}/rolesor/api/tenants/members(403 otherwise),
and removed from a user's global roles only by one;/api/tenants/members/rolesno longer offers
it to anyone else. A tenant admin can still edit or suspend a member who already holds one. Each 403 carries a message
saying why. These requests used to succeed, soX-Api-Contract-Versionmoves to 5, and the
barakoBrew console has to accept contract 5 before it runs against this release. - Switching tenant no longer issues a second session or renews the current one.
POST /api/me/switchused to store and return a new seven day refresh token, and every call,
including one to the tenant the caller was already in, returned a fresh 15 minute access token.
It now returns an access token for the target tenant only, that token expires when the presented
one would have, and the presented token is revoked.refreshTokenstays in the response but is
empty andrefreshTokenExpiryis the default value. The refresh token from sign-in, in the body
or the refresh cookie, already covers every tenant the user belongs to: a refresh mints for the
X-Tenantit is sent and re-checks membership. A client that storedrefreshTokenfrom the
switch response has to keep its existing one when the value is empty, and has to use the returned
token from then on.ITokenIssuergains an overload taking anotAftercap, with a default
implementation that refuses a token it cannot cap. This movesX-Api-Contract-Versionto 5. - A tenant's administrator sees that tenant's data on the admin screens backed by a global
table. Client errors, email events and PWA installs are stored once for the deployment, and
GET /api/client-errors,GET /api/email-eventsandGET /api/pwa/installslisted every
tenant's rows to any Admin. A caller holding the capability through a tenant membership now sees
the current tenant's rows; one holding it through a global role, as on a single-tenant
deployment, still sees every tenant's.POST /api/client-errors/{id}/resolveanswers 404 for
another tenant's error. An email event now carries the tenant that sent the email, recorded
against Resend's id when it is sent and kept 30 days (a newsent_emailstable, created on an existing database by
migrations/4.5.0/email-sent-emails.sql); an event with
no recorded sender is visible through a global role only. Only mail sent on a tenant's behalf is
recorded, which today is a workflow's email: a user's own account mail (sign-in codes,
verification, lockout notices) belongs to no tenant.IEmailServicegainsSendForTenantAsync,
whose default ignores the tenant, so an existing provider keeps working. The same fault reported from two
tenants is now two rows, a report that names no tenant takes the tenant the request resolved to,
and reported tenants are stored lowercased./api/settings,GET /api/settings/email,
everything under/api/feature-flags/adminand all six/api/analyticsroutes are shared by
every tenant, so they now answer 403 to a caller holdingmanage_settings,
manage_feature_flags,view_analyticsormanage_analytics_websitesthrough a membership
only. These requests used to succeed, so this shares the move ofX-Api-Contract-Versionto 5
with the global-roles change.
Changed
- Values placed into an email body were not HTML-encoded. The sign-in code email wrote the
device description (taken from the request's user-agent) and IP address into its HTML as they
were, and a workflowEmailaction did the same with entry fields, which can come from a public
form. Both are encoded now, as is the app name in every system email. In a workflow email, values
inSubjectandTohave their line breaks replaced by a space, and aConditionalresolves its
children's parameters when each child runs instead of substituting values into the branch JSON.
This changes how a rich text or markdown field looks in a workflow email: aBodyof
{{data.Body}}holding<p>Hello <b>world</b></p>now shows the tags as text instead of a bold
"world". A workflow email'sTomust now resolve to exactly one address; a list, or a value that
is not an address, fails the action and sends nothing. The sign-in code email names the device
only when it recognises the browser or system, and says "an unrecognised device" otherwise,
instead of repeating the user-agent. - A portability export returned every entry, unmasked, whatever the caller could read.
GET /api/portability/exportnow applies the content List endpoint's per-entry read rule and the
read endpoints' document and field sensitivity, keyed on the caller. An entry the caller may not
read is left out and counted in the bundle's newcontentsWithheld. A field the caller may not
read comes out under its mask and is named in the record's newmaskedFields, and import skips
those fields, so a mask is never stored as a value. A caller needs a read rule for a type to
export its entries, as onGET /api/contents; SuperAdmin still exports everything. A token whose
user does not exist now gets 401, as on the List endpoint. Each record also carries the entry's
sensitivity, and import creates the entry at that level, so restoring a backup no longer makes
Hidden and Sensitive entries Public; a bundle without it imports as Public, as before. The
portability.exportedaudit entry keepscontentsas the number of entries considered and adds
exportedandwithheld. BarakoCMS.Portability 4.3.1. - The BarakoCMS.Forms package page left out choice fields. Forms has accepted a
choicefield
since 4.3.0, and the form definition lists itsoptionsandmultiple, but the README listed
only the other field types. BarakoCMS.Forms 4.3.1 carries the corrected README; the code is
unchanged. - Refresh tokens were stored as issued, and a refresh answered with the new token in the body
even for a browser that sent only the cookie. The server now stores a SHA-256 hash of each
refresh token and looks tokens up by it. Rows stored before the upgrade still refresh once and
are replaced by a hashed row, so nobody is signed out and the last of them expires within seven
days. Applymigrations/4.5.0/refresh-token-hash-index.sqlon an existing database for the index
on the hash. A refresh sent only thebarako_refreshcookie now sets the new token in the cookie
and returns an emptyrefreshTokenin the body; a refresh sent the token in the body still
returns it there, and sign-in is unchanged.POST /api/auth/logoutaccepts the refresh cookie
when there is no usable bearer: a live cookie revokes that user's refresh tokens, a spent or
unknown one revokes nothing, and every one is cleared with the same 200, so a console whose
access token expired can still sign out. The cookie is now set for/api/auth/logoutas well as
/api/auth/refresh. Logout has its own rate limit of 30 per minute per IP rather than the sign-in
one.
Fixed
- An upload's declared type was matched as a prefix and never compared with the file.
POST /api/filesnow parses the declared type and requires it to be exactly one of PNG, JPEG,
GIF, WebP, AVIF or PDF, and requires the start of the file to be that format. A mismatch is 400.
The type is stored as the bare lower case name, soIMAGE/PNG; x=yis kept asimage/png, and
the storage key's extension comes from that type rather than the uploaded file name. Both
download routes send a sandboxing Content-Security-Policy, and redirect to an object store only
for a file stored with one of those exact types; any other stored file is streamed through the
API asapplication/octet-stream. The redirect also answers 302 now; it used to answer 204.
BarakoCMS.Files 4.3.1.