@directus/api 36.0.0-rc.0 → 36.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ import "../../providers/index.js";
6
6
  import { getAITelemetryConfig } from "../../telemetry/index.js";
7
7
  import { SYSTEM_PROMPT } from "../constants/system-prompt.js";
8
8
  import { formatContextForSystemPrompt } from "../utils/format-context.js";
9
+ import { applyAnthropicConversationCaching, buildCacheAwareSystemPrompt, formatUsageWithCacheTokens, sortToolsByName } from "../utils/prompt-caching.js";
9
10
  import { transformFilePartsForProvider } from "./transform-file-parts.js";
10
11
  import { ServiceUnavailableError } from "@directus/errors";
11
12
  import { convertToModelMessages, stepCountIs, streamText, wrapLanguageModel } from "ai";
@@ -27,35 +28,25 @@ const createUiStream = async (messages, { provider, model, tools, aiSettings, sy
27
28
  model: languageModel,
28
29
  middleware: devToolsMiddleware
29
30
  });
30
- const fullSystemPrompt = contextBlock ? baseSystemPrompt + contextBlock : baseSystemPrompt;
31
- const finalTools = applyAnthropicToolSearch(provider, model, tools);
31
+ const streamSystemPrompt = buildCacheAwareSystemPrompt(provider, provider === "anthropic" || !contextBlock ? baseSystemPrompt : baseSystemPrompt + contextBlock);
32
+ const finalTools = sortToolsByName(applyAnthropicToolSearch(provider, model, tools));
32
33
  const telemetryConfig = getAITelemetryConfig({
33
34
  provider,
34
35
  model,
35
36
  userId,
36
37
  role
37
38
  });
39
+ const streamMessages = applyAnthropicConversationCaching(provider, await convertToModelMessages(transformFilePartsForProvider(messages)), contextBlock);
38
40
  return streamText({
39
- system: baseSystemPrompt,
41
+ system: streamSystemPrompt,
40
42
  model: languageModel,
41
- messages: await convertToModelMessages(transformFilePartsForProvider(messages)),
43
+ messages: streamMessages,
42
44
  stopWhen: [stepCountIs(10)],
43
45
  providerOptions,
44
46
  tools: finalTools,
45
47
  ...telemetryConfig ? { experimental_telemetry: telemetryConfig } : {},
46
- prepareStep: () => {
47
- if (contextBlock) return { system: fullSystemPrompt };
48
- return {};
49
- },
50
- onFinish({ usage }) {
51
- if (onUsage) {
52
- const { inputTokens, outputTokens, totalTokens } = usage;
53
- onUsage({
54
- inputTokens,
55
- outputTokens,
56
- totalTokens
57
- });
58
- }
48
+ onFinish(result) {
49
+ if (onUsage) onUsage(formatUsageWithCacheTokens(result));
59
50
  }
60
51
  });
61
52
  };
@@ -70,8 +70,6 @@ ${promptBlocks}
70
70
  </custom_instructions>`);
71
71
  }
72
72
  const sections = [];
73
- const now = /* @__PURE__ */ new Date();
74
- sections.push(`## Current Date\n${now.toISOString().split("T")[0]}`);
75
73
  if (context.page) {
76
74
  const page = context.page;
77
75
  const pageLines = [`Path: ${escapeAngleBrackets(String(page.path))}`];
@@ -89,6 +87,8 @@ Use the items tool to fetch additional fields or update items when asked.
89
87
 
90
88
  ${itemLines}`);
91
89
  }
90
+ const now = /* @__PURE__ */ new Date();
91
+ sections.push(`## Current Date\n${now.toISOString().split("T")[0]}`);
92
92
  if (sections.length > 0) parts.push(`<user_context>\n${sections.join("\n\n")}\n</user_context>`);
93
93
  if (groups.visualElements.length > 0) {
94
94
  const elementLines = groups.visualElements.map(formatVisualElement).join("\n\n");
@@ -0,0 +1,85 @@
1
+ //#region src/ai/chat/utils/prompt-caching.ts
2
+ function buildCacheAwareSystemPrompt(provider, content) {
3
+ if (provider !== "anthropic") return content;
4
+ return {
5
+ role: "system",
6
+ content,
7
+ providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } }
8
+ };
9
+ }
10
+ /**
11
+ * For Anthropic, place a cache breakpoint on the last existing message so the conversation
12
+ * prefix (tools + system + history) caches as it grows, and append the per-request page
13
+ * context as a new user message after the breakpoint so it stays out of the cached prefix.
14
+ *
15
+ * Context is only appended after a real user turn. On multi-step continuations the last
16
+ * message is an assistant or tool result; appending a user message there would make the
17
+ * model respond to the context instead of synthesizing the tool output. The model still
18
+ * sees context from the originating user turn earlier in the conversation.
19
+ *
20
+ * Other providers either auto-cache (Google/OpenAI) or don't support this, so we keep the
21
+ * context inside the system prompt for them (handled upstream).
22
+ */
23
+ function applyAnthropicConversationCaching(provider, messages, contextBlock) {
24
+ if (provider !== "anthropic" || messages.length === 0) return messages;
25
+ const lastIndex = messages.length - 1;
26
+ const messagesWithCache = messages.map((message, index) => index === lastIndex ? {
27
+ ...message,
28
+ providerOptions: {
29
+ ...message.providerOptions,
30
+ anthropic: {
31
+ ...message.providerOptions?.["anthropic"],
32
+ cacheControl: { type: "ephemeral" }
33
+ }
34
+ }
35
+ } : message);
36
+ if (!contextBlock || messages[lastIndex]?.role !== "user") return messagesWithCache;
37
+ return [...messagesWithCache, {
38
+ role: "user",
39
+ content: contextBlock
40
+ }];
41
+ }
42
+ function sortToolsByName(tools) {
43
+ return Object.fromEntries(Object.entries(tools).sort(([a], [b]) => {
44
+ if (a < b) return -1;
45
+ if (a > b) return 1;
46
+ return 0;
47
+ }));
48
+ }
49
+ function formatUsageWithCacheTokens(result) {
50
+ const usage = result.totalUsage ?? result.usage;
51
+ const providerMetadata = result.steps?.map((step) => step.providerMetadata) ?? [result.providerMetadata];
52
+ const { inputTokens, outputTokens, totalTokens } = usage;
53
+ return {
54
+ inputTokens,
55
+ outputTokens,
56
+ totalTokens,
57
+ ...getCacheTokenUsage(usage, providerMetadata)
58
+ };
59
+ }
60
+ function getCacheTokenUsage(usage, providerMetadata) {
61
+ const cacheReadTokens = usage.inputTokenDetails?.cacheReadTokens ?? usage.cachedInputTokens ?? sumNumbers([...providerMetadata.map((metadata) => getProviderMetadataNumber(metadata, "anthropic", "cacheReadInputTokens")), ...providerMetadata.map(getGoogleCachedContentTokenCount)]);
62
+ const cacheCreationTokens = usage.inputTokenDetails?.cacheWriteTokens ?? sumNumbers(providerMetadata.map((metadata) => getProviderMetadataNumber(metadata, "anthropic", "cacheCreationInputTokens")));
63
+ return {
64
+ ...cacheReadTokens !== void 0 ? { cacheReadTokens } : {},
65
+ ...cacheCreationTokens !== void 0 ? { cacheCreationTokens } : {}
66
+ };
67
+ }
68
+ function getProviderMetadataNumber(providerMetadata, providerName, fieldName) {
69
+ const value = providerMetadata?.[providerName]?.[fieldName];
70
+ return typeof value === "number" ? value : void 0;
71
+ }
72
+ function getGoogleCachedContentTokenCount(providerMetadata) {
73
+ const usageMetadata = providerMetadata?.["google"]?.["usageMetadata"];
74
+ if (!usageMetadata || typeof usageMetadata !== "object" || Array.isArray(usageMetadata)) return;
75
+ const cachedContentTokenCount = usageMetadata["cachedContentTokenCount"];
76
+ return typeof cachedContentTokenCount === "number" ? cachedContentTokenCount : void 0;
77
+ }
78
+ function sumNumbers(values) {
79
+ const definedValues = values.filter((value) => typeof value === "number");
80
+ if (definedValues.length === 0) return;
81
+ return definedValues.reduce((sum, value) => sum + value, 0);
82
+ }
83
+
84
+ //#endregion
85
+ export { applyAnthropicConversationCaching, buildCacheAwareSystemPrompt, formatUsageWithCacheTokens, sortToolsByName };
package/dist/app.js CHANGED
@@ -11,8 +11,6 @@ import { getFlowManager } from "./flows.js";
11
11
  import { getExtensionManager } from "./extensions/index.js";
