npm @medusajs/medusa 2.20.0
v2.20.0: Security Fixes, Performance Improvements, Calculated Shipping Options in Draft Orders

2 hours ago

Highlights

Medusa MCP users can update their project using the following prompt:

Update my Medusa project to v2.20.0

Payment providers are now validated against the cart's region

Security fix: creating a payment session no longer trusts the payment provider ID passed in the request. The createPaymentSessionsWorkflow and the cart payment validation step now verify that the chosen provider is actually enabled in the cart's (or payment collection's) region, and reject the request otherwise. Previously a store customer could initiate a payment session with any payment provider installed in the application, even one that was not linked to their region.

No action is required, but if your storefront relied on passing an arbitrary provider ID, make sure the provider is linked to the region the cart belongs to.


MFA Challenge Enforcement on MFA Routes

Security fix: An attacker who already had a user's valid password could use that token to regenerate the recovery codes through POST /auth/mfa/recovery-codes and use one of them to complete the challenge, or enroll and delete MFA factors. This releases fixes this issue by tracking whether a user has completed MFA before allowing them to access the recovery codes route.


Relations Limit on Store API Routes

🚧 Breaking change

This only affects Store API requests that expand deeply nested relations through fields. Admin routes are unaffected.

Store API routes now reject requests that expand more than three levels of relations in a single query by default, since deeply nested expansions can cause bad performance. A request such as fields=*products.variants.options.values now returns a 400 error listing the offending fields.

The product routes are the only core Store routes that raise the limit above the default, allowing four levels of relations:

  • GET /store/products
  • GET /store/products/:id

To change the limit for every Store route, set storeRelationsLimit in your HTTP configuration:

module.exports = defineConfig({
  projectConfig: {
    http: {
      storeRelationsLimit: 4,
    },
  },
})

To change it for a specific route, set storeRelationsLimit in the query configuration passed to validateAndTransformQuery in your middlewares. It takes precedence over the application-wide configuration:

import { defineMiddlewares, validateAndTransformQuery } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/custom",
      method: "GET",
      middlewares: [
        validateAndTransformQuery(GetCustomSchema, {
          defaults: ["id", "*items"],
          storeRelationsLimit: 5,
          isList: true,
        }),
      ],
    },
  ],
})

Redis Caching Storage Format

This only affects projects using the @medusajs/caching-redis provider.

The Redis caching provider now stores entries under a 64-bit hash key and uses a simplified storage mechanism. Existing cache entries written by earlier versions are not readable in the new format and are effectively invalidated on upgrade, so expect a cold cache after deploying. No configuration change is required.


Fractional Inventory Quantities and Units of Measure

Inventory items can now be tracked in fractional quantities with an associated unit of measure, unlocking use cases such as selling by weight, length, or volume. The support spans the Inventory Module, link modules, API routes, and workflows, and the admin dashboard surfaces the unit of measure across the inventory screens.


Calculated Shipping Options in Draft Orders

Draft orders now support shipping options with calculated prices, so third-party rate providers can be used when an operator builds an order manually. A new setCalculatedShippingPricingContext hook lets you inject additional data into the pricing context sent to the fulfillment provider when calculating those rates.


Breaking Changes

  • Relations limit on store routes — Store API routes now enforce a default relations limit to cap query depth and improve performance. Requests that relied on unbounded relation expansion may see fewer nested associations returned. Run db:migrate after upgrading. (#16688)

  • In-memory local search provider removed@medusajs/search-local (the Orama-backed in-memory provider shipped in v2.19) has been removed. Projects must switch to @medusajs/search-postgres, the Medusa Cloud provider, or a custom provider. Search indexes are no longer created at application startup; run db:migrate to create them. (#16545)

  • searchMany moved to the provider interface — The searchMany method has been pushed down to the search provider interface so each provider can optimize batch queries internally. Custom search providers must implement searchMany. (#16643)

  • MFA routes now require a completed challenge — MFA-protected routes now validate that the MFA challenge has actually been completed before granting access, hardening the authentication flow. (#16610)


Other Changes

Features

  • Vector-based search for Medusa Search — Added foundational vector search support including namespace management, vector ingestion, and improved search-definition APIs. (#16631, #16673, #16676)

  • ESLint plugin: new workflow rules — Three new rules for @medusajs/eslint-plugin: no-nested-when-then catches nested when/then blocks that silently do not compose correctly; missing-when-name requires all when calls to have a name argument for better traceability; throw-in-workflow-definition disallows throw statements in workflow definition functions, which must use MedusaError in steps instead. (#16520, #16585, #16517)

Bug Fixes

  • Payment provider validation on cart — Validate that the payment provider selected for a cart actually belongs to the cart's region before processing. (#16690)

  • Store order routes field expansion — Blocked field-expansion pivot paths on store order routes to prevent information leakage. (#16480)

  • Product category scalar fields — Return all scalar fields from product category queries when no select is specified. (#16493)

  • Variant fields for inventory quantity — Automatically select required variant fields when inventory quantity is requested; handle null variants gracefully. (#16444)

  • Order shipping method fields — Select shipping method fields correctly when listing orders with totals. (#16241)

  • Credit line totals — Preserve net negative credit line totals instead of zeroing them out. (#16303)

  • OAuth state cache — Await the OAuth state cache write so the state is persisted before the redirect completes. (#16571)

  • Redis caching — Fixed the Redis provider overwriting keys on set; await set and clear calls in the caching module; removed an unnecessary quadratic computation in the caching layer. (#16549, #16547, #16496)

  • created_by on fulfillment/shipment — The actor ID is now correctly recorded as created_by when creating fulfillments and shipments. (#16582)

  • 4xx span status — 4xx HTTP responses no longer set the OpenTelemetry span status to Error, reducing noise in tracing dashboards. (#16561)

  • Draft order item change deletion — Fixed inability to delete pending draft order item change actions from the dashboard. (#16616)

  • Create fulfillment form pagination — Fixed select pagination issues in the create-fulfillment form. (#16495)

  • Duplicate query in product — Removed a duplicate query call in the product module. (#16530)

  • Promotion campaign budget type — Campaign budget type is now correctly included in promotion status columns. (#16254)

  • Isolated request allowed fields — Request allowedFields are now isolated per-request so one request cannot bleed its field allowlist into another. (#16313)

  • Admin bundler zoom — The generated admin HTML no longer blocks browser zoom. (#16449)

  • HTML lang attribute — The admin dashboard now sets the correct lang attribute on the <html> element. (#16450)

  • Deleted stock locations in fulfillment — The order fulfillment section now handles deleted stock locations without crashing. (#16006)

  • CSV export columns — Fixed column output for order CSV exports. (#16216)

  • Workflow execution state column — The workflow execution state column in the dashboard now has its own translation key to avoid conflicts. (#16594)

  • Loyalty plugin — Fixed account locking on debit, gift card expiry date validation, and gift card product editing following global product-option changes. (#16347, #16348, #16475)

  • Test utilitiesprocess.env is now restored after test runner cleanup so environment mutations do not leak between test suites. (#16273)

Translations

  • Updated Croatian admin translations. (#16675)
  • Updated Russian (ru) admin translations: added missing keys, plural forms (_few/_many), and fixed mistranslations. (#16402)
  • Fixed missing Polish plural forms in dashboard translations. (#16638)

Contributors

Thank you to all the contributors who made this release possible:

Don't miss a new medusa release

NewReleases is sending notifications on new releases.