## Summary
Adds lightweight profiling utilities for measuring execution time of
sync and async functions. This is a developer tool for debugging
performance issues during development — wrap any function with
`timeSync` or `timeAsync` to measure its duration, then call
`printProfileResults` to see a formatted table of timings.
Not intended for production use or to be imported by default, just
available when needed for performance investigations.
These utilities were used to generate results for:
- https://github.com/payloadcms/payload/pull/16134
- https://github.com/payloadcms/payload/pull/16129
### What?
Two-part fix for intermittent 500s during autosave on serverless
environments.
1. `updateLatestVersion` catches errors from
`updateVersion`/`updateGlobalVersion` and checks whether a concurrent
write already succeeded before falling back to `createVersion`
2. `saveVersion` re-throws instead of `return undefined!` so genuine
failures still surface as real errors
### Why?
On serverless, concurrent autosave requests race to update the same
version row. The losing request gets a DB error. Previously
`saveVersion` caught it and returned `undefined!`, which was assigned to
`result`. Downstream field hooks then accessed properties on
`undefined`:
```
TypeError: Cannot read properties of undefined (reading '_status')
```
### How?
When `updateVersion` fails, we re-query the row and check if `updatedAt`
has moved — if it has, a concurrent request already committed and we
return that result. If not, we fall back to `createVersion`. If that
also fails, `saveVersion` re-throws.
Fixes#16217
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1213985147428664
---------
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
## What
Fixes `formatErrors` silently dropping the `data` field from `APIError`
responses in production builds.
Closes#16050
## Why
`formatErrors` used `proto.constructor.name` string comparison to
identify `APIError` and `ValidationError` instances. In production,
webpack/terser minifies class names (e.g., `APIError` → `t`), causing
the check to fail. This silently drops `error.data`, breaking any
feature that relies on it — for example, 2FA flows that check
`data.requires2FA`.
The `APIErrorName`/`ValidationErrorName` module variables were designed
to handle this via dynamic reassignment in constructors, but minifiers
can inline the initial string value at import sites, defeating the
mechanism.
## Changes
- Replaced `proto.constructor.name === APIErrorName` /
`ValidationErrorName` checks with `instanceof APIError` / `instanceof
ValidationError`
- For the Mongoose `ValidationError` fallback (which isn't a Payload
class), switched to checking `incoming.name === 'ValidationError'` — a
string property that Mongoose sets explicitly and isn't affected by
minification
- Removed unused `APIErrorName` / `ValidationErrorName` imports
## Why instanceof is safe here
The original code avoided `instanceof` citing
[TypeScript#13965](https://github.com/microsoft/TypeScript/issues/13965),
where `instanceof` fails for classes extending `Error` when transpiled
to ES5. However:
- Payload's `tsconfig` targets **ES2022**, so native class syntax is
preserved
- `instanceof` works correctly with ES2015+ classes extending `Error`
- The original workaround itself was broken by minification — a worse
failure mode
## Testing
- `npx tsc --noEmit` passes with no errors
- Verified that Payload's `ValidationError` (which has `data`) is caught
by the `instanceof` branch and never falls through to the Mongoose
branch
- The Mongoose `ValidationError` check uses the `name` property which
Mongoose sets explicitly (`this.name = 'ValidationError'`) — not
affected by minification
---------
Co-authored-by: Paul Popus <paul@payloadcms.com>
Adds a temporary, internal API for plugin execution ordering and
cross-plugin discovery. Plugins can now attach three optional properties
to their function:
- `priority` - controls execution order (ascending, default 0), so a
high-priority plugin is guaranteed to see config changes from all
lower-priority plugins
- `slug` - unique identifier so plugins can find each other in
`config.plugins` without importing each other. Useful for checking
within plugin A whether plugin B is installed
- `options` - exposes the plugin's closure args, allowing other plugins
to mutate them before the plugin executes (e.g. injecting tools into
plugin-mcp's options)
Marked as `@internal` so we can start using this across our own plugins
without committing to a public API yet.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1214027832433527
- replaces a hardcoded `'payload-jobs'` string
- uses `getCurrentDate()` consistently instead of `new Date()`
- removes dead code: redundant `taskStatus` reassignment in
`jobAfterRead`, commented-out debug logging in
`calculateBackoffWaitUntil`, and a redundant queue reassignment in the
run endpoint.
- reduces `getCurrentDate()` calls in backoff calculation from four to
one,
- guards and simplifies logging-only array allocations behind the
`silent` check in `runJobs`
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1214013522421649
### What?
Support `where` sent as a JSON string in REST API requests (e.g.
`?where={"read":{"equals":false}}`). Previously the filter was silently
ignored; now it's parsed correctly.
### Why?
GET query params are always strings. `parseParams` parsed `data` this
way already but skipped `where`, so string-encoded filters had no
effect.
### How?
After the `data` block in `parseParams`: if `where` is a non-empty
string, `JSON.parse` it. If it's already an object (e.g. from bracket
notation), leave it alone.
---------
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
Adds `FieldPosition = 'main' | 'sidebar'` and uses it in `FieldAdmin`
and `AdminClient,` replacing the previous `'sidebar'`-only type.
This is especially useful when overriding a plugin field's default
`position: 'sidebar'` back to `'main'`, which already worked at runtime
but failed type-checking.
Also updates the docs to list both options and exports `FieldPosition`
from the package index.
---------
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
### What?
Virtual fields (`virtual: true` or `virtual: "path"`) now default to
`admin.readOnly: true` during field sanitization.
### Why?
Virtual fields are populated server-side and not persisted from user
input. Without `readOnly`, users can type into them but edits are
silently discarded on save — confusing UX with no indication the field
is virtual.
### How?
In `sanitize.ts`, after initializing `field.admin`, set `readOnly: true`
for virtual fields unless the user has explicitly set `readOnly: false`.
Fixes#16013
Related #16012
### What?
Localized blocks, array, and group fields are empty when
duplicating a selected locale.
### Why?
filterDataToSelectedLocales assumed field types (blocks,
array, group) always stored data directly as arrays. When
these fields are localized, data is wrapped in a locale map ({ en:
[...], de: [...] }), which was not being unwrapped before
processing.
### How?
- Added locale-aware branching for blocks, array, and group field
types — when the field is localized, the locale map is filtered
first, then each locale's value is recursed into
- Extracted a filterLocaleMap helper to consistently filter and
transform locale maps across all field types
- Added unit tests
Fixes#15847
---------
Co-authored-by: pevecj <jakob.pevec@renderspace.si>
## Goal
Let projects use **UUID v7** (time-ordered UUIDs) for collection IDs via
`idType: 'uuidv7'` on Postgres-related adapters (and SQLite wiring),
while keeping the same storage shape as v4 UUIDs (native `uuid` on
Postgres, text on SQLite).
### What?
- Adds a new adapter option `idType: 'uuidv7'` alongside existing
`'serial' | 'uuid'` (Postgres) and `'number' | 'uuid'` (SQLite).
- Generates IDs in application code with `uuid` package `v7()` (not
database-native), so older Postgres versions are still supported.
- Reuses the same Drizzle column modeling as UUID v4 where appropriate
(`defaultV7` on the raw column, `$defaultFn` / generated schema code).
- Adds integration tests behind `PAYLOAD_DATABASE=postgres-uuidv7` and a
test adapter preset `postgres-uuidv7`.
### Why?
UUID v7 is sortable by creation time and tends to be friendlier for
B-tree indexes than random UUID v4, without changing the wire format or
Postgres column type.
### How?
- Bump `uuid` in `@payloadcms/drizzle` to a version that exports `v7`.
- Extend TypeScript `idType` unions and `UUIDRawColumn` with
`defaultV7`.
- In schema builders (`setColumnID`, `buildDrizzleTable`,
`columnToCodeConverter`), set app-side defaults for v7; treat `uuidv7`
like `uuid` for relationships and query sanitization via a small
`isUUIDType` helper and `getCollectionIdType` mapping to `'text'`.
Fixes#11449
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1213991890379567
---------
Co-authored-by: Sasha Rakhmatulin <sasha@ritsuko.dev>
### What?
Adds a `/** @type import("payload").ImportMap */` annotation to the
generated importMap.
### Why?
In a monorepo, it's common to use [Project
References](https://www.typescriptlang.org/docs/handbook/project-references.html)
so that Typescript can do dependency analysis on the packages in your
project. Often, projects will have a single root `tsconfig.json` that
`references` all of the packages, including any apps. To do so, all
dependent `tsconfig.json`s must set `"composite": true`.
When `"composite": true` is set in a package, Typescript does
portability analysis on all exports to ensure that Typescript will be
able to properly determine the type of those exports if they are
imported into a consuming package as a dependency. This is true even if
that package never ultimately exports that type through its `main` or
`exports` fields, or is private, or is not depended on by any other
projects. This portability check happens _even if checkJs is disabled_.
Because `importMap` relies on type inference on transient dependencies,
Typescript requires a type annotation to conclude that its type is
portable.
Ultimately, there's no reason for Payload consumers to care about or
enforce the type of `importMap`. It's automatically generated by
Payload, never hand edited, and only consumed by other auto-generated
Payload code.
Since Payload already has a type for `ImportMap`, we can type
`importMap` with that type. It will satisfy the type, and is also
considered portable.
### How?
Updated the generateImportMap template.
Fixes#9914
Co-authored-by: Paul Popus <paul@payloadcms.com>
Add optional `?prefix=` query string support to the existing
`/api/:collection/file/:filename` endpoint to disambiguate files that
share the same filename but have different storage prefixes.
When a storage prefix (e.g., UUID or folder path) is used to make S3
keys unique, multiple documents can share the same `filename`. The
current endpoint resolves by filename alone, which can match the wrong
document in both access control and file retrieval.
- `checkFileAccess` accepts optional `prefix` and adds it to the `where`
clause alongside existing access filters
- `getFile` handler reads `prefix` from `req.searchParams` and threads
it to `checkFileAccess` and handler `params`
- `getFilePrefix` accepts `explicitPrefix` to skip the DB query when the
prefix is already known from the URL
- S3 `staticHandler` forwards `prefix` from params to `getFilePrefix`
- `afterRead` hook appends `?prefix=` to Payload-proxied URLs
- Added unit tests for `checkFileAccess` and `getFilePrefix`, and
integration tests for the endpoint'
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
### What?
This PR introduces the `disableErrors` option into the global `findOne`
operation.
### Why?
`disableErrors` is handy for certain flows where a user may be
unauthorised but you don't need an error to be thrown, instead you would
prefer an empty result.
Without this, the developer needs to catch the error themselves, leading
to inconsistencies as some operations have `disableErrors` and others do
not.
### How?
Introduced the `disableErrors` option to the global `findOne` operation.
### Additional information
- Initial [discord
discussion](https://discord.com/channels/967097582721572934/1102950643259424828/1438504179961430059).
- A [PR](https://github.com/payloadcms/payload/pull/6357) was created to
address this in mid 2024 but was later closed.
- There is opportunity here to include `disableErrors` in a number of
operations that seem to be lacking it, especially in the `sdk` package.
Originally I included these changes in this PR but have since removed to
instead allow for some discussion before we consider introducing them in
a separate PR.
- There is opportunity to introduce better test coverage in a separate
PR for `disableErrors` across existing operations that use it.
- In `packages/payload/src/collections/operations/restoreVersion.ts`,
`disableErrors` exists in the types but is not actually implemented. I
have left this as is for now but wanted to flag it.
### What?
The code comment for type
`CollectionConfig.versions.drafts.schedulePublish.timeFormat` suggests
using 'hh:mm' for a 24 hour clock format. I changed this comment to
instead suggest 'HH:mm'.
### Why?
The lowercase 'hh' makes a 12 hour format, not the suggested 24 hour
format.
### How?
I replaced the comment to suggest using uppercase 'HH' instead.
```diff
- * @example 'hh:mm' will give a 24 hour clock
+ * @example 'HH:mm' will give a 24 hour clock
```
Co-authored-by: Paul Popus <paul@payloadcms.com>
Currently, when you are creating slugs and leave spaces on beggining or
the end, they will be replaced with hyphen, this enhancement avoids
trims unwanted spaces first.
### What?
Utilities for creating slugs from titles
### Why?
Badly created slug with hyphen in the end, when user added title with
space.
### How?
Adding simple `trim()` function before replacing spaces with hyphens
Co-authored-by: Paul Popus <paul@payloadcms.com>
### What?
Export `FieldAccessArgs` from payload
### Why?
Consumers can currently import `FieldAccess` from payload but not the
args type it uses.
This also makes it consistent with `Access` which is exported alongside
`AccessArgs`.
### How?
Added `FieldAccessArgs` to the root exports.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
This docstring appears to contradict the underlying behavior? Suggest a
quick edit here.
Co-authored-by: Paul Popus <paul@payloadcms.com>
Extracts per-field sanitization logic into a standalone `sanitizeField`
function that can be called independently. The `sanitizeFields` function
now delegates to `sanitizeField` in a loop, maintaining identical
behavior while exposing the core logic.
This enables sanitizing individual fields after initial config
sanitization, which is needed for the orderable optimization in the
follow-up PR.
Exports `sanitizeField` and `SanitizeFieldArgs` from the payload
package.
## Problem
Unpublishing documents with `required: true` + `localized: true` fields
fails with a validation error when `localization.fallback: false`.
The `UnpublishButton` sends only `{ _status: 'draft' }` with
`unpublishAllLocales=true` but no field data, causing `beforeChange`
validation to fail on missing required fields.
## Solution
Added `unpublishAllLocales` to the `skipValidation` condition in
`beforeChange` for both collections and globals — same pattern already
used for `isSavingDraft`.
## Why
Unpublish is a metadata-only operation (changing `_status`). Field
validation is unnecessary since data already exists in the database.
Fixes#15651
---------
Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
Payload's 48 hardcoded default timezones were being validated with `new
Intl.DateTimeFormat()` on every cold start. This is unnecessary since
they're known to be valid.
This change pre-computes a `Set` of default timezone values at module
load and skips validation for any timezone in that set.
Custom/user-provided timezones are still validated.
### Before/After
| Operation | Before | After |
|-----------|--------|-------|
| `sanitizeAdminConfig` | 9.18ms | 0.17ms |
| `validateTimezones` | 9.06ms | 0.03ms |
### What?
Virtual fields (`virtual: true` or `virtual: "path"`) no longer get a
default type-based validator during field sanitization. They receive a
noop validator instead.
### Why?
Virtual fields are populated server-side (via hooks, built-in
population, or plugins), so their value is `undefined` in form state.
The default validator causes false validation errors in the admin UI
that users cannot resolve.
### How?
In `sanitize.ts`, when assigning a default validator, check if the field
is virtual first. If so, assign `() => true` instead of the type-based
validator. Explicit `validate` functions are still preserved.
Fixes#16012
Related #16013
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1213743778134481
## Summary
This pr would allow us to import a generic from the plugin and then use
it. The limitation with json-schema-to-typescript is the inability to
generate a type in the file that takes generics.
There are some generated generics plopped in, internally by our
addSelectGenericsToGeneratedTypes function.
This PR exposes a way for external plugins to manipulate the generated
config post-processing of json-schema-to-typescript which is not
possible today.
## Implementation
In `packages/payload/src/bin/generateTypes.ts`, after
`addSelectGenericsToGeneratedTypes`:
```ts
if (config.typescript?.postProcess?.length) {
for (const fn of config.typescript.postProcess) {
compiled = fn(compiled);
}
}
```
Add to TypeScript config type:
```ts
typescript?: {
postProcess?: Array<(compiled: string) => string>;
}
```
**Use cases**
- Inject imports from plugin packages
- Add utility types
**Example**
```ts
// Plugin adds an import and uses it via tsType on fields
typescript: {
postProcess: [
...(config.typescript?.postProcess || []),
(compiled) => {
const importStmt = `import type { MyPluginType } from '@payloadcms/my-plugin';`;
return compiled.replace(/(\/\*[\s\S]*?\*\/\n)/, `$1\n${importStmt}\n`);
},
],
}
```
**Notes**
- Follows existing pattern (Payload already post-processes for Select
generics)
## Summary
Updates three key dependencies to their latest versions to address
security vulnerabilities, bug fixes, and compatibility improvements.
Fixes https://github.com/payloadcms/payload/issues/16072
## Changes
### file-type (19.3.0 → 21.3.4)
- Updated in @payloadcms/payload, @payloadcms/next, and test suite
- Major version bump includes new file type detection capabilities and
improved performance
- Now requires Node.js >=20 (up from >=18)
### ajv (8.17.1 → 8.18.0)
- Updated in @payloadcms/payload
- Minor version bump with bug fixes and schema validation improvements
### jose (5.9.6 → 5.10.0)
- Updated in @payloadcms/payload
- Patch version bump for JWT/JWS/JWE handling library
- Includes security patches and standards compliance updates
### nodemailer (7.0.12 → 8.0.4)
- Updated in test suite only (dev dependency)
- Major version bump with improved email sending capabilities
## Impact
- All packages remain compatible with the project's Node.js version
requirements (^18.20.2 || >=20.9.0)
- No breaking API changes expected for end users
- Lock file updated to resolve dependency tree
Adds field-level access control to internal auth fields (`hash`, `salt`,
`resetPasswordToken`, `resetPasswordExpiration`, `loginAttempts`,
`lockUntil`, `_verificationToken`) to prevent external writes.
This is a preventative measure. Users should configure collection-level
access control on auth collections (e.g., restricting who can update
users) rather than relying solely on field-level protections. Internal
auth operations are unaffected as they use direct DB operations.
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1213885887366085
Bumps qs-esm from 7.0.2 to 8.0.1. [qs-esm 8 removes some legacy code
that is not used in
payload](https://github.com/payloadcms/qs-esm/pull/5), making the
package a bit smaller.
# Overview
When a relationship points to a collection with `trash: true`,
soft-deleted documents were showing as raw IDs in responses instead of
being omitted.
## Key Changes
In the dataloader, trash-enabled collections now fetch with `trash:
true` so soft-deleted documents are returned alongside live ones.
`deletedAt` is merged into the select to ensure it's always present in
the result.
In `populate()`, the returned doc is checked for `deletedAt`. If set,
the doc is trashed and `relationshipValue` is set to null. For hasMany,
null entries are filtered from the array after all row promises settle.
For single relationships, the field is set to null.
This correctly distinguishes trashed documents from inaccessible ones —
access-denied or permanently deleted documents still fall back to the
raw ID, unchanged from existing behavior. `depth: 0` is also unaffected.
Fixes#16022
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1213860470196517
## Summary
Unpublishing a document currently creates a new version in the versions
table, even though the content hasn't changed. Only `_status`
transitions from `published` to `draft`. This clutters the version
history with redundant entries and has been a source of confusion (see
#10838, #12342, #15125).
This PR makes unpublish update the latest existing version in-place
instead of creating a new one, for both collections and globals.
Closes#10838 (again)
## Related
#10838#12342#15125 (Discussion)
## Changes
- `saveVersion` now accepts an `unpublish` flag. When set, it finds the
latest version and updates it in-place (same pattern as the existing
autosave overwrite path) instead of creating a new version.
- Collection and global update operations pass `unpublish:
unpublishAllLocales` to `saveVersion`.
No UI or `next` package changes are needed. The admin UI already sends
`unpublishAllLocales=true` in the query string when the user clicks
"Unpublish." This parameter already flowed through the endpoint handler,
the operation, and into `updateDocument`. The only thing missing was
that `saveVersion` didn't use it. This PR makes `saveVersion` check for
it.
For normal updates (saving drafts, publishing, editing content),
`unpublishAllLocales` is `undefined`, so `!!undefined` evaluates to
`false` and the new code path is skipped entirely.
## Scope and limitations
This PR covers the main "Unpublish" action, which sends
`unpublishAllLocales=true` even for non-localized collections. This is
the case reported in #10838 and #12342.
Single-locale unpublish (the "Unpublish in [locale]" button, available
when `localizeStatus` is enabled) is **not** covered by this PR. That
flow does not send `unpublishAllLocales=true`. Instead, it falls into
the single-locale publish/unpublish code path, which involves data
merging and snapshots. The current code does not distinguish between
"publish locale X" and "unpublish locale X," so fixing it requires a new
flag (e.g. `unpublishLocale`) that the UI would need to send. This
should be addressed in a follow-up PR.
## Testing
Run the new tests with:
```
PAYLOAD_DATABASE=postgres pnpm run test:int versions -- --testNamePattern="Unpublish"
```
Without this PR's changes, 2 of the 3 tests fail:
```
FAIL should not create a new version when unpublishing a collection document
AssertionError: expected [ { id: 249, parent: 24, …(6) }, …(1) ] to have a length of 1 but got 2
FAIL should not create a new version when unpublishing a global
AssertionError: expected [ …(2) ] to have a length of 1 but got 2
```
The third test (`should update main table _status to draft when
unpublishing`) passes both before and after, confirming the main table
was already updated correctly.
---------
Co-authored-by: German Jablonski <GermanJablo@users.noreply.github.com>
# Overview
Fixes document data being silently lost when bulk-trashing draft-only
documents on collections with `trash: true` and `versions.drafts`
enabled.
Localized fields are the most visible symptom, but any field data that
exists only in the versions table (i.e. never published) is affected.
## Key Changes
- In the bulk `update` operation, `queryDrafts` (versions table) is now
also used when fetching documents for a trash attempt, not only when
`shouldSaveDraft` is true
## Root Cause
Draft saves skip `updateOne` on the main collection table
(`isSavingDraft === true`), so the main table only reflects the last
published state. When bulk-trashing, the operation was reading
`docWithLocales` from the main table via `find()` — picking up
stale/empty data — and creating a new version from that stale snapshot.
Single-document trash (`updateByID`) was unaffected because it uses
`getLatestCollectionVersion()`, which always reads from the versions
table.
## Fix
```typescript
// Before
if (hasDraftsEnabled(collectionConfig) && shouldSaveDraft) {
// After
if (hasDraftsEnabled(collectionConfig) && (shouldSaveDraft || isTrashAttempt)) {
```
Fixes#15980
Fixes https://github.com/payloadcms/payload/issues/14938
This PR adds a new property `disableUnique` which is useful when you
want to add a compound unique index to the collection `indexes` config
instead, for example with the multi-tenant plugin where the slug is only
unique per tenant, not globally.
# Overview
Fixes all high/critical severity vulnerabilities reported by `pnpm audit
--prod`.
## Key Changes
- **Bump `undici` in `packages/payload`**
- 7.18.2 → 7.24.4. Fixes 6 CVEs:
[CVE-2026-1526](https://nvd.nist.gov/vuln/detail/CVE-2026-1526),
[CVE-2026-1528](https://nvd.nist.gov/vuln/detail/CVE-2026-1528),
[CVE-2026-2229](https://nvd.nist.gov/vuln/detail/CVE-2026-2229)
(WebSocket memory/crash),
[CVE-2026-2581](https://nvd.nist.gov/vuln/detail/CVE-2026-2581)
(response buffering DoS),
[CVE-2026-1525](https://nvd.nist.gov/vuln/detail/CVE-2026-1525) (request
smuggling),
[CVE-2026-1527](https://nvd.nist.gov/vuln/detail/CVE-2026-1527) (CRLF
injection). Minor version bump, same major. Payload only uses `Agent`
and `fetch`.
- **Bump `rollup` in `packages/payload`**
- 4.52.3 → 4.59.0. Fixes
[CVE-2026-27606](https://nvd.nist.gov/vuln/detail/CVE-2026-27606)
(arbitrary file write via path traversal in output filenames). Minor
version bump within ^4. Used only for `rollup-plugin-dts` type bundling.
- **Bump `@sentry/nextjs` and `@sentry/types` in
`packages/plugin-sentry`**
- ^8.33.1 → ^9.5.0. All 8.x versions pin rollup 3.29.5, vulnerable to
[CVE-2026-27606](https://nvd.nist.gov/vuln/detail/CVE-2026-27606);
9.5.0+ switched to rollup ^4.34.9. The plugin uses a small API surface
(one type import, `ErrorBoundary`, `captureException`) — none have
breaking changes in v9.
- **Bump `changelogen` in `tools/releaser` and `tools/scripts`**
- ^0.5.5 → ^0.6.2. The new version pulls in giget 2.x which dropped the
vulnerable `tar` dependency entirely. Resolves 6 high-severity node-tar
CVEs: [CVE-2026-31802](https://nvd.nist.gov/vuln/detail/CVE-2026-31802),
[CVE-2026-29786](https://nvd.nist.gov/vuln/detail/CVE-2026-29786),
[CVE-2026-26960](https://nvd.nist.gov/vuln/detail/CVE-2026-26960),
[CVE-2026-24842](https://nvd.nist.gov/vuln/detail/CVE-2026-24842),
[CVE-2026-23950](https://nvd.nist.gov/vuln/detail/CVE-2026-23950),
[CVE-2026-23745](https://nvd.nist.gov/vuln/detail/CVE-2026-23745)
(symlink/hardlink traversal, path sanitization bypasses).
- **Override `@vercel/blob>undici`**
- 5.29.0 → 6.24.1 via pnpm override. Fixes the same undici CVEs listed
above. undici 6.x had no API changes vs 5.x (only dropped Node 14/16
support), and `@vercel/blob` only imports `fetch` and types. Bumping
`@vercel/blob` itself (0.x → 2.x) requires `allowOverwrite` handling and
has no test coverage, so that will be a separate PR.
## Design Decisions
Where possible, vulnerabilities are resolved through direct dependency
bumps rather than pnpm overrides. This is more maintainable since future
`pnpm install` runs will naturally resolve to patched versions. The one
exception is `@vercel/blob>undici`, kept as an override because
upgrading `@vercel/blob` from 0.x to 2.x changes overwrite semantics on
`put()` and the adapter has no integration tests. That upgrade warrants
its own PR with proper testing.
Misc input validation improvements, sanitizing path segments in both SQL
and JSON queries, standardizing the processing of column and JSON paths
across different adapters, and making adjustments to traversal and alias
generation to align behavior across components.
## Summary
Fixes#15828
When using `locale: 'all'` with a required localized text field, the
`text` validator incorrectly rejects the value. This happens because:
1. With `locale: 'all'`, the value passed to the validator is an object
like `{en: "Hello", es: "World"}` (locale unflattening via
`mergeLocaleActions` happens *after* validation in `promise.ts`)
2. The old condition `!(typeof value === 'string' ||
Array.isArray(value))` evaluates to `true` for objects, triggering the
required error
3. The `textarea` validator already handles this correctly by using
`!value` (objects are truthy)
### Fix
Changed the text validator's required check from:
```ts
if (!(typeof value === 'string' || Array.isArray(value)) || value?.length === 0)
```
to:
```ts
if (!value || ((typeof value === 'string' || Array.isArray(value)) && value.length === 0))
```
This aligns with the textarea validator's approach:
- `!value` catches `null`, `undefined`, and empty string `""`
- The second clause catches empty arrays `[]` (truthy but length 0)
- Localized objects like `{en: "Hello"}` pass because `!obj` is `false`
### Changes
- **`packages/payload/src/fields/validations.ts`** — 1-line fix to the
required check condition
- **`packages/payload/src/fields/validations.spec.ts`** — 3 unit tests
(localized object accepted, null rejected, empty string rejected)
- **`test/fields/collections/Text/index.ts`** — Added
`localizedRequiredText` test field
- **`test/fields/int.spec.ts`** — Integration test verifying `locale:
'all'` with required localized text
## Test plan
- [x] Unit tests: 88/88 passed (85 existing + 3 new)
- [x] Integration tests (SQLite): 154/154 passed
- [ ] CI passes on all database adapters
## Summary
- Adds `Sec-Fetch-Site` header validation for cookie-based
authentication when `Origin` header is absent
- Accepts cookies for `same-origin` and `same-site` requests
- Rejects cookies for `cross-site` and `none` requests
- Falls back to rejecting cookies when `Sec-Fetch-Site` is absent and
`csrf` is configured (non-browser clients should use `Authorization`
header)
---------
Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
Fix a broken documentation link in the `Config` type JSDoc for admin
custom components.
- Updated:
- `https://payloadcms.com/docs/admin/custom-components/overview`
- To:
- `https://payloadcms.com/docs/custom-components/overview`
### Why?
The previous URL was broken/outdated, so developers reading type
definitions were sent to an invalid docs path.
### How?
Changed the `@see` link in:
- `packages/payload/src/config/types.ts`
No runtime behavior or API logic changed; this is a docs-link correction
only.
This PR properly runs `sanitizeWhereQuery` for joined collections which
is important specifically for virtual relationship fields path
transformation.
Fixes https://github.com/payloadcms/payload/issues/15332
## Context
The root problem was scope leakage in join-based ordering. Reorder
operations were effectively treating join items as one shared list, so
moving an item in one parent context could influence ordering in a
different parent context.
## Changes
- Reorder key generation, adjacent-row lookup, and initial missing-key
backfill now run in the same join `on` scope as the target item.
- Fast drag/drop cases that send `pending` now resolve against scoped
context before computing new keys.
- `index.ts` keeps orchestration, and scoped query logic is extracted
into focused helpers to keep behavior changes easier to review.
- Replaced a local where-wrapper with the shared
`combineWhereConstraints` utility.
## Impact
Users can reorder join rows without cross-parent side effects. Items in
unrelated parent relations no longer jump or get new keys from another
parent's ordering context.
---------
Co-authored-by: German Jablonski <GermanJablo@users.noreply.github.com>
## Summary
- fix collection `restoreVersion` to read version/current docs with all
locales before restore processing
- validate restore data in the default locale context while preserving
full locale data for persistence
- add an integration regression test for restoring versions when
required localized fields are empty in a non-default locale
Closes#15698
## Related
payloadcms/payload#15698
## Changes
- update `packages/payload/src/collections/operations/restoreVersion.ts`
- add coverage in `test/versions/int.spec.ts`
## Testing
- [x] `PAYLOAD_DATABASE=sqlite NODE_OPTIONS="--no-deprecation
--no-experimental-strip-types" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true
pnpm exec vitest --project int test/versions/int.spec.ts -t "should
restore published version with correct data|should restore a published
version when required localized fields are empty in a non-default
locale"`
---------
Co-authored-by: German Jablonski <GermanJablo@users.noreply.github.com>
This PR adds a new images property to BlockConfig, allowing developers
to specify separate images for different UI contexts:
- **images.icon** — small icon displayed in Lexical editor slash menus
and toolbars (20x20px)
- **images.thumbnail** — larger thumbnail shown in the block selection
drawer (3:2 aspect ratio)
## Why
Currently, `imageURL` serves dual purposes:
1. Block selection drawer thumbnails (3:2 aspect ratio, e.g., 480x320px)
2. Lexical editor icons (scaled down to 20x20px)
Citing documentation:
> Display Contexts:
> Block Selection Drawer: Images appear as thumbnails in a responsive
grid when editors add blocks
> Lexical Editor: Images are scaled down to 20x20px icons in menus and
toolbars
This creates problem , where developer needs to choose either good
looking icons (but worse UX on block drawers) or good looking thumnails,
but scaled down images as icons.
## Solution
Add a dedicated `images` property with a fallback chain:
```
Lexical editor icon: images.icon → images.thumbnail → imageURL (deprecated) → default BlockIcon
Block drawer thumbnail: images.thumbnail → imageURL (deprecated) → default placeholder
```
Now you can set different URL for block icons in lexical blocks:
<img width="562" height="370" alt="image"
src="https://github.com/user-attachments/assets/912e1482-0b37-40f7-a144-e21e43fc7aec"
/>
Example:
```ts
const QuoteBlock: Block = {
slug: 'quote',
images: {
icon: 'https://example.com/icons/quote-20x20.svg',
thumbnail: { url: 'https://example.com/thumbnails/quote-480x320.jpg', alt: 'Quote block' },
},
fields: [...],
}
```
## Backwards Compatibility
**Fully backwards compatible** — imageURL and imageAltText still work (marked as @deprecated), with automatic fallback to the new images property behavior.
- Renames `Widget.ComponentPath` to `Widget.Component` and types it as
`PayloadComponent` instead of `string`
- This aligns dashboard widgets with every other component reference in
Payload (collections, globals, fields, admin components) - none of them
use _path_ in the property name, and all of them are typed as
`PayloadComponent`
- Enables new [typescript
plugin](https://github.com/payloadcms/payload/pull/15779) to work for
widget paths (the plugin uses `PayloadComponent` contextual type
detection - `string`-typed properties were invisible to it)
## Breaking Change
`Widget.ComponentPath` has been renamed to `Widget.Component`.
### Migration
Find and replace in your Payload config:
```diff
widgets: [
{
slug: 'my-widget',
- ComponentPath: './components/MyWidget#MyWidget',
+ Component: './components/MyWidget#MyWidget',
},
]
```