12
12
  import { registerAuthProviders } from "./auth.js";
13
13
  import { ensureDeploymentWebhooks, registerDeploymentDrivers } from "./deployment.js";
14
- import rate_limiter_global_default from "./middleware/rate-limiter-global.js";
15
- import rate_limiter_ip_default from "./middleware/rate-limiter-ip.js";
16
14
  import { getLicenseManager } from "./license/manager.js";
17
15
  import "./license/index.js";
18
16
  import { aiRouter } from "./ai/chat/router.js";
@@ -63,6 +61,8 @@ import cors_default from "./middleware/cors.js";
63
61
  import { errorHandler } from "./middleware/error-handler.js";
64
62
  import extract_token_default from "./middleware/extract-token.js";
65
63
  import mcp_oauth_guard_default from "./middleware/mcp-oauth-guard.js";
64
+ import rate_limiter_global_default from "./middleware/rate-limiter-global.js";
65
+ import rate_limiter_ip_default from "./middleware/rate-limiter-ip.js";
66
66
  import request_counter_default from "./middleware/request-counter.js";
67
67
  import sanitize_query_default from "./middleware/sanitize-query.js";
68
68
  import schema_default$1 from "./middleware/schema.js";
@@ -8,6 +8,7 @@ import { getLicenseManager } from "../license/manager.js";
8
8
  import { createAdmin } from "../utils/create-admin.js";
9
9
  import { useEnv } from "@directus/env";
10
10
  import { ErrorCode, ForbiddenError, InvalidPayloadError, RouteNotFoundError, isDirectusError } from "@directus/errors";
11
+ import { toBoolean } from "@directus/utils";
11
12
  import { Router } from "express";
12
13
  import { fromZodError } from "zod-validation-error";
13
14
  import z from "zod";
@@ -49,7 +50,7 @@ router.get("/info", async_handler_default(async (req, res, next) => {
49
50
  res.locals["payload"] = { data };
50
51
  return next();
51
52
  }), respond);
