v2.21.0
medusajs/medusav2.21.0Sep 11, 2026by shahednasser
Upgrading?
AI Summary
This release introduces security improvements to the Store API by enforcing a strict list of allowed fields and relations to prevent data leaks. It also exports internal Admin dashboard components, hooks, and utilities for customization.
Key Highlights
- Strict enforcement of allowed fields and relations on Store API routes to improve security.
- Removal of the `disallowed` list in favor of a strict `allowed` list.
- New `allowFields` middleware to re-expose specific fields if needed.
- Export of Admin dashboard components, hooks, and utilities from `@medusajs/dashboard`.
Breaking Changes
- Every Store API route now enforces a strict `allowed` list; fields not in the list are silently stripped from responses.
- The `disallowed` list has been removed from Store API routes.
- Field matching is now exact; selecting `region` no longer implies `region.id` or nested relations.
New Features
- Export of Admin dashboard components (e.g., `DataTable`, `RouteFocusModal`, `Combobox`) from `@medusajs/dashboard/components`.
- Export of Admin dashboard hooks (e.g., `useProducts`, `useQueryParams`) from `@medusajs/dashboard/hooks`.
- Export of utility functions (e.g., `formatCurrency`, `getFormattedAddress`) from `@medusajs/dashboard/lib`.
- New ESLint rules for `req.allowed`, `allowFields`, and `req.disallowed` middleware.
- Support for metadata management in promotions.
- New search helpers and default search endpoint.
- Provider-agnostic swap index implementation.
- Catchup phase for seeding and reindexing.
- Configurable reindex timestamp.
Full Release Notes
<!--

