diff --git a/content/configuration/1.general.md b/content/configuration/1.general.md index a13b58ca..66f1df11 100644 --- a/content/configuration/1.general.md +++ b/content/configuration/1.general.md @@ -32,8 +32,8 @@ See [Licensing](/licensing/overview) for a full explanation of license keys, lic | Variable | Description | Default Value | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `LICENSE_KEY` | License key in the format `DXXXX-XXXXX-XXXXX-XXXXX-XXXXC`. Activated against the Directus licensing service on first use and revalidated periodically. Takes precedence over `LICENSE_TOKEN` and any key set through the Studio. | | -| `LICENSE_TOKEN` | Pre-issued license token for offline use. Validated locally on each startup and never contacts the licensing service. Mutually exclusive with `LICENSE_KEY` — setting both is a configuration error and Directus will refuse to start. | | +| `LICENSE_KEY` | License key in the format `DXXXX-XXXXX-XXXXX-XXXXX-XXXXC`. Activated against the Directus licensing service on first use and revalidated periodically. Takes precedence over `LICENSE_TOKEN` and any key set through the Studio. If activation or revalidation fails, Directus keeps running on its current license, or on the core tier if none is in effect, and retries automatically. | | +| `LICENSE_TOKEN` | Pre-issued license token for offline use. Validated locally on startup and on each scheduled license check, and never contacts the licensing service. Mutually exclusive with `LICENSE_KEY`. Setting both is a configuration error and Directus will refuse to start. | | | `LICENSE_KEY_MANAGEMENT_ENABLED` | Whether the license key can be managed in the Studio and via the API. Set to `false` to prevent the license from being activated, updated, or deactivated. | `true` | ::callout{icon="i-lucide-info"} diff --git a/content/configuration/cache.md b/content/configuration/cache.md index c127f40a..2d565bd0 100644 --- a/content/configuration/cache.md +++ b/content/configuration/cache.md @@ -38,8 +38,7 @@ than you would cache database content. To learn more, see [Files](/configuration | `CACHE_AUTO_PURGE_IGNORE_LIST`[3] | List of collections that prevent cache purging when `CACHE_AUTO_PURGE` is enabled. | `directus_activity,directus_presets` | | `CACHE_SYSTEM_TTL`[4][5] | How long `CACHE_SCHEMA` is persisted. | -- | | `CACHE_SCHEMA`[4] | Whether or not the database schema is cached. One of `false`, `true` | `true` | -| `CACHE_SCHEMA_MAX_ITERATIONS`[4] | Safe value to limit max iterations on get schema cache. This value should only be adjusted for high scaling applications. | `100` | -| `CACHE_SCHEMA_SYNC_TIMEOUT` | How long to wait for other containers to message before trying again | `10000` | +| `CACHE_SCHEMA_SYNC_TIMEOUT` | Maximum time, in milliseconds, to wait for the schema to finish building. Requests fail if the timeout is exceeded. | `60000` | | `CACHE_SCHEMA_FREEZE_ENABLED` | Whether or not to freeze the schema to improve memory efficiency | false | | `CACHE_NAMESPACE`[5] | How to scope the cache data. | `system-cache` | | `CACHE_STORE`[5] | Where to store the cache data. Either `memory`, `redis`. | `memory` | diff --git a/content/configuration/security-limits.md b/content/configuration/security-limits.md index da34e2ef..946fef42 100644 --- a/content/configuration/security-limits.md +++ b/content/configuration/security-limits.md @@ -34,7 +34,7 @@ This page documents environment variables. For in-app security configuration (pe | `USER_REGISTER_URL_ALLOW_LIST` | List of URLs that can be used as `verification_url` in the `/users/register` endpoint. | | | `IP_TRUST_PROXY` | Settings for the Express.js trust proxy setting. | false | | `IP_CUSTOM_HEADER` | What custom request header to use for the IP address. | false | -| `IMPORT_IP_DENY_LIST`[2] | Deny importing files from these IP addresses / IP ranges / CIDR blocks. Use `0.0.0.0` to match any local IP address. | `0.0.0.0,169.254.169.254` | +| `IMPORT_IP_DENY_LIST`[2] | Deny outbound requests, such as file imports and the **Request URL** flow operation, to these IP addresses, IP ranges (`10.0.0.1-10.0.0.50`), or CIDR blocks (`10.0.0.0/8`). Use `0.0.0.0` to match any local IP address. | `0.0.0.0,169.254.169.254` | | `HSTS_ENABLED` | Enable the Strict-Transport-Security policy header. When enabled, Directus will send the `Strict-Transport-Security: max-age=15552000; includeSubDomains` header on all responses. | `false` | | `HSTS_*` | Custom overrides for the Strict-Transport-Security header. See [helmet's documentation](https://helmetjs.github.io). Example: `HSTS_MAX_AGE=63072000` | | @@ -42,7 +42,7 @@ This page documents environment variables. For in-app security configuration (pe restarts or horizontally scaled deployments. Must be explicitly set to a secure random value in production. [2] localhost can get resolved to `::1` as well as `127.0.0.1` depending on the system - ensure to include -both if you want to specifically block localhost. +both if you want to specifically block localhost. Wildcards (`10.0.0.*`), netmasks (`10.0.0.0/255.255.255.0`), short ranges (`10.0.0.1-50`), and malformed prefixes (`10.0.0.0/ 24`) are invalid. Directus checks `IMPORT_IP_DENY_LIST` each time it makes an outbound request. If any entry is invalid, Directus logs a warning and denies every outbound request. Browsers are pretty strict when it comes to third-party cookies. If you're running into unexpected problems when running your project and API on different domains, make sure to verify your configuration for `REFRESH_TOKEN_COOKIE_NAME`, `REFRESH_TOKEN_COOKIE_SECURE`, and `REFRESH_TOKEN_COOKIE_SAME_SITE`. diff --git a/content/getting-started/10.accessibility.md b/content/getting-started/10.accessibility.md index 2f599614..8bda29c2 100644 --- a/content/getting-started/10.accessibility.md +++ b/content/getting-started/10.accessibility.md @@ -21,7 +21,7 @@ Special shortcuts for the WYSIWYG editor: - Use `Tab` to move focus into and through the toolbar, and arrow keys to move between adjacent tools. - Press `Esc` to return focus to the editor content area. -- Standard formatting shortcuts apply in the content area, for example `meta` + `b` (bold), `meta` + `i` (italic), and `meta` + `k` (insert link). +- Standard formatting shortcuts apply in the content area, for example `meta` + `b` (bold), `meta` + `i` (italic), and `meta` + `k` (insert a link, or link a selected image). diff --git a/content/guides/01.data-model/4.rich-text.md b/content/guides/01.data-model/4.rich-text.md index 7700b26e..c1a42e73 100644 --- a/content/guides/01.data-model/4.rich-text.md +++ b/content/guides/01.data-model/4.rich-text.md @@ -271,6 +271,28 @@ Behavior worth knowing: - Deleting the image out of a figure removes the orphaned caption too. - Stored `
` and `
` markup round-trips, including a caption placed before the image and a figure holding only a caption. +## Linked images + +To link an image, select it and use the **Add/Edit Link** toolbar button or `meta` + `k`. In the link drawer, enter the **URL**. **Tooltip** sets the image's `title`, and **New Tab** opens the link in a new tab. The drawer hides **Display Text** for images. To remove the link, use **Remove Link**. + +The image is stored wrapped in an anchor. This also works inside a captioned `
`: + +```html +A wind turbine +``` + +The editor keeps `href`, `target`, and `rel` on the wrapping ``. Other attributes on that anchor, such as `class` or `data-*`, are not supported and trigger the [read-only normalization notice](/releases/breaking-changes/version-12#content-is-normalized-on-first-edit). Clicking a linked image in the editor selects it rather than following the link. + +## Pasting content + +Some apps, such as Word and Figma, wrap copied content in markup the editor does not support. When you paste content like this, the editor inserts a cleaned version that keeps your text and supported formatting, and shows a notice above the field. Select **See what was removed** to view a diff and choose an option: + +- **Keep Cleaned** keeps the cleaned paste. +- **Undo Paste** removes the paste. +- **Paste Raw** replaces the cleaned paste with the HTML exactly as copied and switches the field to the raw HTML editor. If the cursor was inside a paragraph, the paragraph is split around the pasted HTML. + +**Undo Paste** and **Paste Raw** are only available until you make another change. Pasting plain text, or HTML the editor supports, never shows the notice. + ## Styling the output on your frontend Directus stores the HTML. Rendering and styling it is your frontend's job. diff --git a/content/guides/03.auth/2.access-control.md b/content/guides/03.auth/2.access-control.md index e61d53e6..299cca61 100644 --- a/content/guides/03.auth/2.access-control.md +++ b/content/guides/03.auth/2.access-control.md @@ -101,6 +101,8 @@ An admin can set the user status. Only the active state is able to authenticate, A policy can also have a specific allowlist of IP addresses, IP ranges, and CIDR blocks which allow access. Leave this empty to allow all IP addresses. +Each entry must be an IPv4 or IPv6 address (`192.168.1.10`), a range of full addresses (`10.0.0.1-10.0.0.50`), or a CIDR block (`10.0.0.0/24`). Directus rejects wildcards (`10.0.0.*`), netmasks, short ranges (`10.0.0.1-50`), and malformed prefixes such as `10.0.0.0/ 24`. + IP access is configured at the individual policy level, meaning each policy maintains its own independent set of IP restrictions. This granular approach provides several advantages: - Each policy's IP allowlist operates independently without affecting other policies in your system. @@ -163,6 +165,8 @@ The combined rule becomes: `(user_id = current_user.id) OR (department = current IP access restrictions follow a subtractive model, meaning that if a user's IP address doesn't match the allowlist, that entire policy is removed from the evaluation chain. +This also applies to **Require 2FA**. A policy that requires two-factor authentication only requires it when the user logs in from an IP address on that policy's allowlist. + **How It Works**: When a request arrives, the system first checks the requesting IP address against each policy's IP allowlist. Any policy whose IP restrictions are not satisfied is immediately excluded from further evaluation. Only policies that pass the IP access check remain active for that request. **Practical Example**: Consider a user assigned three policies: diff --git a/content/guides/03.auth/5.2fa.md b/content/guides/03.auth/5.2fa.md index 3312ac15..13a6803e 100644 --- a/content/guides/03.auth/5.2fa.md +++ b/content/guides/03.auth/5.2fa.md @@ -6,7 +6,9 @@ navigation: title: Two-Factor Auth --- -Two-factor authentication (2FA) in Directus is a security measure that requires a generated one-use code to be provided after log in to complete authentication. 2FA for the Data Studio can be enabled or enforced in the user page. A one-time password (OTP) is required when logging in via the Data Studio or API. +Two-factor authentication (2FA) in Directus is a security measure that requires a generated one-use code to be provided after log in to complete authentication. Users can enable 2FA from their user page. A one-time password (OTP) is required when logging in via the Data Studio or API. + +To require 2FA, enable **Require 2FA** on a [policy](/guides/auth/access-control). Directus enforces 2FA if at least one of the user's policies has it enabled. A policy with an IP allowlist only requires 2FA when the user logs in from an allowed IP address. Users who have not set up 2FA are redirected to set it up after logging in, including after SSO logins. To enable 2FA, you will need an external authenticator app or support for OTPs in your password manager. diff --git a/content/guides/04.connect/5.errors.md b/content/guides/04.connect/5.errors.md index 6ed4ef17..0b8fb97e 100644 --- a/content/guides/04.connect/5.errors.md +++ b/content/guides/04.connect/5.errors.md @@ -101,6 +101,7 @@ The `code` value in `extensions` lets you handle errors programmatically without | `INVALID_PROVIDER_CONFIG` | 503 | The authentication provider is misconfigured. | | `INVALID_QUERY` | 400 | The query parameters can't be used as provided. | | `INVALID_TOKEN` | 403 | The access token is malformed or invalid. | +| `LICENSE_INVALID` | 400 | The license key is invalid. `extensions.failure` gives the cause. | | `LIMIT_EXCEEDED` | 403 | A configured limit (relations, depth, etc.) was exceeded. | | `METHOD_NOT_ALLOWED` | 405 | The HTTP method isn't allowed on this endpoint. | | `NOT_NULL_VIOLATION` | 400 | A required field was submitted as null. | diff --git a/content/guides/05.files/1.upload.md b/content/guides/05.files/1.upload.md index 862f683c..21466a43 100644 --- a/content/guides/05.files/1.upload.md +++ b/content/guides/05.files/1.upload.md @@ -61,5 +61,5 @@ The file contents has to be provided in a property called `file`. All other prop the file object can be provided as well, except `filename_disk` and `filename_download`. ::callout{icon="i-lucide-info" color="info"} -If `storage` is not specified, it defaults to the first location listed in [`STORAGE_LOCATIONS`](/configuration/files#storage-locations). +If `storage` is not specified, it defaults to the first location listed in [`STORAGE_LOCATIONS`](/configuration/files#storage-locations). A `storage` value must match one of the configured locations exactly, including case. Other values are rejected with an `INVALID_PAYLOAD` error. :: diff --git a/content/guides/06.flows/5.manage-flows.md b/content/guides/06.flows/5.manage-flows.md index 6db0dd2a..69ad1f3a 100644 --- a/content/guides/06.flows/5.manage-flows.md +++ b/content/guides/06.flows/5.manage-flows.md @@ -1,10 +1,10 @@ --- stableId: a953ba99-dea5-44a2-8c7b-176180759fe9 title: Manage Flows -description: Organize, find, duplicate, import, and export Flows from the Flows module. +description: Organize, find, activate, duplicate, import, and export Flows from the Flows module. --- -Flows have their own module in the module bar. The Flows page lists every Flow in a table with its folder, status, trigger type, name, and description. From here you can organize Flows into folders, search and filter the list, duplicate a Flow, and import and export Flows. +Flows have their own module in the module bar. The Flows page lists every Flow in a table with its folder, status, trigger type, name, and description. From here you can organize Flows into folders, search and filter the list, activate or deactivate Flows, duplicate a Flow, and import and export Flows. ::callout{icon="i-lucide-info"} Flows used to live in the Settings module. Links to the old location redirect to the Flows module, so existing bookmarks keep working. @@ -30,6 +30,14 @@ Deleting a folder never deletes the Flows inside it. Choose **Delete this folder Use the search box in the header bar to find Flows by name. Select the filter icon to add conditions on Flow fields such as **Status**, **Trigger**, or the folder's **Name**. +## Activate or Deactivate Flows + +Only active Flows run. To change the status of one Flow, open its context menu and select **Set Flow to Active** or **Set Flow to Inactive**. + +To change several Flows at once, select them in the table. The header bar shows **Set Flows to Active** when at least one selected Flow is inactive, and **Set Flows to Inactive** when at least one is active. A mixed selection shows both buttons. Directus only updates the Flows whose status changes. + +Both buttons require update permission on `directus_flows`. + ## Duplicate a Flow To copy a Flow together with its trigger configuration and all of its operations, open the Flow's context menu and select **Duplicate Flow**. Enter a name for the copy and select **Duplicate**. The copy is placed in the same folder as the original. diff --git a/content/licensing/1.overview.md b/content/licensing/1.overview.md index 88ab02f9..0908a4fd 100644 --- a/content/licensing/1.overview.md +++ b/content/licensing/1.overview.md @@ -93,6 +93,10 @@ You configure one or the other — not both. Setting both in your environment is In online mode, your instance periodically revalidates with the Directus licensing service. Feature access, plan upgrades, and revocations propagate without restarting. Adding a license key through the Studio validates instantly. +Revalidation runs on a schedule set by the license's settings, at least every 12 hours. If a key is set but no license is in effect, for example because it could not be applied during startup, Directus retries every hour. + +If a revalidation request fails, your instance keeps its current license and retries on the next scheduled check. Admins see a warning in **Settings > License** that explains why the license could not be renewed and the date it lapses. If renewal still fails by that date, the instance moves to the core tier. Your license key is kept, so the license is restored automatically if a check succeeds before downgrade. + This is the default mode and is appropriate for any instance with outbound network access. ### Offline Mode @@ -156,6 +160,21 @@ If outbound access to `https://licensing.directus.com` is not possible, use an [ **Enforcement never deletes data.** When limits are exceeded or a license is downgraded, Directus restricts access by deactivating resources or blocking endpoints — your data is never removed. :: +### Why a License Could Not Be Renewed + +The `GET /license` endpoint returns an `invalid_reason` field and a `token_expires_at` field. `invalid_reason` explains why the last license request failed, both for a license that is still in effect and for one that has lapsed. `token_expires_at` is the Unix timestamp when the current license lapses. `invalid_reason` is `null` once a license request succeeds. + +| Value | Meaning | +| ------------------ | ----------------------------------------------------------------------------- | +| `verification` | The stored license token could not be verified. | +| `expired` | The license has expired. | +| `canceled` | The subscription was canceled. | +| `suspended` | The license was suspended. | +| `invalid_key` | The licensing service did not recognize the key. | +| `activation_limit` | The license has no activations left. | +| `binding_mismatch` | The key is bound to another project or `PUBLIC_URL`. | +| `unavailable` | The licensing service could not be reached. This is usually temporary. | + ## Managing Your License Admins can manage their license from **Settings > License** in the Studio. From this screen you can: @@ -180,6 +199,8 @@ Deactivating a license frees its activation on the licensing service so the bind - **Studio-managed license** — open **Settings > License** in the Studio and click **Deactivate License**. The instance drops to the core tier immediately. - **Env-managed license** — remove the `LICENSE_KEY` (or `LICENSE_TOKEN`) value from your environment and restart Directus. The Studio license editor becomes available, but the previous plan and entitlements remain displayed until you click **Deactivate License**. Removing the env var alone does not free the activation on the licensing service — the deactivate action does. +If the licensing service no longer recognizes the stored key, or the key is bound to another project, Directus removes the key locally and still completes the deactivation. + If your current usage is above core tier limits when you deactivate, the Studio will surface the [Resolution Flow](#resolution-flow) before completing the downgrade. ::callout{icon="i-lucide-triangle-alert" color="warning"} @@ -197,7 +218,7 @@ To move an instance to a new URL: 3. Re-apply your license key. This registers a new activation against the new URL. ::callout{icon="i-lucide-triangle-alert" color="warning"} -**Changing `PUBLIC_URL` without re-applying your license eventually downgrades the instance to the core tier.** Once the URL no longer matches its activation, periodic revalidation fails and the instance continues to run on its cached license. When the cached license expires, the downgrade takes effect without warning. If usage is above core tier limits at that point, the instance becomes [locked down](#locked-down-instances) and the API returns errors until the license is resolved. Your data is unaffected, and re-applying the license key restores normal operation immediately. +**Changing `PUBLIC_URL` without re-applying your license eventually downgrades the instance to the core tier.** Once the URL no longer matches its activation, periodic revalidation fails with a `binding_mismatch` reason. The instance keeps running on its current license, and admins see a warning in **Settings > License** with the date it lapses. If you don't re-apply the license by then, the instance moves to the core tier. If usage is above core tier limits at that point, the instance becomes [locked down](#locked-down-instances) and the API returns errors until the license is resolved. Your data is unaffected, and re-applying the license key restores normal operation immediately. :: ### Replicas and Horizontal Scaling diff --git a/content/releases/2.changelog.md b/content/releases/2.changelog.md index 149f6df7..98210bdb 100644 --- a/content/releases/2.changelog.md +++ b/content/releases/2.changelog.md @@ -10,9 +10,22 @@ Each month, some of the Directus team talk through what’s new including core r [Watch The Changelog on Directus TV.](https://directus.com/tv/the-changelog) +## October 2026 + +- [Directus 12.5.0](https://github.com/directus/directus/releases/tag/v12.5.0) has several potential breaking changes: policy IP access and `IMPORT_IP_DENY_LIST` values are validated more strictly, 2FA enforcement follows the policies that apply to each login, `CACHE_SCHEMA_MAX_ITERATIONS` is removed and `CACHE_SCHEMA_SYNC_TIMEOUT` now defaults to `60000`, the `GET /license` response replaces `downgrade_reason` with `invalid_reason`, several license error classes are removed from `@directus/errors`, file `storage` values must match a configured location, and a number of system type definitions in `@directus/sdk` and `@directus/types` were corrected. Review the [full list](/releases/breaking-changes/version-12#version-1250) before upgrading. +- Added a **Preview Base URL** project setting. Select it as a variable in a collection's Preview URL so the [live preview](/guides/content/live-preview#set-a-preview-base-url) host can differ between environments. +- A failed license request now [keeps the current license in effect](/licensing/overview#online-mode) and retries on a schedule. Admins see a warning in **Settings > License** with the reason and the date the license lapses. +- Added header bar buttons to [activate or deactivate several Flows](/guides/flows/manage-flows#activate-or-deactivate-flows) at once. +- Editing the Public policy now shows a persistent warning, and saving permission changes on system collections asks you to confirm them first. +- You can now [link images](/guides/data-model/rich-text#linked-images) in the WYSIWYG editor. Pasted content with unsupported markup is [cleaned on paste](/guides/data-model/rich-text#pasting-content), with options to undo the paste or paste the raw HTML. +- Fixed GraphQL marking not-null fields with a default value as nullable, and requiring fields with falsy default values in create mutations. Generated GraphQL clients may show type changes. +- Fixed filtering and GraphQL typing of geometry subtype fields such as `geometry.Point`. +- Creating a collection with neither `schema` nor `meta` now returns a `400` validation error instead of a `403`. +- Updated several dependencies to address CVEs. + ## September 2026 -- [Directus 12.4.0](https://github.com/directus/directus/releases/tag/v12.4.0) has several potential breaking changes: inactive collections now reject API reads and writes with a `COLLECTION_INACTIVE` error, update and delete by query enforce read permissions when resolving the affected items, the map layout and interface require WebGL2, `@directus/themes` requires `@unhead/vue` 3, and a number of system type definitions in `@directus/types` and `@directus/sdk` were corrected. Review the [full list](/releases/breaking-changes/version-12#version-1240) before upgrading. +- [Directus 12.4.0](https://github.com/directus/directus/releases/tag/v12.4.0) has several potential breaking changes: inactive collections now reject API reads and writes with a `COLLECTION_INACTIVE` error, update and delete by query enforce read permissions when resolving the affected items, Flow folders are stored in `directus_folders` with a new `type` field, the map layout and interface require WebGL2, `@directus/themes` requires `@unhead/vue` 3, and a number of system type definitions in `@directus/types` and `@directus/sdk` were corrected. Review the [full list](/releases/breaking-changes/version-12#version-1240) before upgrading. - Moved Flows into their own module in the module bar. The new [Manage Flows](/guides/flows/manage-flows) page adds folders, search and filtering, duplication, and import and export of Flows. - A manual Flow can now [hide its own button](/guides/flows/triggers#manual), so it runs only from a **Button Links** interface configured to trigger it. - The **Send Email** operation now accepts a [**From Name**](/guides/flows/operations#send-email), shown to recipients as the sender name. diff --git a/content/releases/3.breaking-changes/3.version-12.md b/content/releases/3.breaking-changes/3.version-12.md index b5fe8ccc..fb6b8b0a 100644 --- a/content/releases/3.breaking-changes/3.version-12.md +++ b/content/releases/3.breaking-changes/3.version-12.md @@ -4,11 +4,84 @@ title: Version 12 description: Breaking changes may require action on your part before upgrading. --- +## Version 12.5.0 + +### Stricter IP access validation + +Policy **IP Access** (`ip_access`) values and the [`IMPORT_IP_DENY_LIST`](/configuration/security-limits) environment variable are now validated more strictly. Each entry must be one of the following: + +- An IPv4 or IPv6 address, such as `192.168.1.10`. +- A range of two full addresses in the same family, such as `10.0.0.1-10.0.0.50`, with no spaces around the `-`. +- A CIDR block with a numeric prefix, such as `10.0.0.0/24`. + +Subnets with a malformed prefix, such as `10.0.0.0/ 24` or `10.0.0.0/+24`, were previously accepted and are now rejected. Saving a policy with an invalid entry fails with a validation error. + +A policy that already stores an invalid entry is not migrated. Every request from a user with that policy fails with a `500` error, including logins. Before upgrading, review the `ip_access` column of `directus_policies` and correct any invalid entries. If an admin's own policy is affected, correct the value directly in the database or from another admin account that does not have that policy. + +If any `IMPORT_IP_DENY_LIST` entry is invalid, Directus logs a warning and denies every outbound request that checks the list. This includes file imports, the **Request URL** flow operation, and requests made by sandboxed extensions. + +### Two-factor enforcement follows effective policies + +When a user without 2FA logs in, Directus decides whether to require 2FA setup from the policies that apply to that request. Previously it only checked policies attached directly to the user's primary role, and ignored IP access. This changes who is asked to set up 2FA: + +- A **Require 2FA** policy attached to a parent role, or assigned directly to the user, now requires 2FA. +- A **Require 2FA** policy with an IP allowlist only requires 2FA when the user logs in from an allowed IP address. + +This mainly affects SSO logins and API clients that read the `enforce_tfa` claim in the access token. The Data Studio already used effective policies. See [Two-Factor Authentication](/guides/auth/2fa). + +### `CACHE_SCHEMA_MAX_ITERATIONS` removed + +`CACHE_SCHEMA_MAX_ITERATIONS` has been removed. [`CACHE_SCHEMA_SYNC_TIMEOUT`](/configuration/cache) now limits the whole schema build, both for the process building the schema and for processes waiting on it. Its default has increased from `10000` to `60000` milliseconds. + +When a schema build exceeds the timeout, requests will fail and Directus logs a warning. If you set a lower value, make sure it is long enough for your schema to finish building. + +### License endpoint returns `invalid_reason` + +The `GET /license` response no longer includes `downgrade_reason`. Read `invalid_reason` instead. It reports why a license was downgraded, and also why a license that is still in effect could not be renewed. + +`invalid_reason` previously reported only `expired`, `canceled`, or `suspended`. It can now also be `verification`, `invalid_key`, `activation_limit`, `binding_mismatch`, or `unavailable`. A new `token_expires_at` field gives the Unix timestamp when the current license lapses. See [Why a License Could Not Be Renewed](/licensing/overview#why-a-license-could-not-be-renewed). + +### License error classes removed from `@directus/errors` + +`@directus/errors` no longer exports `LicenseManagedByEnvError`, `LicenseOfflineUnsupportedError`, `LicenseResolveIncompleteError`, `LicenseServiceUnavailableError`, or `LicenseImmutableError`. The `LICENSE_MANAGED_BY_ENV`, `LICENSE_OFFLINE_UNSUPPORTED`, `LICENSE_RESOLVE_INCOMPLETE`, and `LICENSE_SERVICE_UNAVAILABLE` error codes are removed too. The API never returned these codes, so only extensions that import them are affected. + +License request failures now return one of the following: + +- `LICENSE_INVALID` (`400`), with a `failure` extension of `expired`, `canceled`, `suspended`, `invalid_key`, `activation_limit`, or `binding_mismatch`. +- `INVALID_PAYLOAD`, `FORBIDDEN`, `REQUESTS_EXCEEDED`, or `SERVICE_UNAVAILABLE`. + +`BINDING_MISMATCH` and activation limit failures previously returned `FORBIDDEN`, and now return `LICENSE_INVALID`. + +### File `storage` must be a configured location + +Creating, uploading, or updating a file with a `storage` value that is not listed in [`STORAGE_LOCATIONS`](/configuration/files#storage-locations) now fails with an `INVALID_PAYLOAD` error. The match is exact and case-sensitive. Scripts or migrations that write file records pointing at an unconfigured location must use a configured one. + +Resumable (TUS) uploads that replace an existing file only work for files stored in the first location in `STORAGE_LOCATIONS`. + +### System type definitions corrected + +Several type definitions in `@directus/sdk` and `@directus/types` were out of date with the fields they describe. TypeScript projects may fail to compile after upgrading. + +In `@directus/sdk`: + +- `DirectusNotification.id` is now typed as `number`. +- `DirectusField.schema` is now nullable, and its `schema`, `comment`, and `foreign_key_schema` properties are now optional. +- `FieldMetaConditionOptionType` has been removed. Condition `options` is now `Record`. +- `FieldMetaConditionType.hidden`, `readonly`, `required`, and `options` are now optional, and `rule` is typed as a filter. +- All `ExtensionSchema` properties are now optional, and `DirectusExtension.schema` can also be an `ExtensionSchemaEntry`. + +In `@directus/types`: + +- `Notification.id` is now typed as `number`. +- `Notification.status` and `Notification.timestamp` are now nullable. + +The SDK's `readActivity`, `readNotification`, `readRevision`, `readPermission`, `updatePermission`, `deletePermission`, and `readItemPermissions` functions now throw before sending a request when the key or collection is `null` or `undefined`. Previously they sent a request to a path such as `/permissions/null`. + ## Version 12.4.0 ::callout{icon="i-lucide-triangle-alert" color="warning"} **Upgrade to 12.4.1 instead of 12.4.0** -In 12.4.0, reading `directus_folders` as a non-admin user fails with a `500` error, which breaks the File Library for non-admin users and any `GET /folders` request made with a non-admin token. [12.4.1](https://github.com/directus/directus/releases/tag/v12.4.1) fixes this. The breaking changes below apply to both versions. +In 12.4.0, reading `directus_folders` as a non-admin user fails with a `500` error, which breaks the File Library for non-admin users and any `GET /folders` request made with a non-admin token. [12.4.1](https://github.com/directus/directus/releases/tag/v12.4.1) fixes this and has no breaking changes of its own. The breaking changes below apply to both versions. :: ### Inactive collections reject reads and writes @@ -32,6 +105,16 @@ Collections are deactivated through the licensing [resolution flow](/licensing/o This applies to `PATCH` and `DELETE` requests that pass a `query`, to the equivalent GraphQL mutations, and to the **Update Items** and **Delete Items** flow operations when they run with a non-admin accountability. Review policies that grant update or delete access without a matching read permission. +### Flow folders are stored in `directus_folders` + +Flows can now be organized into folders, which are stored in `directus_folders` alongside File Library folders. A new `type` field tells them apart. It is `files` for File Library folders and `flows` for Flow folders. Existing folders are set to `files`. + +Flow folders are admin-only. Non-admin requests to `GET /folders` never return them. Admin requests, including those made with an admin's static token, return both kinds. If you build a folder tree from `/folders` with an admin token, filter on the folder type: + +```http +GET /folders?filter[type][_eq]=files +``` + ### Map layout and map interface require WebGL2 The Data Studio's map layout and map interface now run on [MapLibre GL JS](https://maplibre.org) 6, which requires WebGL2. Browsers that only support WebGL1, chiefly Safari 14 and earlier and older Android devices, no longer render maps. All other Studio functionality is unaffected in those browsers. @@ -106,6 +189,7 @@ The editor supports the following HTML: - Media and tables: ``, `