52
- router.get("/health", async_handler_default(async (req, res, next) => {
53
+ if (toBoolean(env["HEALTHCHECK_ENABLED"]) !== false) router.get("/health", async_handler_default(async (req, res, next) => {
53
54
  const data = await new ServerService({
54
55
  accountability: req.accountability,
55
56
  schema: req.schema
@@ -85,6 +86,7 @@ router.post("/setup", async_handler_default(async (req, _res, next) => {
85
86
  if (error) throw new InvalidPayloadError({ reason: fromZodError(error).message });
86
87
  const licenseManager = getLicenseManager();
87
88
  try {
89
+ if (data.license_key) await licenseManager.activate(data.license_key);
88
90
  await createAdmin(req.schema, {
89
91
  email: data.admin.email,
90
92
  password: data.admin.password,
@@ -92,9 +94,6 @@ router.post("/setup", async_handler_default(async (req, _res, next) => {
92
94
  last_name: data.admin.last_name
93
95
  });
94
96
  const settingsService = new SettingsService({ schema: req.schema });
95
- if (data.license_key) try {
96
- await licenseManager.activate(data.license_key);
97
- } catch {}
98
97
  if (data.owner) settingsService.setOwner(data.owner);
99
98
  } catch (error$1) {
100
99
  if (isDirectusError(error$1, ErrorCode.Forbidden)) return next();
@@ -34,7 +34,8 @@ function extractError(error, data) {
34
34
  const matches = error.message.match(/"(.*?)"/g);
35
35
  if (!matches) return error;
36
36
  const collection = matches[0].slice(1, -1);
37
- const field = matches[1]?.slice(1, -1) ?? null;
37
+ const fields = Object.keys(data);
38
+ const field = fields.length === 1 ? fields[0] : null;
38
39
  return new ValueOutOfRangeError({
39
40
  collection,
40
41
  field,
@@ -9,13 +9,13 @@ function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode, fieldA
9
9
  const parentItems = clone(toArray(parentItem));
10
10
  if (nestedNode.type === "m2o") {
11
11
  const parentsByForeignKey = /* @__PURE__ */ new Map();
12
+ const nestedPrimaryKeyField = schema.collections[nestedNode.relation.related_collection].primary;
12
13
  for (const parentItem$1 of parentItems) {
13
- const relationKey = parentItem$1[nestedNode.relation.field];
14
+ const relationKey = parentItem$1[nestedNode.relation.field]?.[nestedPrimaryKeyField] ?? parentItem$1[nestedNode.relation.field];
14
15
  if (!parentsByForeignKey.has(relationKey)) parentsByForeignKey.set(relationKey, []);
15
16
  parentItem$1[nestedNode.fieldKey] = null;
16
17
  parentsByForeignKey.get(relationKey).push(parentItem$1);
17
18
  }
18
- const nestedPrimaryKeyField = schema.collections[nestedNode.relation.related_collection].primary;
19
19
  for (const nestedItem$1 of nestedItems) {
20
20
  const nestedPK = nestedItem$1[nestedPrimaryKeyField];
21
21
  if (nestedPK === null) continue;
@@ -84,8 +84,10 @@ function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode, fieldA
84
84
  parentItem$1[nestedNode.fieldKey] = null;
85
85
  continue;
86
86
  }
87
+ const relatedPrimaryKeyField = schema.collections[relatedCollection].primary;
88
+ const foreignKey = parentItem$1[nestedNode.relation.field]?.[relatedPrimaryKeyField] ?? parentItem$1[nestedNode.relation.field];
87
89
  const itemChild = nestedItem[relatedCollection].find((nestedItem$1) => {
88
- return nestedItem$1[nestedNode.relatedKey[relatedCollection]] == parentItem$1[nestedNode.fieldKey];
90
+ return nestedItem$1[nestedNode.relatedKey[relatedCollection]] == foreignKey;
89
91
  });
90
92
  parentItem$1[nestedNode.fieldKey] = itemChild || null;
91
93
  }
@@ -3,7 +3,7 @@ import { ItemsService } from "../../../services/items.js";
3
3
  import { getSchema } from "../../../utils/get-schema.js";
4
4
  import "../../../services/index.js";
5
5
  import { isEqual } from "lodash-es";
6
- import { appAccessMinimalPermissions, appRecommendedPermissions } from "@directus/system-data";
6
+ import { appRecommendedPermissions } from "@directus/system-data";
7
7
 
8
8
  //#region src/license/entitlements/lib/custom-permission-rules-enabled.ts
9
9
  function hasCustomRule(permission) {
@@ -16,11 +16,6 @@ function isRecommendedAppPermission(permission) {
16
16
  if (!foundPermission) return false;
17
17
  return isEqual(foundPermission.fields ?? null, permission.fields ?? null) && isEqual(foundPermission.permissions ?? null, permission.permissions ?? null);
18
18
  }
19
- function isMinimumAppPermission(permission) {
20
- const foundPermission = appAccessMinimalPermissions.find((p) => p.action === permission.action && p.collection === permission.collection);
21
- if (!foundPermission) return false;
22
- return isEqual(foundPermission.fields ?? null, permission.fields ?? null) && isEqual(foundPermission.permissions ?? null, permission.permissions ?? null) && isEqual(foundPermission.validation ?? null, permission.validation ?? null) && isEqual(foundPermission.presets ?? null, permission.presets ?? null);
23
- }
24
19
  async function checkCustomPermissionRules(opts) {
25
20
  const knex = opts?.knex ?? database_default();
26
21
  return (await new ItemsService("directus_permissions", {
@@ -38,4 +33,4 @@ async function checkCustomPermissionRules(opts) {
38
33
  }
39
34
 
40
35
  //#endregion
41
- export { checkCustomPermissionRules, hasCustomRule, isMinimumAppPermission, isRecommendedAppPermission };
36
+ export { checkCustomPermissionRules, hasCustomRule, isRecommendedAppPermission };
@@ -8,10 +8,49 @@ import { toBoolean } from "@directus/utils";
8
8
  import { USER_INACTIVE_LICENSE_STATUS } from "@directus/constants";
9
9
 
10
10
  //#region src/license/entitlements/lib/seats.ts
11
+ /**
12
+ * Group access rows to the admin/app users and roles that occupy a seat.
13
+ */
14
+ function getSeatUsersAndRoles(accessRows) {
15
+ const adminRoles = /* @__PURE__ */ new Set();
16
+ const appRoles = /* @__PURE__ */ new Set();
17
+ const adminUsers = /* @__PURE__ */ new Set();
18
+ const appUsers = /* @__PURE__ */ new Set();
19
+ const appUsersByRole = /* @__PURE__ */ new Map();
20
+ for (const accessRow of accessRows) {
21
+ const { admin_access, app_access } = accessRow["policy"] || {};
22
+ const isAdmin = toBoolean(admin_access);
23
+ const isApp = !isAdmin && toBoolean(app_access);
24
+ if (!isAdmin && !isApp) continue;
25
+ if (accessRow["user"] && accessRow["user"].status === "active") {
26
+ const { id, role } = accessRow["user"];
27
+ if (isAdmin) {
28
+ adminUsers.add(id);
29
+ appUsers.delete(id);
30
+ } else if (adminUsers.has(id) === false && (!role || adminRoles.has(role) === false)) {
31
+ appUsers.add(id);
32
+ if (role) {
33
+ const roleUsers = appUsersByRole.get(role) ?? /* @__PURE__ */ new Set();
34
+ appUsersByRole.set(role, roleUsers.add(id));
35
+ }
36
+ }
37
+ }
38
+ if (accessRow["role"]) if (isAdmin) {
39
+ adminRoles.add(accessRow["role"]);
40
+ for (const id of appUsersByRole.get(accessRow["role"]) ?? []) appUsers.delete(id);
41
+ } else appRoles.add(accessRow["role"]);
42
+ }
43
+ return {
44
+ adminUsers,
45
+ appUsers,
46
+ adminRoles,
47
+ appRoles
48
+ };
49
+ }
11
50
  async function getActiveSeats(opts) {
12
51
  const knex = opts?.knex ?? database_default();
13
52
  const schema = await getSchema({ database: knex });
14
- const accessRows = await new AccessService({
53
+ const { adminUsers, appUsers, adminRoles, appRoles } = getSeatUsersAndRoles(await new AccessService({
15
54
  schema,
16
55
  knex
17
56
  }).readByQuery({
@@ -24,22 +63,7 @@ async function getActiveSeats(opts) {
24
63
  "policy.admin_access"
25
64
  ],
26
65
  limit: -1
27
- });
28
- const adminRoles = /* @__PURE__ */ new Set();
29
- const appRoles = /* @__PURE__ */ new Set();
30
- const adminUsers = /* @__PURE__ */ new Set();
31
- const appUsers = /* @__PURE__ */ new Set();
32
- for (const accessRow of accessRows) {
33
- const isAdmin = toBoolean(accessRow["policy"]?.["admin_access"]);
34
- const isApp = !isAdmin && toBoolean(accessRow["policy"]?.["app_access"]);
35
- if (!isAdmin && !isApp) continue;
36
- if (accessRow["user"] && accessRow["user"].status === "active") {
37
- if (isAdmin) adminUsers.add(accessRow["user"].id);
38
- else if (adminUsers.has(accessRow["user"].id) === false && adminRoles.has(accessRow["user"]?.role) === false) appUsers.add(accessRow["user"].id);
39
- }
40
- if (accessRow["role"]) if (isAdmin) adminRoles.add(accessRow["role"]);
41
- else appRoles.add(accessRow["role"]);
42
- }
66
+ }));
43
67
  const { adminRoles: allAdminRoles, appRoles: allAppRoles } = await fetchAccessRoles({
44
68
  adminRoles,
45
69
  appRoles
@@ -100,4 +124,4 @@ async function resolveSeats(seats, ctx) {
100
124
  }
101
125
 
102
126
  //#endregion
103
- export { countActiveSeats, getActiveSeats, resolveSeats };
127
+ export { countActiveSeats, getActiveSeats, getSeatUsersAndRoles, resolveSeats };
@@ -3,6 +3,7 @@ import database_default from "../../../database/index.js";
3
3
  import { getSchema } from "../../../utils/get-schema.js";
4
4
  import { UsersService } from "../../../services/users.js";
5
5
  import "../../../services/index.js";
6
+ import { isObject } from "@directus/utils";
6
7
  import { USER_INACTIVE_LICENSE_STATUS } from "@directus/constants";
7
8
 
8
9
  //#region src/license/entitlements/lib/sso-enabled.ts
@@ -33,7 +34,7 @@ async function resolveSSOUsers(resolution, ctx) {
33
34
  _neq: DEFAULT_AUTH_PROVIDER,
34
35
  _nnull: true
35
36
  } }, { id: { _neq: adminId } }] } }, { status: USER_INACTIVE_LICENSE_STATUS });
36
- if (typeof resolution === "object" && Object.keys(resolution.admin).length) {
37
+ if (isObject(resolution) && Object.keys(resolution.admin ?? {}).length) {
37
38
  const payload = { provider: DEFAULT_AUTH_PROVIDER };
38
39
  if (resolution.admin.email?.length) payload["email"] = resolution.admin.email;
39
40
  if (resolution.admin.password?.length) payload["password"] = resolution.admin.password;
@@ -160,17 +160,17 @@ function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTypes, Rea
160
160
  schema: gql.schema
161
161
  }).serverInfo();
162
162
  }, "server_info")
163
- },
164
- server_health: {
165
- type: GraphQLJSON,
166
- resolve: dedupeResolver(async () => {
167
- return await new ServerService({
168
- accountability: gql.accountability,
169
- schema: gql.schema
170
- }).health();
171
- }, "server_health")
172
163
  }
173
164
  });
165
+ if (toBoolean(env["HEALTHCHECK_ENABLED"]) !== false) schemaComposer.Query.addFields({ server_health: {
166
+ type: GraphQLJSON,
167
+ resolve: dedupeResolver(async () => {
168
+ return await new ServerService({
169
+ accountability: gql.accountability,
170
+ schema: gql.schema
171
+ }).health();
172
+ }, "server_health")
173
+ } });
174
174
  if ("directus_collections" in schema.read.collections) {
175
175
  const Collection = getCollectionType(schemaComposer, schema, "read");
176
176
  schemaComposer.Query.addFields({
@@ -119,26 +119,26 @@ function getTypes(schemaComposer, scope, schema, inconsistentFields, action) {
119
119
  CollectionTypes[relation.collection]?.addFields({ [relation.field]: {
120
120
  type: CollectionTypes[relation.related_collection],
121
121
  resolve: (obj, _, __, info) => {
122
- return obj[info?.path?.key ?? relation.field];
122
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
123
123
  }
124
124
  } });
125
125
  VersionTypes[relation.collection]?.addFields({ [relation.field]: {
126
126
  type: GraphQLJSON,
127
127
  resolve: (obj, _, __, info) => {
128
- return obj[info?.path?.key ?? relation.field];
128
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
129
129
  }
130
130
  } });
131
131
  if (relation.meta?.one_field) {
132
132
  CollectionTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: {
133
133
  type: [CollectionTypes[relation.collection]],
134
134
  resolve: (obj, _, __, info) => {
135
- return obj[info?.path?.key ?? relation.meta.one_field];
135
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.meta.one_field];
136
136
  }
137
137
  } });
138
138
  if (scope === "items") VersionTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: {
139
139
  type: GraphQLJSON,
140
140
  resolve: (obj, _, __, info) => {
141
- return obj[info?.path?.key ?? relation.meta.one_field];
141
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.meta.one_field];
142
142
  }
143
143
  } });
144
144
  }
@@ -160,7 +160,7 @@ function getTypes(schemaComposer, scope, schema, inconsistentFields, action) {
160
160
  }
161
161
  }),
162
162
  resolve: (obj, _, __, info) => {
163
- return obj[info?.path?.key ?? relation.field];
163
+ return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
164
164
  }
165
165
  } });
166
166
  return {
@@ -41,13 +41,13 @@ var PermissionsService = class extends ItemsService {
41
41
  return withAppMinimalPermissions(this.accountability, mappedPermissions, query.filter);
42
42
  }
43
43
  async createOne(data, opts) {
44
- if (hasCustomRule(data) && !isRecommendedAppPermission(data)) await getEntitlementManager().assert("custom_permission_rules_enabled", { knex: this.knex });
44
+ if (!getEntitlementManager().isEntitled("custom_permission_rules_enabled") && hasCustomRule(data) && !isRecommendedAppPermission(data)) throw new ResourceRestrictedError({ category: "custom_permission_rules_enabled" });
45
45
  const res = await super.createOne(data, opts);
46
46
  await this.clearCaches(opts);
47
47
  return res;
48
48
  }
49
49
  async updateMany(keys, data, opts) {
50
- if (hasCustomRule(data) && !isRecommendedAppPermission(data)) await getEntitlementManager().assert("custom_permission_rules_enabled", { knex: this.knex });
50
+ if (!getEntitlementManager().isEntitled("custom_permission_rules_enabled") && hasCustomRule(data) && !isRecommendedAppPermission(data)) throw new ResourceRestrictedError({ category: "custom_permission_rules_enabled" });
51
51
  const res = await super.updateMany(keys, data, opts);
52
52
  await this.clearCaches(opts);
53
53
  return res;
@@ -1,20 +1,25 @@
1
+ import { useRedis } from "../redis/lib/use-redis.js";
2
+ import { redisConfigAvailable } from "../redis/utils/redis-config-available.js";
3
+ import "../redis/index.js";
1
4
  import { useLogger } from "../logger/index.js";
5
+ import { getMilliseconds } from "../utils/get-milliseconds.js";
2
6
  import { FILE_UPLOADS, RESUMABLE_UPLOADS } from "../constants.js";
3
- import { getCache } from "../cache.js";
4
7
  import { getStorage } from "../storage/index.js";
5
8
  import database_default, { hasDatabaseConnection } from "../database/index.js";
9
+ import { useStore } from "../utils/store.js";
6
10
  import getMailer from "../mailer.js";
7
11
  import { getEntitlementManager } from "../license/entitlements/manager.js";
8
12
  import { SettingsService } from "./settings.js";
9
- import { rateLimiterGlobal } from "../middleware/rate-limiter-global.js";
10
- import { rateLimiter } from "../middleware/rate-limiter-ip.js";
11
13
  import { getAllowedLogLevels } from "../utils/get-allowed-log-levels.js";
12
14
  import { SERVER_ONLINE } from "../server.js";
15
+ import { isUnauthenticated } from "../utils/is-unauthenticated.js";
13
16
  import { getLicenseManager } from "../license/manager.js";
14
17
  import "../license/index.js";
15
18
  import { useEnv } from "@directus/env";
19
+ import { ForbiddenError } from "@directus/errors";
16
20
  import { toArray, toBoolean } from "@directus/utils";
17
21
  import { merge } from "lodash-es";
22
+ import { createKv } from "@directus/memory";
18
23
  import { performance } from "perf_hooks";
19
24
  import { Readable } from "node:stream";
20
25
  import { version } from "directus/version";
@@ -22,6 +27,8 @@ import { version } from "directus/version";
22
27
  //#region src/services/server.ts
23
28
  const env = useEnv();
24
29
  const logger = useLogger();
30
+ const HEALTHCHECK_CACHE_TTL = getMilliseconds(env["HEALTHCHECK_CACHE_TTL"], 3e5);
31
+ const store = useStore(env["HEALTHCHECK_NAMESPACE"] ?? "directus:healthcheck", { ttl: HEALTHCHECK_CACHE_TTL });
25
32
  var ServerService = class {
26
33
  knex;
27
34
  accountability;
@@ -121,17 +128,25 @@ var ServerService = class {
121
128
  return info;
122
129
  }
123
130
  async health() {
131
+ if (isUnauthenticated(this.accountability)) throw new ForbiddenError();
132
+ const healthResult = await store(async (store$1) => {
133
+ try {
134
+ return await store$1.get("health");
135
+ } catch (err) {
136
+ logger.warn(err, "Failed to read health check cache");
137
+ }
138
+ });
139
+ if (healthResult) return this.accountability?.admin === true ? healthResult : { status: healthResult["status"] };
124
140
  const { nanoid } = await import("nanoid");
125
141
  const checkID = nanoid(5);
142
+ const enabledServices = toArray(env["HEALTHCHECK_SERVICES"]);
126
143
  const data = {
127
144
  status: "ok",
128
145
  releaseId: version,
129
146
  serviceId: env["PUBLIC_URL"],
130
147
  checks: merge(...await Promise.all([
131
148
  testDatabase(),
132
- testCache(),
133
- testRateLimiter(),
134
- testRateLimiterGlobal(),
149
+ testRedis(),
135
150
  testStorage(),
136
151
  testEmail()
137
152
  ]))
@@ -152,9 +167,14 @@ var ServerService = class {
152
167
  }
153
168
  if (data.status === "error") break;
154
169
  }
155
- if (this.accountability?.admin !== true) return { status: data.status };
156
- else return data;
170
+ await store(async (store$1) => {
171
+ await store$1.set("health", data).catch((err) => {
172
+ logger.warn(err, "Failed to write health check cache");
173
+ });
174
+ });
175
+ return this.accountability?.admin === true ? data : { status: data.status };
157
176
  async function testDatabase() {
177
+ if (enabledServices.includes("database") === false) return {};
158
178
  const database = database_default();
159
179
  const client = env["DB_CLIENT"];
160
180
  const checks = {};
@@ -186,10 +206,15 @@ var ServerService = class {
186
206
  }];
187
207
  return checks;
188
208
  }
189
- async function testCache() {
190
- if (env["CACHE_ENABLED"] !== true) return {};
191
- const { cache } = getCache();
192
- const checks = { "cache:responseTime": [{
209
+ async function testRedis() {
210
+ if (enabledServices.includes("redis") === false || redisConfigAvailable() !== true) return {};
211
+ const redis = createKv({
212
+ type: "redis",
213
+ redis: useRedis(),
214
+ namespace: env["HEALTHCHECK_NAMESPACE"] ?? "directus:healthcheck",
215
+ ttl: HEALTHCHECK_CACHE_TTL
216
+ });
217
+ const checks = { "redis:responseTime": [{
193
218
  status: "ok",
194
219
  componentType: "cache",
195
220
  observedValue: 0,
@@ -198,65 +223,20 @@ var ServerService = class {
198
223
  }] };
199
224
  const startTime = performance.now();
200
225
  try {
201
- await cache.set(`directus-health-${checkID}`, true, 5);
202
- await cache.delete(`directus-health-${checkID}`);
203
- } catch (err) {
204
- checks["cache:responseTime"][0].status = "error";
205
- checks["cache:responseTime"][0].output = err;
206
- } finally {
207
- const endTime = performance.now();
208
- checks["cache:responseTime"][0].observedValue = +(endTime - startTime).toFixed(3);
209
- if (checks["cache:responseTime"][0].observedValue > checks["cache:responseTime"][0].threshold && checks["cache:responseTime"][0].status !== "error") checks["cache:responseTime"][0].status = "warn";
210
- }
211
- return checks;
212
- }
213
- async function testRateLimiter() {
214
- if (env["RATE_LIMITER_ENABLED"] !== true) return {};
215
- const checks = { "rateLimiter:responseTime": [{
216
- status: "ok",
217
- componentType: "ratelimiter",
218
- observedValue: 0,
219
- observedUnit: "ms",
220
- threshold: env["RATE_LIMITER_HEALTHCHECK_THRESHOLD"] ? +env["RATE_LIMITER_HEALTHCHECK_THRESHOLD"] : 150
221
- }] };
222
- const startTime = performance.now();
223
- try {
224
- await rateLimiter.consume(`directus-health-${checkID}`, 1);
225
- await rateLimiter.delete(`directus-health-${checkID}`);
226
- } catch (err) {
227
- checks["rateLimiter:responseTime"][0].status = "error";
228
- checks["rateLimiter:responseTime"][0].output = err;
229
- } finally {
230
- const endTime = performance.now();
231
- checks["rateLimiter:responseTime"][0].observedValue = +(endTime - startTime).toFixed(3);
232
- if (checks["rateLimiter:responseTime"][0].observedValue > checks["rateLimiter:responseTime"][0].threshold && checks["rateLimiter:responseTime"][0].status !== "error") checks["rateLimiter:responseTime"][0].status = "warn";
233
- }
234
- return checks;
235
- }
236
- async function testRateLimiterGlobal() {
237
- if (env["RATE_LIMITER_GLOBAL_ENABLED"] !== true) return {};
238
- const checks = { "rateLimiterGlobal:responseTime": [{
239
- status: "ok",
240
- componentType: "ratelimiter",
241
- observedValue: 0,
242
- observedUnit: "ms",
243
- threshold: env["RATE_LIMITER_GLOBAL_HEALTHCHECK_THRESHOLD"] ? +env["RATE_LIMITER_GLOBAL_HEALTHCHECK_THRESHOLD"] : 150
244
- }] };
245
- const startTime = performance.now();
246
- try {
247
- await rateLimiterGlobal.consume(`directus-health-${checkID}`, 1);
248
- await rateLimiterGlobal.delete(`directus-health-${checkID}`);
226
+ await redis.set(`directus-health-${checkID}`, 1);
227
+ await redis.delete(`directus-health-${checkID}`);
249
228
  } catch (err) {
250
- checks["rateLimiterGlobal:responseTime"][0].status = "error";
251
- checks["rateLimiterGlobal:responseTime"][0].output = err;
229
+ checks["redis:responseTime"][0].status = "error";
230
+ checks["redis:responseTime"][0].output = err;
252
231
  } finally {
253
232
  const endTime = performance.now();
254
- checks["rateLimiterGlobal:responseTime"][0].observedValue = +(endTime - startTime).toFixed(3);
255
- if (checks["rateLimiterGlobal:responseTime"][0].observedValue > checks["rateLimiterGlobal:responseTime"][0].threshold && checks["rateLimiterGlobal:responseTime"][0].status !== "error") checks["rateLimiterGlobal:responseTime"][0].status = "warn";
233
+ checks["redis:responseTime"][0].observedValue = +(endTime - startTime).toFixed(3);
234
+ if (checks["redis:responseTime"][0].observedValue > checks["redis:responseTime"][0].threshold && checks["redis:responseTime"][0].status !== "error") checks["redis:responseTime"][0].status = "warn";
256
235
  }
257
236
  return checks;
258
237
  }
259
238
  async function testStorage() {
239
+ if (enabledServices.includes("storage") === false) return {};
260
240
  const storage = await getStorage();
261
241
  const checks = {};
262
242
  for (const location of toArray(env["STORAGE_LOCATIONS"])) {
@@ -284,6 +264,7 @@ var ServerService = class {
284
264
  return checks;
285
265
  }
286
266
  async function testEmail() {
267
+ if (enabledServices.includes("email") === false || toBoolean(env["EMAIL_VERIFY_SETUP"]) === false) return {};
287
268
  const checks = { "email:connection": [{
288
269
  status: "ok",
289
270
  componentType: "email"
@@ -117,6 +117,12 @@ var UsersService = class UsersService extends ItemsService {
117
117
  }
118
118
  }
119
119
  /**
120
+ * Block setting a non-default auth provider when the current license isn't entitled to SSO
121
+ */
122
+ checkProviderEntitlement(input) {
123
+ if ((Array.isArray(input) ? input : [input]).some((provider) => provider && provider !== DEFAULT_AUTH_PROVIDER) && !getEntitlementManager().isEntitled("sso_enabled")) throw new InvalidPayloadError({ reason: `Setting a custom "provider" isn't included in the current license` });
124
+ }
125
+ /**
120
126
  * Create a new user
121
127
  */
122
128
  async createOne(data, opts = {}) {
@@ -126,6 +132,7 @@ var UsersService = class UsersService extends ItemsService {
126
132
  await this.checkUniqueEmails([data["email"]]);
127
133
  }
128
134
  if ("password" in data) await this.checkPasswordPolicy([data["password"]]);
135
+ if ("provider" in data) this.checkProviderEntitlement(data["provider"]);
129
136
  } catch (err) {
130
137
  opts.preMutationError = err;
131
138
  }
@@ -141,6 +148,7 @@ var UsersService = class UsersService extends ItemsService {
141
148
  async createMany(data, opts = {}) {
142
149
  const emails = data.map((payload) => payload["email"]).filter((email) => email);
143
150
  const passwords = data.map((payload) => payload["password"]).filter((password) => password);
151
+ const providers = data.map((payload) => payload["provider"]).filter((provider) => provider);
144
152
  const someActive = data.some((payload) => !("status" in payload) || payload["status"] === "active");
145
153
  try {
146
154
  if (emails.length) {
@@ -148,6 +156,7 @@ var UsersService = class UsersService extends ItemsService {
148
156
  await this.checkUniqueEmails(emails);
149
157
  }
150
158
  if (passwords.length) await this.checkPasswordPolicy(passwords);
159
+ if (providers.length) this.checkProviderEntitlement(providers);
151
160
  } catch (err) {
152
161
  opts.preMutationError = err;
153
162
  }
@@ -179,6 +188,7 @@ var UsersService = class UsersService extends ItemsService {
179
188
  if (data["tfa_secret"] !== void 0) throw new InvalidPayloadError({ reason: `You can't change the "tfa_secret" value manually` });
180
189
  if (data["provider"] !== void 0) {
181
190
  if (this.accountability && this.accountability.admin !== true) throw new InvalidPayloadError({ reason: `You can't change the "provider" value manually` });
191
+ this.checkProviderEntitlement(data["provider"]);
182
192
  data["auth_data"] = null;
183
193
  }
184
194
  if (data["external_identifier"] !== void 0) {
@@ -55,7 +55,9 @@ var VersionsService = class VersionsService extends ItemsService {
55
55
  knex: this.knex,
56
56
  schema: this.schema
57
57
  }).readOne(data["collection"])).meta?.versioning) throw new UnprocessableContentError({ reason: `Content Versioning is not enabled for collection "${data["collection"]}"` });
58
- if (itemLess) return;
58
+ const isSingleton = !!this.schema.collections[data["collection"]]?.singleton;
59
+ if (itemLess) if (isSingleton) await this.assertSingletonEmpty(data["collection"]);
60
+ else return;
59
61
  if ((await new VersionsService({
60
62
  knex: this.knex,
61
63
  schema: this.schema
@@ -64,9 +66,9 @@ var VersionsService = class VersionsService extends ItemsService {
64
66
  filter: {
65
67
  key: { _eq: data["key"] },
66
68
  collection: { _eq: data["collection"] },
67
- item: { _eq: data["item"] }
69
+ item: itemLess ? { _null: true } : { _eq: data["item"] }
68
70
  }
69
- }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Version "${data["key"]}" already exists for item "${data["item"]}" in collection "${data["collection"]}"` });
71
+ }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: itemLess ? `Singleton collection "${data["collection"]}" already has an item-less version` : `Version "${data["key"]}" already exists for item "${data["item"]}" in collection "${data["collection"]}"` });
70
72
  }
71
73
  async getMainItem(collection, item, query) {
72
74
  return await new ItemsService(collection, {
@@ -114,6 +116,7 @@ var VersionsService = class VersionsService extends ItemsService {
114
116
  if (!Array.isArray(data)) throw new InvalidPayloadError({ reason: "Input should be an array of items" });
115
117
  const keyCombos = /* @__PURE__ */ new Set();
116
118
  for (const item of data) {
119
+ if (isNil(item["item"])) continue;
117
120
  const keyCombo = `${item["key"]}-${item["collection"]}-${item["item"]}`;
118
121
  if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot create multiple versions on "${item["item"]}" in collection "${item["collection"]}" with the same key "${item["key"]}"` });
119
122
  keyCombos.add(keyCombo);
@@ -139,7 +142,20 @@ var VersionsService = class VersionsService extends ItemsService {
139
142
  const item = "item" in data ? data["item"] : existingVersion.item;
140
143
  const key = "key" in data ? data["key"] : existingVersion.key;
141
144
  if (key !== VERSION_KEY_DRAFT && item === null) throw new InvalidPayloadError({ reason: `"key" must be "${VERSION_KEY_DRAFT}" for versions not linked to an item` });
142
- if (item === null) continue;
145
+ if (item === null) {
146
+ if (this.schema.collections[collection]?.singleton) {
147
+ await this.assertSingletonEmpty(collection);
148
+ if ((await super.readByQuery({
149
+ aggregate: { count: ["*"] },
150
+ filter: {
151
+ id: { _neq: pk },
152
+ collection: { _eq: collection },
153
+ item: { _null: true }
154
+ }
155
+ }))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Singleton collection "${collection}" already has an item-less version` });
156
+ }
157
+ continue;
158
+ }
143
159
  const keyCombo = `${key}-${collection}-${item}`;
144
160
  if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot update multiple versions on "${item}" in collection "${collection}" to the same key "${key}"` });
145
161
  keyCombos.add(keyCombo);
@@ -282,6 +298,7 @@ var VersionsService = class VersionsService extends ItemsService {
282
298
  let updatedItemKey;
283
299
  if (item) updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
284
300
  else {
301
+ await this.assertSingletonEmpty(collection);
285
302
  updatedItemKey = await itemsService.createOne(payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
286
303
  await this.updateOne(version, { item: String(updatedItemKey) });
287
304
  }
@@ -297,6 +314,11 @@ var VersionsService = class VersionsService extends ItemsService {
297
314
  });
298
315
  return updatedItemKey;
299
316
  }
317
+ async assertSingletonEmpty(collection) {
318
+ const collectionMeta = this.schema.collections[collection];
319
+ if (!collectionMeta?.singleton) return;
320
+ if (await this.knex(collection).first(collectionMeta.primary)) throw new UnprocessableContentError({ reason: `Singleton collection "${collection}" already contains an item` });
321
+ }
300
322
  mapDelta(version) {
301
323
  const delta = version.delta ?? {};
302
324
  delta[this.schema.collections[version.collection].primary] = version.item;
@@ -27,6 +27,9 @@ const defaultAdminPolicy = {
27
27
  async function createAdmin(schema, admin) {
28
28
  const logger = useLogger();
29
29
  const env = useEnv();
30
+ const adminEmail = admin?.email ?? env["ADMIN_EMAIL"];
31
+ const adminPassword = admin?.password ?? env["ADMIN_PASSWORD"];
32
+ if (!adminEmail || !adminPassword) return;
30
33
  logger.info("Setting up first admin role...");
31
34
  const accessService = new AccessService({ schema });
32
35
  const policiesService = new PoliciesService({ schema });
@@ -37,9 +40,6 @@ async function createAdmin(schema, admin) {
37
40
  role
38
41
  });
39
42
  const usersService = new UsersService({ schema });
40
- const adminEmail = admin?.email ?? env["ADMIN_EMAIL"];
41
- const adminPassword = admin?.password ?? env["ADMIN_PASSWORD"];
42
- if (!adminEmail || !adminPassword) return;
43
43
  const token = env["ADMIN_TOKEN"] ?? null;
44
44
  logger.info("Adding first admin user...");
45
45
  await usersService.createOne({
@@ -34,6 +34,7 @@ async function getCacheKey(req) {
34
34
  path,
35
35
  query: isGraphQl ? getGraphqlQueryAndVariables(req) : req.sanitizedQuery,
36
36
  ...flowTriggerQuery && { rawQuery: flowTriggerQuery },
37
+ ...req.accountability?.share && { share: req.accountability.share },
37
38
  ...includeIp && { ip: req.accountability.ip }
38
39
  });
39
40
  }
@@ -0,0 +1,15 @@
1
+ //#region src/utils/is-unauthenticated.ts
2
+ /**
3
+ * Checks if the given accountability is unauthenticated
4
+ *
5
+ * @param accountability
6
+ * @returns True if the user is unauthenticated, false otherwise.
7
+ */
8
+ function isUnauthenticated(accountability) {
9
+ if (accountability === null) return false;
10
+ if (accountability === void 0) return true;
11
+ return accountability?.role === null && accountability?.user === null;
12
+ }
13
+
14
+ //#endregion
15
+ export { isUnauthenticated };
@@ -143,7 +143,7 @@ async function sanitizeDeep(deep, schema, accountability) {
143
143
  for (const [key, value] of Object.entries(level)) {
144
144
  if (!key) break;
145
145
  if (key.startsWith("_")) subQuery[key.substring(1)] = value;
146
- else if (isPlainObject(value)) parse(value, [...path, key]);
146
+ else if (isPlainObject(value)) await parse(value, [...path, key]);
147
147
  }
148
148
  if (Object.keys(subQuery).length > 0) {
149
149
  const parsedSubQuery = await sanitizeQuery(subQuery, schema, accountability);
@@ -13,7 +13,7 @@ function useStore(namespace, options) {
13
13
  namespace,
14
14
  redis: useRedis()
15
15
  };
16
- if (config.type === "redis" && options?.ttl) config.ttl = options?.ttl;
16
+ if (options?.ttl) config.ttl = options?.ttl;
17
17
  const store = createCache(config);
18
18
  return (callback) => store.usingLock(`lock`, async () => {
19
19
  return await callback({
@@ -144,7 +144,8 @@ async function handleVersion(self, key, query, opts) {
144
144
  return result;
145
145
  });
146
146
  const env = useEnv();
147
- if (results.length < (query.limit ?? Number(env["QUERY_LIMIT_DEFAULT"]))) results.push(...itemlessErrors.map((errorMeta) => {
147
+ const effectiveLimit = query.limit ?? Number(env["QUERY_LIMIT_DEFAULT"]);
148
+ if (effectiveLimit === -1 || results.length < effectiveLimit) results.push(...itemlessErrors.map((errorMeta) => {
148
149
  let item = { $meta: errorMeta };
149
150
  if (errorMeta.error) item = Object.assign({}, defaultItem, item, pick(errorMeta.delta, requestedFields));
150
151
  return item;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "36.0.0-rc.0",
3
+ "version": "36.0.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -166,30 +166,30 @@
166
166
  "ws": "8.18.3",
167
167
  "zod": "4.1.12",
168
168
  "zod-validation-error": "4.0.2",
169
- "@directus/app": "16.0.0-rc.0",
170
- "@directus/env": "6.0.0-rc.0",
171
- "@directus/constants": "14.4.0-rc.0",
172
- "@directus/ai": "1.3.2-rc.0",
173
- "@directus/extensions": "4.0.0-rc.0",
174
- "@directus/extensions-registry": "4.0.0-rc.0",
175
- "@directus/errors": "2.4.0-rc.0",
176
- "@directus/extensions-sdk": "18.0.0-rc.0",
177
- "@directus/memory": "4.0.0-rc.0",
178
- "@directus/format-title": "13.0.0-rc.0",
179
- "@directus/pressure": "4.0.0-rc.0",
180
- "@directus/schema": "14.0.0-rc.0",
181
- "@directus/storage": "13.0.0-rc.0",
182
- "@directus/specs": "14.0.0-rc.0",
183
- "@directus/storage-driver-azure": "13.0.0-rc.0",
184
- "@directus/storage-driver-cloudinary": "13.0.0-rc.0",
185
- "@directus/storage-driver-gcs": "13.0.0-rc.0",
186
- "@directus/storage-driver-supabase": "4.0.0-rc.0",
187
- "@directus/storage-driver-s3": "13.0.0-rc.0",
188
- "@directus/storage-driver-local": "13.0.0-rc.0",
189
- "@directus/utils": "13.5.0-rc.0",
190
- "@directus/system-data": "4.5.0-rc.0",
191
- "@directus/validation": "3.0.0-rc.0",
192
- "directus": "12.0.0-rc.1"
169
+ "@directus/app": "16.0.0",
170
+ "@directus/ai": "1.3.2",
171
+ "@directus/env": "6.0.0",
172
+ "@directus/constants": "14.4.0",
173
+ "@directus/errors": "2.4.0",
174
+ "@directus/extensions-registry": "4.0.0",
175
+ "@directus/extensions": "4.0.0",
176
+ "@directus/format-title": "13.0.0",
177
+ "@directus/memory": "4.0.0",
178
+ "@directus/extensions-sdk": "18.0.0",
179
+ "@directus/schema": "14.0.0",
180
+ "@directus/pressure": "4.0.0",
181
+ "@directus/storage": "13.0.0",
182
+ "@directus/storage-driver-azure": "13.0.0",
183
+ "@directus/storage-driver-local": "13.0.0",
184
+ "@directus/storage-driver-gcs": "13.0.0",
185
+ "@directus/storage-driver-cloudinary": "13.0.0",
186
+ "@directus/specs": "14.0.0",
187
+ "@directus/storage-driver-s3": "13.0.0",
188
+ "@directus/system-data": "4.5.0",
189
+ "@directus/storage-driver-supabase": "4.0.0",
190
+ "@directus/utils": "13.5.0",
191
+ "@directus/validation": "3.0.0",
192
+ "directus": "12.0.0"
193
193
  },
194
194
  "devDependencies": {
195
195
  "@directus/tsconfig": "4.0.0",
@@ -224,15 +224,15 @@
224
224
  "@types/stream-json": "1.7.8",
225
225
  "@types/wellknown": "0.5.8",
226
226
  "@types/ws": "8.18.1",
227
- "@vitest/coverage-v8": "3.2.4",
227
+ "@vitest/coverage-v8": "3.2.6",
228
228
  "copyfiles": "2.4.1",
229
229
  "form-data": "4.0.4",
230
230
  "get-port": "7.1.0",
231
231
  "knex-mock-client": "3.0.2",
232
232
  "typescript": "5.9.3",
233
- "vitest": "3.2.4",
234
- "@directus/schema-builder": "1.0.0-rc.0",
235
- "@directus/types": "16.0.0-rc.0"
233
+ "vitest": "3.2.6",
234
+ "@directus/schema-builder": "1.0.0",
235
+ "@directus/types": "16.0.0"
236
236
  },
237
237
  "optionalDependencies": {
238
238
  "@keyv/redis": "3.0.1",