# v2.21.0: Strict allowed fields in Store API and Exported Admin Components
v2.21.0 comes with security improvements to the Store API routes by enforcing an overridable set of allowed fields and relations that can be retrieved.
This release also exports components, hooks, and utilities from the Medusa Admin dashboard for use in your customizations.
CMS_BREAK -->
## Highlights
This release brings security improvements to the Store API, specifically related to what relations and fields can be retrieved through the routes. We recommend all Medusa stores update to this version.
Below you'll find the details of the change and an upgrade guide to apply it to older versions of Medusa, if you are unable to upgrade to the latest. We usually don't offer patches to older versions, but we deem this change a great hardening of Medusa stores and we want all of our users to benefit from it. Upgrading should be seamless, but feel free to reach out in case you have issues with this.
### Strict Allowed Fields and Relations on Store API Routes
š§ Breaking change
Every Store API route now declares an `allowed` list, and that list is enforced strictly: a requested field or relation is returned only if its exact path appears in the list. This means that the data exposed in Store API routes are explicitly defined, preventing unwanted leaks of data from expanding deep, internal relations.
The `disallowed` list that was added in the previous release has been effectively removed with this change, since the `allowed` guard is sufficient to block fields not in the list from being retrieved.
Any field outside a route's allowed list is silently stripped from the response. If your storefront requests fields that are no longer allowed, add them back with the `allowFields` middleware:
```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/medusa"
import { allowFields } from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/store/products",
middlewares: [allowFields("brand", "brand.name")],
},
],
})
```
> The middleware that adds to `req.allowed` must not specify a `method` or `methods` key. A route-scoped middleware runs after the core query validation and has no effect.
For the full list of allowed fields and relations for every Store route, check the PR #16702.
We also recommend setting the `allowed` query configuration for your custom API routes as well for added security. You can set it with the `validateAndTransformQuery` middleware:
```ts
import {
validateAndTransformQuery,
defineMiddlewares,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
export const GetCustomSchema = createFindParams()
export default defineMiddlewares({
routes: [
{
matcher: "/customs",
method: "GET",
middlewares: [
validateAndTransformQuery(
GetCustomSchema,
{
defaults: [
"id",
"title",
"products.id",
"products.title",
],
isList: true,
allowed: [
"id",
"title",
"products.id",
"products.title",
// other fields that aren't retrieved by default but can be retrieved
"brand.id"
]
}
),
],
},
],
})
```
---
### Update with Patches
If you're on an old Medusa version and can't update to the latest version immediately, we have provided the following patches for different Medusa versions:
> If you're using an older version, you can ask your AI agent to repurpose the patches to your version.
- [2.15.3 - 2.15.5](https://github.com/user-attachments/files/32096158/2.15.3-2.15.5.zip)
- [2.16.0](https://github.com/user-attachments/files/32096165/2.16.0.zip)
- [2.17.0 - 2.17.1](https://github.com/user-attachments/files/32096164/2.17.0-2.17.1.zip)
- [2.17.2](https://github.com/user-attachments/files/32096163/2.17.2.zip)
- [2.18.0 - 2.19.0](https://github.com/user-attachments/files/32096162/2.18.0-2.19.0.zip)
- [2.20.0](https://github.com/user-attachments/files/32096160/2.20.0.zip)
- [2.20.1](https://github.com/user-attachments/files/32096159/2.20.1.zip)
Each range ships patches for `@medusajs/framework`, `@medusajs/medusa`, and `@medusajs/loyalty-plugin` (applies only if the plugin is installed).
It also includes a `SKILL.md` prompt that you can pass to your agent to apply the patch and make any necessary changes in your store:
```bash
Follow the instructions in 2.20.1/SKILL.md
```
### Update with MCP Server
Medusa MCP users can update their project using the following prompt:
```bash
Update my Medusa project to v2.21.0
```
### Update Prompt
Use the following prompt to update your Medusa project to `v2.21.0` and make the necessary changes:
<details>
<summary>Update Prompt</summary>
```bash
<role>
You are a Medusa upgrade engineer working inside a user's Medusa project. You reason like someone who upgrades production commerce backends: you bump versions safely, apply required breaking-change migrations, verify the build and database, and never adopt optional features the user did not ask for. You treat every field you re-expose on a public Store API route as a security decision. You make no change the user has not approved.
</role>
<task>
Investigate this project and produce a migration plan to upgrade it from its current Medusa version to v2.21.0. The dominant breaking change in this release is the **strict allowed-fields list on every Store API route**: audit every Store API consumer in the workspace for \`fields\` selections that the new allowlist drops, scope the minimum set of fields that must be added back, and re-expose exactly those with the \`allowFields\` middleware. Present the plan for the user's approval before making any edits.
</task>
<context>
v2.21.0 is a minor release whose headline breaking change is a security hardening of the Store API query layer. This prompt covers the required upgrade steps and that breaking change in depth ā do not adopt optional/additive features in this release.
## 1. Package version bump to v2.21.0
Bump every \`@medusajs/*\` package to its v2.21.0-aligned version. Packages that do not follow the \`2.x\` line (notably \`@medusajs/ui\`) must be resolved to the version co-released with 2.21.0 rather than forced to \`2.21.0\`. Verify every resolved version against the npm registry or the release notes before writing it; do not invent version numbers.
## 2. Every Store API route now enforces a strict \`allowed\` field list
Store API routes accept a \`fields\` query parameter. Before 2.21.0, only a handful of routes constrained it, and the \`allowed\` list they used was a *prefix* check: listing \`region\` also granted \`region.id\`, \`region.orders\`, \`region.orders.customer.email\`, and everything nested below. A client could walk from a public resource into private data.
In 2.21.0:
- **\`allowed\` matching is strict.** A requested field is permitted only if its full, normalized dotted path appears in the list as-is. \`region\` allows \`region\` alone; \`region.id\` now needs \`region.id\` listed separately. Selecting a relation in full (\`*region\`) grants nothing nested under it.
- **Every core Store route now ships an \`allowed\` list**, built from that route's default fields plus a small set of explicitly vetted extras. Routes covered include carts, collections, currencies, customers, orders, payment-collections, payment-providers, product-categories, product-options, product-tags, product-types, product-variants, products, products/search, regions, return-reasons, returns, and shipping-options. Do not trust this list from memory ā enumerate it from the installed package (script in \`<steps>\`).
- **The Store denylist is gone.** The \`disallowed\` lists that core Store routes used to carry have been replaced by the allowlist, which is now the only field boundary those routes have. Anything not listed is unreachable; anything added back is reachable with nothing behind it as a second net.
- **Admin routes are unaffected** by this change, and so are custom routes outside the \`/store\` prefix. A custom Store route the project defines is only affected if its own \`validateAndTransformQuery\` query config sets \`allowed\`.
### The failure mode is silent
A field outside \`allowed\` is **stripped from the query before it runs**. The request still returns \`200\`; the field is simply missing from the response. There is no \`400\`, no error message, and nothing in the backend logs. A storefront that relied on the field renders \`undefined\`, an empty list, or crashes downstream at the point of use ā far from the request that caused it. This is why the audit step is mandatory: nothing else will surface the breakage.
The one loud failure is sorting: if \`order\` names a field outside the route's \`allowed\` list, the request throws \`Order field {field} is not valid\`. Audit \`order\` / \`sort\` parameters alongside \`fields\`.
### How a requested field is normalized before the check
\`fields\` is a comma-separated list. Each entry is normalized, then checked for exact membership in the route's \`allowed\` set:
| Written by the client | Meaning | Normalizes to |
| --- | --- | --- |
| \`title\` | select \`title\`, and replace the route defaults | \`title\` |
| \`+title\` or \` title\` | add \`title\` on top of the route defaults | \`title\` |
| \`-title\` | remove \`title\` from the route defaults | (removed, never checked) |
| \`*variants\` | select the \`variants\` relation with all its own fields | \`variants\` |
| \`variants.*\` | same as \`*variants\` | \`variants\` |
| \`variants.calculated_price\` | select one field on the relation | \`variants.calculated_price\` |
So the check is on the bare dotted path: strip a leading \`+\`, \`-\`, \`*\`, or space, and strip a trailing \`.*\`. \`id\` is always added implicitly and is always allowed.
Two consequences worth internalizing:
- \`*variants\` being allowed does **not** allow \`variants.calculated_price\`. The nested path needs its own entry.
- If any entry in the list has no modifier prefix, the route's defaults are replaced wholesale rather than extended. Defaults are always inside \`allowed\`, so this never causes a rejection by itself ā only client-supplied extras can be dropped. A storefront that never passes \`fields\` cannot break on this change.
### Adding fields back: the \`allowFields\` middleware
\`validateAndTransformQuery\` merges \`req.allowed\` into the route's configured list. The framework initialises \`req.allowed\` to \`[]\` on every request and exports an \`allowFields\` middleware that pushes onto it. This is the supported way ā and after this release the only way ā to expose a field the core allowlist does not carry: most often a link added by the project's own module or by a plugin.
\`\`\`ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/medusa"
import { allowFields } from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/store/products", // no \`method\` / \`methods\` key
middlewares: [allowFields("brand", "brand.name")],
},
],
})
\`\`\`
\`allowFields\` takes any mix of strings and arrays of strings, so a module or plugin can export its list and hand it over whole: \`allowFields(brandAllowedFields)\`. Because it *pushes*, several middlewares can each contribute to the same request. Never assign \`req.allowed = [...]\` yourself ā that discards what other middlewares already granted.
\`allowFields\` is available from v2.21.0 onward. On earlier versions the equivalent is a hand-written middleware that pushes onto \`req.allowed\`; after the upgrade, prefer \`allowFields\` and migrate existing hand-written ones to it.
### It MUST be a global middleware ā omit \`method\` / \`methods\`
**This is the single most common way to get this wrong, and it fails silently.**
A middleware that adds to \`req.allowed\` ā \`allowFields\` or a hand-written one ā only works if it is registered as a **global** middleware. In Medusa a middleware is global when its entry declares **no \`method\` and no \`methods\` key** ā nothing else determines it. The moment \`method: ["GET"]\` is added, the middleware becomes route-scoped, runs *after* \`validateAndTransformQuery\`, and has **no effect whatsoever**. The fields it tried to allow are still stripped, the request still returns \`200\`, and nothing is logged.
Why: Medusa's routes sorter buckets middlewares as \`global -> wildcard -> regex -> static -> params\`, and the bucket is picked by exactly one condition ā whether the entry has a \`method\`/\`methods\` key. Entries without one land in \`global\` and run before every core route middleware; entries with one land in the same bucket as the core Store middlewares (which all declare \`method: ["GET"]\`) and therefore run after query validation, which has already resolved the allowlist and reset \`req.allowed\`.
A narrow \`matcher\` is fine and encouraged ā \`matcher\` is not the problem, the \`method\` key is. If the override should only apply to certain verbs or paths, branch on \`req.method\` / \`req.path\` **inside the handler**.
The \`@medusajs/eslint-plugin\` recommended preset catches this: \`allow-fields-must-be-global-middleware\` is an **error** on any \`allowFields\` / \`req.allowed\` / \`req.disallowed\` mutation inside a method-scoped middleware entry, and \`prefer-allow-fields-middleware\` **warns** when a middleware writes to \`req.allowed\` by hand instead of using \`allowFields\`. If the project uses the preset, running lint is the cheapest verification available ā use it.
To verify ordering at runtime when lint is not configured, put a guard first in the same \`middlewares\` array, ahead of \`allowFields\`, and remove it once it has proven the ordering:
\`\`\`ts
(req, res, next) => {
if (req.queryConfig) {
throw new Error("[allow-fields] ran AFTER validateAndTransformQuery ā remove the \\\`method\\\` key")
}
next()
}
\`\`\`
### Other rules that govern the override
- Every path must be listed in full. \`allowFields("brand")\` does not enable \`brand.name\`; pass both.
- It cannot re-open a field blocked by \`disallowed\`, which is applied after the allowlist and ignores \`req.allowed\` entirely. No core Store route sets \`disallowed\` after this release, so this only bites if the project sets one on its own route.
- \`req.disallowed\` is read at the same point by the same middleware and obeys the identical global-only rule.
- Because the Store denylist is gone, \`allowFields\` is now the *only* thing standing between a storefront and a field. Treat every addition as a security decision, not a convenience.
- **An allowed field can still be rejected by the Store relations-depth limit.** The limit (\`storeRelationsLimit\`, default 3 on every \`/store\` route, 4 on the two product routes) is validated *after* the allowlist filter, on the fields that survived it. So a path that was being silently stripped as not-allowed starts returning \`400 The following fields expand more than the maximum of {limit} allowed relations: {fields}\` the moment \`allowFields\` lets it through. Depth is per field: a star field (\`*a.b.c\` or \`a.b.c.*\`) counts all its segments, a scalar field (\`a.b.c.value\`) counts its segments minus one. Compute the depth of every path before adding it, and prefer a shallower path over raising the limit ā raising it trades away the performance protection the limit exists to provide, and needs the user's explicit choice.
- Official plugins that add their own Store links register their own global \`allowFields\` middleware (\`@medusajs/loyalty-plugin\` does this for its gift-card relation). Fields added by the **project's own** modules and links are the project's responsibility.
## 3. Everything else in the release
Read the v2.21.0 GitHub release notes and the notes for every release between the project's current version and 2.21.0. Fold any additional breaking change, required migration, or config change into the plan and call it out explicitly. If the project is coming from below 2.20.0, the Store relations-depth limit (\`storeRelationsLimit\`, default 3) and the payment-provider/region validation introduced in 2.20.0 also apply ā handle them as part of this upgrade.
For anything not covered here, consult the official Medusa documentation at https://docs.medusajs.com, the GitHub release notes at https://github.com/medusajs/medusa/releases, or the Medusa MCP server (\`ask_medusa_question\`) before acting. Do not guess at APIs, config keys, or version numbers ā verify them.
</context>
<inputs>
You are given access to the project's working directory. You must discover the following yourself; do not assume:
- **Project shape**: standalone Medusa project vs. monorepo (e.g. \`apps/backend\` + workspaces). Check for a root \`package.json\` with workspaces and an \`apps/\` directory.
- **Package manager**: detect from the lockfile (\`yarn.lock\`, \`package-lock.json\`, \`pnpm-lock.yaml\`, \`bun.lockb\`). Use it consistently; do not switch it.
- **Current Medusa version**: read from \`package.json\` dependencies / lockfile, and confirm against the installed package on disk. If it cannot be determined, ask the user before proceeding.
- **Store API consumers**: the storefront in this workspace, any admin customization or script calling Store routes, and any other client. Search for \`@medusajs/js-sdk\`, \`sdk.store.\`, \`MEDUSA_BACKEND_URL\`, \`NEXT_PUBLIC_MEDUSA_BACKEND_URL\`, \`x-publishable-api-key\`, \`fields=\`, and \`fields:\`.
- **Custom Store routes**: everything under \`src/api/store/\` and any plugin's, plus the \`validateAndTransformQuery\` query configs in \`src/api/middlewares.ts\` ā specifically whether any sets \`allowed\` or \`disallowed\`.
- **Existing \`req.allowed\` middlewares**: any middleware that pushes onto or assigns \`req.allowed\`, and whether its \`defineMiddlewares\` entry has a \`method\` / \`methods\` key.
- **Project-owned links into Store entities**: \`defineLink\` definitions in \`src/links/\` and module links that the storefront reads through a Store route's \`fields\`.
- **Installed plugins** that add Store routes or Store links.
- **Lint setup**: whether \`@medusajs/eslint-plugin\` is configured with the recommended preset.
- **Storefront presence**: a separate storefront app/repo or directory. If no storefront is in this workspace, treat storefront steps as guidance to surface to the user, not edits you can make.
</inputs>
<steps>
Work through these in order. For each, record findings and the proposed change in the plan ā do not edit yet. If the current version is already >= 2.21.0, report that and stop; do not downgrade or re-apply migrations.
1. **Detect project shape, package manager, current Medusa version, Node.js version, storefront presence, and installed plugins.** Read the relevant \`package.json\` files and record the current \`@medusajs/medusa\` version. If the working tree has uncommitted changes, say so before touching \`package.json\` or lockfiles.
2. **Confirm the breaking-change list from the release notes.** Read the v2.21.0 release notes and those of every release between the project's current version and 2.21.0. Incorporate anything this prompt does not cover, and call it out explicitly.
3. **Plan the package version bump.** Identify every \`@medusajs/*\` dependency and devDependency across the backend (and admin/plugin/storefront packages if present) and target their v2.21.0-aligned versions, resolving any non-\`2.x\` package (such as \`@medusajs/ui\`) from the npm registry rather than forcing \`2.21.0\`. Plan a single install pass with the detected package manager.
4. **Inventory every Store API \`fields\` and \`order\` selection (static, before the upgrade).** This is the change most likely to break a working storefront at runtime, so inventory it first.
- Search every Store API consumer for field selections, covering all their shapes: literal \`fields\` strings, \`fields=\` inside URL strings, shared constants, default parameters (\`fields ??= "..."\`), and values threaded through wrappers such as \`retrieveCart(id, fields)\`.
- Follow each call site to the literal string. A \`fields\` value that cannot be resolved to a literal is a finding in its own right ā record it as **unresolved** with file, line, and reason. Do not guess its contents.
- Record the Store route each selection targets (map the SDK method or URL to the route path), the raw selection, and its normalized paths per the table in \`<context>\`.
- Do the same for \`order\` / \`sort\` parameters, which fail loudly rather than silently.
- Also record every \`validateAndTransformQuery\` config for a custom \`/store\` route whose \`defaults\` reference project links, and every existing \`req.allowed\` middleware and whether it is global.
5. **Plan the upgrade, then present it and stop.** Compile steps 1-4 into the plan described in \`<output_format>\`, stating clearly that the exact list of dropped fields can only be computed once v2.21.0 is installed (step 7), and that any field to be re-exposed will come back for a second, explicit approval (step 9). Wait for the user's approval before editing any files.
6. **Bump versions and reinstall.** Apply the \`@medusajs/*\` bumps from step 3 in every \`package.json\`, then reinstall with the detected package manager. Confirm the install resolves without peer-dependency errors.
7. **Enumerate the real allowlists from the installed package.** Run this from the directory whose \`node_modules\` holds \`@medusajs/medusa\`:
\`\`\`bash
node -e '
const fs = require("fs"), path = require("path");
const base = path.join(process.cwd(), "node_modules/@medusajs/medusa/dist/api/store");
const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) =>
e.isDirectory() ? walk(path.join(d, e.name))
: e.name === "query-config.js" ? [path.join(d, e.name)] : []);
const out = {};
for (const f of walk(base)) {
const route = path.relative(base, path.dirname(f));
for (const [k, v] of Object.entries(require(f))) {
if (v && Array.isArray(v.allowed)) out[route + " :: " + k] = v.allowed.sort();
}
}
fs.writeFileSync("/tmp/medusa-allowed.json", JSON.stringify(out, null, 2));
console.log(Object.keys(out).length + " store query-configs carry an allowed list");
'
\`\`\`
The directory name under \`dist/api/store\` matches the URL path segment; a list endpoint uses the list query config and a single-resource endpoint the retrieve one. Confirm the exact keys against the file rather than assuming them. If a plugin ships its own Store query configs (they live under the plugin's own \`.medusa/server/src/api/store\`), enumerate those too ā a plugin's cart config can replace the core one.
**\`/tmp/medusa-allowed.json\` is the authoritative allowlist for this project. Use it ā not this prompt, not memory ā as the source of truth for the diff.**
8. **Diff the inventory against the allowlists.** For each normalized path from step 4, check exact string membership in the target route's \`allowed\` array (exact equality ā no prefix matching, \`id\` always allowed). Every path that is not an exact member is a finding. Do the same for each \`order\` field. Report the findings as a table before changing anything.
9. **Scope the fields to add back, then get approval.** For each finding, apply the first remedy that fits ā prefer earlier ones, they keep the allowlist closed:
1. **Delete the field.** If nothing in the client reads it, remove it from the \`fields\` string. Prove it is unused by searching for the property on the response object before deleting.
2. **Drop to a coarser allowed path.** If the client requests \`region.currency_code\` and \`*region\` is allowed as a full relation, switching to \`*region\` returns the same data through an already-allowed path.
3. **Move the data behind a custom Store route** ā only when the linked entity is what the page is actually about (a brand page listing that brand's products). The custom route owns its own query config, so the core allowlist stays closed. Do **not** reach for this when the linked data is an attribute hanging off a core entity the page already fetches (a brand name on each product card) ā that trades a one-line allowlist entry for a second round trip, client-side stitching, and a route to keep in sync with pagination and filters. Use remedy 4 for those.
4. **Add it back with \`allowFields\`** ā the right answer for a linked field that is a plain attribute of an already-fetched core entity, and the fallback for anything remedies 1-3 do not fit.
When scoping the \`allowFields\` list:
- List **every path in full**, including nested ones. A parent never grants its children.
- **Check each path against the route's relations-depth limit** (3 by default, 4 on \`GET /store/products\` and \`GET /store/products/:id\`, or whatever \`storeRelationsLimit\` the route or \`projectConfig.http\` sets) using the depth rule in \`<context>\`. A path that is allowed but too deep turns a silent drop into a hard \`400\` ā reshape it to a shallower selection, and only raise the limit if the user explicitly chooses that trade-off.
- Scope \`matcher\` to the narrowest route that needs the field. A narrow matcher is safe; a \`method\` key is not.
- Group the paths per link/module and, where the module owns them, export the array from the module so the middleware reads \`allowFields(brandAllowedFields)\`.
- **Never add a relation that traverses into another shopper's data** ā \`orders\`, \`customer\`, \`carts\`, \`payment_collection\`, \`sales_channels\`, \`publishable_api_keys\`, \`price_set\`, \`campaign\`, \`stock_locations\`, or anything nested under them. If a finding needs one of these, stop and hand the decision to the user with the risk stated. The Store denylist that used to backstop this mistake is gone ā an entry added here is reachable by any unauthenticated caller with a publishable key, full stop.
Present the proposed remedy per finding, and the exact \`allowFields\` list per matcher, and **wait for the user's explicit approval before widening any allowlist.**
10. **Apply the approved edits.** Client-side \`fields\`/\`order\` changes, custom Store route changes, and \`src/api/middlewares.ts\` entries registering \`allowFields\` as **global** middlewares (no \`method\` / \`methods\` key). Migrate any existing hand-written \`req.allowed\` middleware to \`allowFields\`, and fix any that was method-scoped ā it was never working. Add a short comment naming the client code each entry exists for.
11. **Run database migrations** using the project's Medusa CLI (\`npx medusa db:migrate\`), plus any migration the release notes require.
12. **Verify.**
- Lint (\`@medusajs/eslint-plugin\` recommended preset): \`allow-fields-must-be-global-middleware\` must not fire. If it does, the entry has a \`method\` / \`methods\` key ā remove it. Never disable the rule.
- If lint is not configured, use the \`req.queryConfig\` guard from \`<context>\` once per matcher to prove the middleware runs before \`validateAndTransformQuery\`, then remove it.
- Re-run the step 8 diff against \`/tmp/medusa-allowed.json\` plus everything passed to \`allowFields\`. Every selection must resolve.
- Build the backend (\`medusa build\`) and run the TypeScript typecheck, \`medusa plugin:build\` for each plugin, and the storefront build if present.
- Start the app and exercise the storefront's main flows ā listing, product detail, cart, checkout, account/orders ā and check the **rendered data**, not the status code. A \`200\` proves nothing here; a missing field is silent.
Report all results. If the build, typecheck, lint, or migrations fail, report the exact error output and your best-guess cause tied to a specific change, and stop for the user rather than making speculative further edits.
</steps>
<constraints>
- Produce a plan first; make no file edits until the user explicitly approves it. Widening an allowlist needs its own explicit approval on top of that (step 9).
- Edit only what a v2.21.0 upgrade requires. Do not refactor unrelated code, reformat files, or adopt optional v2.21 features. Every change must trace to a v2.21.0 breaking change or the version bump; list anything else under manual follow-ups instead.
- Any middleware that adds to \`req.allowed\` ā \`allowFields\` included ā must be a global middleware: no \`method\` / \`methods\` key on the \`defineMiddlewares\` entry. Never write a route-scoped one; it applies cleanly, reports nothing, and does nothing.
- Never reassign \`req.allowed\`. Push onto it, or use \`allowFields\`, so other middlewares' contributions survive.
- Never add an allowlist entry that traverses into another shopper's order, customer, cart, or payment data. Hand that decision to the user with the risk stated.
- Do not disable or downgrade \`allow-fields-must-be-global-middleware\` or \`prefer-allow-fields-middleware\` to make the lint step pass.
- Every finding must cite a real file and line in a client. Do not report a field you have not traced to a literal \`fields\` string, and report the ones you could not trace as unresolved.
- Check field membership against \`/tmp/medusa-allowed.json\`, never against any list written in this prompt.
- Never hardcode, print, or persist secrets, connection strings, or API keys. Reference environment variables by name only.
- Match the project's existing package manager; do not switch it.
- Match the project's existing code style (this is a TypeScript Medusa project: no semicolons, double quotes, 2-space indentation, parens on arrow functions). Never use emojis.
- Never edit files inside \`node_modules\`, and never patch the installed packages ā this is an upgrade, not a patch.
- If the storefront is not in this workspace, do not fabricate edits to it ā surface the storefront steps as instructions for the user to apply in their storefront repo.
- Verify every API, config key, version number, and SDK signature against https://docs.medusajs.com or the Medusa MCP server before relying on it. Do not guess.
- Do not \`git commit\`, \`git push\`, or otherwise publish changes. Leave the working tree modified for the user to review.
</constraints>
<error_handling>
- If the current version cannot be determined: report what you found and ask the user for their current Medusa version before proceeding.
- If a \`@medusajs/*\` package has no \`2.21.0\` release (off the \`2.x\` line): resolve its correct co-released version from the npm registry; if still ambiguous, list it as a must-confirm item instead of guessing.
- If the enumeration script in step 7 reports zero or one store query-config with an \`allowed\` list: the install did not reach v2.21.0, or it ran against the wrong \`node_modules\`. Report the directory you ran it from and stop.
- If a \`fields\` value is assembled at runtime and cannot be resolved statically: flag it for runtime verification rather than rewriting it blindly.
- If a field is missing from a response after the upgrade: it was stripped by the allowlist. Normalize it, check it against \`/tmp/medusa-allowed.json\`, and take it through step 9 ā do not raise it straight to \`allowFields\`.
- If a field added with \`allowFields\` is still being dropped: the middleware is route-scoped. Check the \`defineMiddlewares\` entry for a \`method\` / \`methods\` key and remove it. Do not edit \`node_modules\`.
- If \`allowFields\` throws \`Cannot read properties of undefined (reading 'push')\`: the running framework is older than 2.21.0 ā the install did not take effect, or the app is running against a stale build.
- If a request starts failing with \`The following fields expand more than the maximum of ... allowed relations\` after you widened the allowlist: the field you allowed exceeds the route's relations-depth limit and was previously being stripped before the check could see it. The message names the offending fields ā reshape them to a shallower selection, and raise \`storeRelationsLimit\` only if the user chooses it.
- If a request fails with \`Order field {field} is not valid\`: the sort field is outside that route's allowlist. Sort on an allowed field, or take the field through step 9.
- If the build, typecheck, plugin build, lint, or migrations fail after your edits: report the exact error output, your best-guess cause tied to a specific change, and stop for the user rather than making speculative further edits.
</error_handling>
<output_format>
Respond in Markdown with these sections, in order:
1. **Detected environment** ā project shape, package manager, current version -> 2.21.0, Node.js version, storefront present (yes/no), plugins that add Store routes or links, custom Store routes, existing \`req.allowed\` middlewares (and whether each is global), and whether \`@medusajs/eslint-plugin\` is configured.
2. **Migration plan (approval required)** ā the intended version bumps, the static inventory of Store API \`fields\` and \`order\` selections (file:line, route, selection, unresolved yes/no), any other breaking change from the release notes, and the migrations to run. State that the dropped-field diff follows the install. End with an explicit request for approval; do not proceed past this section until approved.
3. **Allowlist diff (approval required before widening)** ā a table of findings:
\`\`\`
| # | File:line | Route | Requested field | Status | Proposed remedy |
|---|-----------|-------|-----------------|--------|-----------------|
| 1 | src/lib/data/products.ts:72 | GET /store/products | brand.name | dropped | 4 ā allowFields on /store/products |
| 2 | src/lib/data/cart.ts:26 | GET /store/carts/:id | <expression> | unresolved | runtime verification |
\`\`\`
Followed by the exact \`allowFields\` list per matcher, with a one-line security justification for each path. If nothing was dropped, say "No field selection falls outside the allowlist." and skip the table.
4. **Changes applied** ā bulleted, grouped as: Version bumps / Client field selections / Custom Store routes / \`allowFields\` middlewares / Other breaking changes. Each item: file path + a one-line description. State "none required" for any empty group.
5. **Verification** ā the command run, and PASS/FAIL with relevant output for: dependency install, allowlist enumeration (N configs), the re-run field diff, database migrations, lint (naming \`allow-fields-must-be-global-middleware\` explicitly), backend build, TypeScript typecheck, plugin builds, storefront build (if present), and the runtime smoke check with the *rendered data* confirmed.
6. **Manual follow-ups** ā anything the user must review or decide: unresolved \`fields\` values to verify at runtime, storefronts living in another repo that need the same audit, allowlist additions declined pending their decision, and other breaking changes surfaced from the release notes. "None" if empty.
</output_format>
<success_criteria>
- Every \`@medusajs/*\` dependency resolves to its v2.21.0-aligned version, verified against the registry, and the install completes without dependency errors.
- The allowlist enumeration ran against the upgraded \`node_modules\` and reported the Store query configs that now carry an \`allowed\` list.
- Every \`fields\` and \`order\` selection in every in-repo Store API client has been traced to a literal string or explicitly reported as unresolved, and checked by exact membership against \`/tmp/medusa-allowed.json\` ā not against any list in this prompt.
- No finding was fixed by widening the allowlist when remedies 1-3 genuinely fit, and no allowlist was widened without the user's explicit approval.
- Every middleware that adds to \`req.allowed\` is registered globally with no \`method\` / \`methods\` key, uses \`allowFields\`, and was proven to run before \`validateAndTransformQuery\` (lint clean, or the \`req.queryConfig\` guard did not throw).
- No allowlist entry added traverses into another shopper's order, customer, cart, or payment data.
- Database migrations run to completion; backend build, TypeScript typecheck, lint, and every plugin build pass; the storefront build passes if a storefront is present.
- The runtime smoke check confirmed rendered data, not just \`200\` responses, on listing, product detail, cart, checkout, and account/orders.
- No secrets appear in edits or output; no files under \`node_modules\` were modified; no unrelated files were changed; no commits or pushes were made.
- No file is edited before user approval, and the response contains all six required sections.
</success_criteria>
```
</details>
### Exported Admin Dashboard Components, Hooks, and Utilities
The Medusa Admin dashboard now exports the building blocks it uses internally, so admin customizations can match the dashboard's look and behavior instead of reimplementing it. They are available under three subpaths of `@medusajs/dashboard`:
```ts
import { DataTable, RouteFocusModal, Combobox } from "@medusajs/dashboard/components"
import { useProducts, useComboboxData, useQueryParams } from "@medusajs/dashboard/hooks"
import { formatCurrency, getFormattedAddress } from "@medusajs/dashboard/lib"
```
- `@medusajs/dashboard/components` exposes layout and table primitives such as `LayoutComposer`, `DataTable`, `ConfigurableDataTable`, and `DataGrid`, modals such as `RouteDrawer`, `RouteFocusModal`, `StackedDrawer`, and `StackedFocusModal`, inputs such as `Combobox`, `CountrySelect`, and `HandleInput`, and common pieces such as `ActionMenu`, `Form`, `SectionRow`, `Thumbnail`, `NoRecords`, and the product table cells.
- `@medusajs/dashboard/hooks` exposes the dashboard's data hooks and query keys for customers, orders, products, product variants, promotions, regions, sales channels, shipping options, price preferences, store, and users, along with utility hooks such as `useComboboxData`, `useDate`, `useDebouncedSearch`, and `useQueryParams`.
- `@medusajs/dashboard/lib` exposes helpers for tables, search entities, currency and money formatting, validation schemas, and address formatting.
The full reference of every exported component, hook, and utility is at [docs.medusajs.com/resources/admin-components](https://docs.medusajs.com/resources/admin-components).
## Features
* feat(dashboard,js-sdk): export common components, hooks by [@leobenzol](https://github.com/leobenzol) in [#16506](https://github.com/medusajs/medusa/pull/16506)
* feat(framework,medusa): allow overriding a route's disallowed query fields from a global middleware by [@shahednasser](https://github.com/shahednasser) in [#16716](https://github.com/medusajs/medusa/pull/16716)
* feat(eslint-plugin): add rules for req.allowed, allowFields, and req.disallowed by [@shahednasser](https://github.com/shahednasser) in [#16778](https://github.com/medusajs/medusa/pull/16778)
* feat(dashboard, types, medusa): add support for metadata management for promotions by [@shahednasser](https://github.com/shahednasser) in [#16719](https://github.com/medusajs/medusa/pull/16719)
* feat(loyalty-plugin): add a validate hook to addGiftCardToCartWorkflow by [@shahednasser](https://github.com/shahednasser) in [#16755](https://github.com/medusajs/medusa/pull/16755)
* feat: add search helpers, add default search endpoint by [@sradevski](https://github.com/sradevski) in [#16762](https://github.com/medusajs/medusa/pull/16762)
* feat: use cross-module joins instead of index module by [@sradevski](https://github.com/sradevski) in [#16156](https://github.com/medusajs/medusa/pull/16156)
* feat: implement a provider-agnostic swap index by [@sradevski](https://github.com/sradevski) in [#16710](https://github.com/medusajs/medusa/pull/16710)
* feat: implement catchup phase for seeding and reindexing by [@sradevski](https://github.com/sradevski) in [#16726](https://github.com/medusajs/medusa/pull/16726)
* feat: make reindex timestamp configurable by [@sradevski](https://github.com/sradevski) in [#16753](https://github.com/medusajs/medusa/pull/16753)
## Bugs
* fix(payment): correctly persist the acting user on payment captures by [@shahednasser](https://github.com/shahednasser) in [#16750](https://github.com/medusajs/medusa/pull/16750)
* fix(product, utils, medusa): return published products only for tags, collections, categories, and type store routes by [@shahednasser](https://github.com/shahednasser) in [#16674](https://github.com/medusajs/medusa/pull/16674)
* fix(core-flows): only autocapture if payment is authorized successfully by [@shahednasser](https://github.com/shahednasser) in [#16722](https://github.com/medusajs/medusa/pull/16722)
* fix(core-flows): validate cart currency_code against the region's currency on create by [@shahednasser](https://github.com/shahednasser) in [#16129](https://github.com/medusajs/medusa/pull/16129)
* fix(notification): forward the shared context into getProviderForChannels by [@shahednasser](https://github.com/shahednasser) in [#16721](https://github.com/medusajs/medusa/pull/16721)
* fix(framework): mark migration scripts as completed only after successful run by [@shahednasser](https://github.com/shahednasser) in [#16351](https://github.com/medusajs/medusa/pull/16351)
* fix(framework): stabilize stateful path regexes by [@DS123-ally](https://github.com/DS123-ally) in [#16370](https://github.com/medusajs/medusa/pull/16370)
* fix(promotion): apply buy-get promotions independently of line item order by [@danlapteacru](https://github.com/danlapteacru) in [#16397](https://github.com/medusajs/medusa/pull/16397)
* fix(fulfillment): compare numeric shipping option rule values numerically by [@shuvamk](https://github.com/shuvamk) in [#16301](https://github.com/medusajs/medusa/pull/16301)
* fix(event-bus-redis): move EXPIRE into same pipeline as RPUSH by [@vansh17June](https://github.com/vansh17June) in [#16563](https://github.com/medusajs/medusa/pull/16563)
* fix(utils): honor decimalPlaces of 0 in MathBN.convert by [@Arunendra21](https://github.com/Arunendra21) in [#16342](https://github.com/medusajs/medusa/pull/16342)
* fix(utils): title-case script subtags in two-segment locales by [@tushardev-365](https://github.com/tushardev-365) in [#16456](https://github.com/medusajs/medusa/pull/16456)
* fix(loyalty-plugin): compensate the workflow each order hook actually ran by [@shahednasser](https://github.com/shahednasser) in [#16754](https://github.com/medusajs/medusa/pull/16754)
* fix(loyalty-plugin): exclude soft-deleted transactions from store credit balances by [@shahednasser](https://github.com/shahednasser) in [#16757](https://github.com/medusajs/medusa/pull/16757)
* fix(loyalty-plugin): honor a store credit amount of 0 on a cart by [@lazerg](https://github.com/lazerg) in [#16378](https://github.com/medusajs/medusa/pull/16378)
* fix(dashboard): add missing Polish translations by [@owlcode](https://github.com/owlcode) in [#16763](https://github.com/medusajs/medusa/pull/16763)
* fix(dashboard): strip table query prefix in product tag list loader by [@lazerg](https://github.com/lazerg) in [#16393](https://github.com/medusajs/medusa/pull/16393)
* fix(dashboard): add global add-row action to metadata editor by [@lazerg](https://github.com/lazerg) in [#16288](https://github.com/medusajs/medusa/pull/16288)
* fix(dashboard): preserve inventory kit variant indices by [@sansynx](https://github.com/sansynx) in [#16366](https://github.com/medusajs/medusa/pull/16366)
* fix(docs-ui): MainNav z-index overlap, duplicate GA key, and Windows broken-link-checker path bug by [@vjymisal0](https://github.com/vjymisal0) in [#16362](https://github.com/medusajs/medusa/pull/16362)
* fix: improve seed lock handling by [@sradevski](https://github.com/sradevski) in [#16781](https://github.com/medusajs/medusa/pull/16781)
* fix: resolve several issues with the locking implementation by [@sradevski](https://github.com/sradevski) in [#16768](https://github.com/medusajs/medusa/pull/16768)
* fix: search improvements by [@sradevski](https://github.com/sradevski) in [#16774](https://github.com/medusajs/medusa/pull/16774)
* fix: apply highlighting and typos more gracefully by [@sradevski](https://github.com/sradevski) in [#16725](https://github.com/medusajs/medusa/pull/16725)
* fix: changeset for loyalty by [@sradevski](https://github.com/sradevski) in [#16782](https://github.com/medusajs/medusa/pull/16782)
## Documentation
* docs: add troubleshooting for cloud projects by [@shahednasser](https://github.com/shahednasser) in [#16772](https://github.com/medusajs/medusa/pull/16772)
* docs: add pagination and filters to api reference versions route by [@shahednasser](https://github.com/shahednasser) in [#16758](https://github.com/medusajs/medusa/pull/16758)
* docs: clarify disabling body parser and remove bloom suggestion by [@shahednasser](https://github.com/shahednasser) in [#16723](https://github.com/medusajs/medusa/pull/16723)
* docs: fix loyalty plugin workflow import paths to use /workflows subpath by [@shahednasser](https://github.com/shahednasser) in [#16735](https://github.com/medusajs/medusa/pull/16735)
* docs-utils: extract allowed list from API routes into OAS by [@shahednasser](https://github.com/shahednasser) in [#16767](https://github.com/medusajs/medusa/pull/16767)
* docs: fix build by [@shahednasser](https://github.com/shahednasser) in [#16771](https://github.com/medusajs/medusa/pull/16771)
## Chores
* chore(medusa): revert using cross-modules back to index module by [@shahednasser](https://github.com/shahednasser) in [#16787](https://github.com/medusajs/medusa/pull/16787)
* chore(framework, medusa): set a strict allowed list for store API routes by [@shahednasser](https://github.com/shahednasser) in [#16702](https://github.com/medusajs/medusa/pull/16702)
* chore(dashboard,medusa): name admin search entities after their graph entity by [@NicolasGorga](https://github.com/NicolasGorga) in [#16587](https://github.com/medusajs/medusa/pull/16587)
* chore(create-medusa-app): remove unused node-fetch dependency by [@shahednasser](https://github.com/shahednasser) in [#16780](https://github.com/medusajs/medusa/pull/16780)
* chore(create-medusa-app): change Next.js starter wording by [@NicolasGorga](https://github.com/NicolasGorga) in [#16712](https://github.com/medusajs/medusa/pull/16712)
* chore: resolve dependabot alert for js-yaml by [@shahednasser](https://github.com/shahednasser) in [#16773](https://github.com/medusajs/medusa/pull/16773)
* chore(deps): bump the security-updates group across 1 directory with 4 updates by [@dependabot](https://github.com/dependabot) in [#16766](https://github.com/medusajs/medusa/pull/16766)
* chore(deps): bump multer by [@dependabot](https://github.com/dependabot) in [#16764](https://github.com/medusajs/medusa/pull/16764)
* chore(docs): automated cloud documentation update by [@shahednasser](https://github.com/shahednasser) in [#16779](https://github.com/medusajs/medusa/pull/16779)
* chore(docs): update version in documentation (automated) by [@github-actions](https://github.com/features/actions) in [#16711](https://github.com/medusajs/medusa/pull/16711)
* chore(docs): updated API Reference (automated) by [@github-actions](https://github.com/features/actions) in [#16709](https://github.com/medusajs/medusa/pull/16709)
* chore(docs): generated References (automated) by [@github-actions](https://github.com/features/actions) in [#16708](https://github.com/medusajs/medusa/pull/16708)
* chore(docs): generated and updated UI Reference (automated) by [@github-actions](https://github.com/features/actions) in [#16707](https://github.com/medusajs/medusa/pull/16707)
* chore(docs): generated DML JSON files (automated) by [@github-actions](https://github.com/features/actions) in [#16706](https://github.com/medusajs/medusa/pull/16706)
## New Contributors
* @sansynx made their first contribution in [#16366](https://github.com/medusajs/medusa/pull/16366)
* @Arunendra21 made their first contribution in [#16342](https://github.com/medusajs/medusa/pull/16342)
* @tushardev-365 made their first contribution in [#16456](https://github.com/medusajs/medusa/pull/16456)
* @vjymisal0 made their first contribution in [#16362](https://github.com/medusajs/medusa/pull/16362)
* @danlapteacru made their first contribution in [#16397](https://github.com/medusajs/medusa/pull/16397)
* @owlcode made their first contribution in [#16763](https://github.com/medusajs/medusa/pull/16763)
**Full Changelog**: [v2.20.1...v2.21.0](https://github.com/medusajs/medusa/compare/v2.20.1...v2.21.0)
Related Repositories
openclaw/openclawā
392,714
The AI that really does things. Any OS. Any Platform. The lobster way. š¦
nilbuild/developer-roadmapā
370,095
Interactive roadmaps, guides and other educational content to help developers grow in their careers.
morluto/reaā
360,215
Reverse engineer anything with agents, from app behavior down to native binaries.
anomalyco/opencodeā
219,858
The open source coding agent.
n8n-io/n8nā
208,278
Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.