@directus/api 35.2.0 → 36.0.0-rc.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.
Files changed (188) hide show
  1. package/dist/ai/chat/models/chat-request.js +48 -48
  2. package/dist/ai/chat/models/object-request.js +6 -6
  3. package/dist/ai/chat/models/providers.js +14 -14
  4. package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
  5. package/dist/ai/mcp/server.js +44 -6
  6. package/dist/ai/mcp/utils.js +31 -0
  7. package/dist/ai/tools/assets/index.js +3 -3
  8. package/dist/ai/tools/collections/index.js +18 -18
  9. package/dist/ai/tools/fields/index.js +18 -18
  10. package/dist/ai/tools/files/index.js +18 -18
  11. package/dist/ai/tools/flows/index.js +16 -16
  12. package/dist/ai/tools/folders/index.js +18 -18
  13. package/dist/ai/tools/items/index.js +17 -17
  14. package/dist/ai/tools/operations/index.js +16 -16
  15. package/dist/ai/tools/relations/index.js +22 -22
  16. package/dist/ai/tools/schema/index.js +3 -3
  17. package/dist/ai/tools/schema.js +159 -159
  18. package/dist/ai/tools/system/index.js +3 -3
  19. package/dist/ai/tools/trigger-flow/index.js +3 -3
  20. package/dist/app.js +33 -9
  21. package/dist/auth/drivers/ldap.js +3 -1
  22. package/dist/auth/drivers/local.js +2 -0
  23. package/dist/auth/drivers/oauth2.js +3 -1
  24. package/dist/auth/drivers/openid.js +3 -1
  25. package/dist/auth/drivers/saml.js +2 -0
  26. package/dist/auth/utils/check-local-disabled.js +16 -0
  27. package/dist/auth/utils/check-sso-enabled.js +14 -0
  28. package/dist/auth.js +8 -5
  29. package/dist/cli/commands/bootstrap/index.js +3 -0
  30. package/dist/cli/commands/cache/clear.js +6 -1
  31. package/dist/cli/commands/roles/create.js +4 -1
  32. package/dist/cli/commands/users/create.js +3 -0
  33. package/dist/constants.js +8 -1
  34. package/dist/controllers/access.js +1 -1
  35. package/dist/controllers/activity.js +2 -1
  36. package/dist/controllers/assets.js +2 -0
  37. package/dist/controllers/auth.js +13 -5
  38. package/dist/controllers/collections.js +1 -1
  39. package/dist/controllers/comments.js +1 -1
  40. package/dist/controllers/dashboards.js +1 -1
  41. package/dist/controllers/fields.js +1 -1
  42. package/dist/controllers/files.js +3 -1
  43. package/dist/controllers/flows.js +6 -5
  44. package/dist/controllers/folders.js +1 -1
  45. package/dist/controllers/graphql.js +2 -0
  46. package/dist/controllers/items.js +3 -1
  47. package/dist/controllers/license.js +119 -0
  48. package/dist/controllers/mcp/index.js +38 -0
  49. package/dist/controllers/mcp/oauth-clients.js +68 -0
  50. package/dist/controllers/mcp/oauth-consent-page.js +316 -0
  51. package/dist/controllers/mcp/oauth.js +381 -0
  52. package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
  53. package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
  54. package/dist/controllers/notifications.js +1 -1
  55. package/dist/controllers/operations.js +1 -1
  56. package/dist/controllers/panels.js +1 -1
  57. package/dist/controllers/permissions.js +1 -1
  58. package/dist/controllers/policies.js +1 -1
  59. package/dist/controllers/presets.js +1 -1
  60. package/dist/controllers/revisions.js +3 -2
  61. package/dist/controllers/roles.js +1 -1
  62. package/dist/controllers/server.js +38 -9
  63. package/dist/controllers/shares.js +1 -1
  64. package/dist/controllers/translations.js +1 -1
  65. package/dist/controllers/users.js +1 -1
  66. package/dist/controllers/utils.js +2 -2
  67. package/dist/controllers/versions.js +12 -5
  68. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
  69. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
  70. package/dist/database/helpers/fn/dialects/mysql.js +7 -12
  71. package/dist/database/helpers/fn/dialects/oracle.js +3 -4
  72. package/dist/database/helpers/fn/dialects/postgres.js +4 -26
  73. package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
  74. package/dist/database/helpers/fn/json/parse-function.js +14 -6
  75. package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
  76. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
  77. package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
  78. package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
  79. package/dist/database/migrations/20260507A-add-licensing.js +22 -0
  80. package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
  81. package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
  82. package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
  83. package/dist/database/run-ast/lib/apply-query/index.js +4 -1
  84. package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
  85. package/dist/database/run-ast/lib/get-db-query.js +21 -9
  86. package/dist/database/run-ast/lib/parse-current-level.js +2 -1
  87. package/dist/database/run-ast/run-ast.js +2 -1
  88. package/dist/database/run-ast/utils/get-column.js +2 -1
  89. package/dist/extensions/lib/installation/manager.js +1 -1
  90. package/dist/extensions/lib/sandbox/register/operation.js +1 -1
  91. package/dist/extensions/lib/sync/sync.js +1 -1
  92. package/dist/extensions/manager.js +3 -3
  93. package/dist/flows.js +5 -5
  94. package/dist/license/entitlements/lib/collections.js +37 -0
  95. package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
  96. package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
  97. package/dist/license/entitlements/lib/flows.js +29 -0
  98. package/dist/license/entitlements/lib/seats.js +103 -0
  99. package/dist/license/entitlements/lib/sso-enabled.js +45 -0
  100. package/dist/license/entitlements/manager.js +256 -0
  101. package/dist/license/index.js +4 -0
  102. package/dist/license/manager.js +505 -0
  103. package/dist/license/utils/compute-license-status.js +27 -0
  104. package/dist/license/utils/get-core-grace-expires-at.js +38 -0
  105. package/dist/license/utils/get-license-key.js +23 -0
  106. package/dist/license/utils/get-license-token.js +23 -0
  107. package/dist/license/utils/handle-license-error.js +41 -0
  108. package/dist/license/utils/is-in-core-grace-period.js +11 -0
  109. package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
  110. package/dist/license/utils/use-rpc.js +33 -0
  111. package/dist/middleware/cache.js +4 -1
  112. package/dist/middleware/error-handler.js +11 -0
  113. package/dist/middleware/extract-token.js +11 -2
  114. package/dist/middleware/is-admin.js +16 -0
  115. package/dist/middleware/is-locked.js +16 -0
  116. package/dist/middleware/mcp-oauth-guard.js +23 -0
  117. package/dist/middleware/request-counter.js +5 -2
  118. package/dist/packages/types/dist/index.js +117 -122
  119. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
  120. package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
  121. package/dist/request/is-denied-ip.js +2 -0
  122. package/dist/schedules/license.js +31 -0
  123. package/dist/schedules/oauth-cleanup.js +26 -0
  124. package/dist/schedules/retention.js +1 -1
  125. package/dist/schedules/telemetry.js +4 -1
  126. package/dist/schedules/tus.js +1 -1
  127. package/dist/schedules/utils/duration-to-cron.js +36 -0
  128. package/dist/services/activity.js +15 -0
  129. package/dist/services/authentication.js +12 -5
  130. package/dist/services/collections.js +40 -10
  131. package/dist/services/fields.js +6 -6
  132. package/dist/services/flows.js +12 -0
  133. package/dist/services/graphql/resolvers/system-admin.js +2 -2
  134. package/dist/services/graphql/resolvers/system-global.js +1 -1
  135. package/dist/services/graphql/resolvers/system.js +34 -18
  136. package/dist/services/graphql/schema/get-types.js +23 -2
  137. package/dist/services/graphql/schema/parse-query.js +8 -0
  138. package/dist/services/graphql/schema/read.js +12 -0
  139. package/dist/services/graphql/types/json-filter.js +30 -0
  140. package/dist/services/index.js +6 -6
  141. package/dist/services/items.js +32 -14
  142. package/dist/services/mcp-oauth/cimd.js +307 -0
  143. package/dist/services/mcp-oauth/index.js +1185 -0
  144. package/dist/services/mcp-oauth/types/error.js +22 -0
  145. package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
  146. package/dist/services/mcp-oauth/utils/domain.js +21 -0
  147. package/dist/services/mcp-oauth/utils/loopback.js +11 -0
  148. package/dist/services/mcp-oauth/utils/redirect.js +84 -0
  149. package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
  150. package/dist/services/payload.js +2 -1
  151. package/dist/services/permissions.js +31 -9
  152. package/dist/services/revisions.js +15 -0
  153. package/dist/services/server.js +21 -4
  154. package/dist/services/settings.js +37 -3
  155. package/dist/services/users.js +13 -6
  156. package/dist/services/utils.js +6 -1
  157. package/dist/services/versions.js +137 -69
  158. package/dist/utils/calculate-field-depth.js +1 -0
  159. package/dist/utils/deep-freeze.js +24 -0
  160. package/dist/utils/extract-function-name.js +13 -0
  161. package/dist/utils/generate-translations.js +5 -5
  162. package/dist/utils/get-accountability-for-token.js +13 -1
  163. package/dist/utils/get-cache-key.js +1 -1
  164. package/dist/utils/get-history-filter-query.js +22 -0
  165. package/dist/utils/get-schema.js +2 -2
  166. package/dist/utils/get-service.js +3 -3
  167. package/dist/utils/is-admin.js +9 -0
  168. package/dist/utils/parse-oauth-scope.js +12 -0
  169. package/dist/utils/sanitize-query.js +1 -1
  170. package/dist/utils/split-field-path.js +29 -0
  171. package/dist/utils/transaction.js +2 -2
  172. package/dist/utils/translations-validation.js +2 -2
  173. package/dist/utils/validate-query.js +35 -4
  174. package/dist/utils/validate-user-count-integrity.js +28 -5
  175. package/dist/utils/verify-session-jwt.js +5 -2
  176. package/dist/utils/versioning/handle-version.js +130 -48
  177. package/dist/utils/versioning/remove-circular.js +17 -0
  178. package/dist/websocket/authenticate.js +2 -1
  179. package/dist/websocket/collab/collab.js +1 -1
  180. package/dist/websocket/collab/room.js +1 -1
  181. package/dist/websocket/controllers/base.js +12 -0
  182. package/dist/websocket/controllers/graphql.js +1 -1
  183. package/dist/websocket/handlers/subscribe.js +1 -1
  184. package/dist/websocket/messages.js +64 -64
  185. package/dist/websocket/utils/items.js +2 -2
  186. package/license +90 -80
  187. package/package.json +33 -32
  188. package/dist/controllers/mcp.js +0 -31
@@ -61,7 +61,7 @@ function sanitizeFields(rawFields) {
61
61
  }
62
62
  function sanitizeSort(rawSort) {
63
63
  let fields = [];
64
- if (typeof rawSort === "string") fields = rawSort.split(",");
64
+ if (typeof rawSort === "string") fields = splitFields(rawSort);
65
65
  else if (Array.isArray(rawSort)) fields = rawSort;
66
66
  fields = fields.map((field) => field.trim());
67
67
  return fields;
@@ -0,0 +1,29 @@
1
+ //#region src/utils/split-field-path.ts
2
+ /**
3
+ * Splits a field path on dots that are outside parentheses.
4
+ *
5
+ * A plain `.split('.')` breaks when a path segment contains a function call
6
+ * whose arguments include dots (e.g. `category_id.json(metadata, settings.theme)`).
7
+ * This helper only splits on dots at paren-depth 0.
8
+ *
9
+ * @example
10
+ * splitFieldPath('a.b.c') // ['a', 'b', 'c']
11
+ * splitFieldPath('category_id.json(metadata, color)') // ['category_id', 'json(metadata, color)']
12
+ * splitFieldPath('a.json(meta, x.y.z)') // ['a', 'json(meta, x.y.z)']
13
+ */
14
+ function splitFieldPath(path) {
15
+ const parts = [];
16
+ let depth = 0;
17
+ let start = 0;
18
+ for (let i = 0; i < path.length; i++) if (path[i] === "(") depth++;
19
+ else if (path[i] === ")") depth--;
20
+ else if (path[i] === "." && depth === 0) {
21
+ parts.push(path.slice(start, i));
22
+ start = i + 1;
23
+ }
24
+ parts.push(path.slice(start));
25
+ return parts;
26
+ }
27
+
28
+ //#endregion
29
+ export { splitFieldPath };
@@ -11,8 +11,8 @@ import "knex";
11
11
  * Can be used to ensure the handler is run within a transaction,
12
12
  * while preventing nested transactions.
13
13
  */
14
- const transaction = async (knex$1, handler) => {
15
- if (knex$1.isTransaction) return handler(knex$1);
14
+ const transaction = async (knex$1, handler, forceNewTransaction = false) => {
15
+ if (knex$1.isTransaction && forceNewTransaction === false) return handler(knex$1);
16
16
  else try {
17
17
  return await knex$1.transaction((trx) => handler(trx));
18
18
  } catch (error) {
@@ -1,9 +1,9 @@
1
- import { z } from "zod";
1
+ import { z as z$1 } from "zod";
2
2
  import { REGEX_DB_SAFE_IDENTIFIER } from "@directus/constants";
3
3
 
4
4
  //#region src/utils/translations-validation.ts
5
5
  const DB_SAFE_IDENTIFIER_MESSAGE = "must be a db-safe identifier (letters, numbers, underscores; cannot start with a number)";
6
- const dbSafeIdentifierSchema = z.string().trim().min(1).max(63).regex(REGEX_DB_SAFE_IDENTIFIER, DB_SAFE_IDENTIFIER_MESSAGE);
6
+ const dbSafeIdentifierSchema = z$1.string().trim().min(1).max(63).regex(REGEX_DB_SAFE_IDENTIFIER, DB_SAFE_IDENTIFIER_MESSAGE);
7
7
 
8
8
  //#endregion
9
9
  export { dbSafeIdentifierSchema };
@@ -1,3 +1,5 @@
1
+ import { parseJsonFunction, parseJsonPath } from "../database/helpers/fn/json/parse-function.js";
2
+ import { extractFunctionName } from "./extract-function-name.js";
1
3
  import { calculateFieldDepth } from "./calculate-field-depth.js";
2
4
  import { getFieldRelationalDepth } from "./get-field-relational-depth.js";
3
5
  import { useEnv } from "@directus/env";
@@ -30,6 +32,7 @@ function validateQuery(query) {
30
32
  const { error } = querySchema.validate(query);
31
33
  if (query.filter && Object.keys(query.filter).length > 0) validateFilter(query.filter);
32
34
  if (query.alias) validateAlias(query.alias);
35
+ if (query.sort) validateSort(query.sort);
33
36
  validateRelationalDepth(query);
34
37
  if (error) throw new InvalidQueryError({ reason: error.message });
35
38
  return query;
@@ -57,6 +60,9 @@ function validateFilter(filter) {
57
60
  case "_nintersects_bbox":
58
61
  validateGeometry(value, key);
59
62
  break;
63
+ case "_json":
64
+ validateJsonFilter(value);
65
+ break;
60
66
  case "_none":
61
67
  case "_some":
62
68
  validateFilter(nested);
@@ -110,12 +116,36 @@ function validateGeometry(value, key) {
110
116
  }
111
117
  return true;
112
118
  }
119
+ function validateJsonFilter(value) {
120
+ if (!isPlainObject(value)) throw new InvalidQueryError({ reason: `"_json" filter value must be an object` });
121
+ for (const [path, innerFilter] of Object.entries(value)) {
122
+ if (path.length === 0) throw new InvalidQueryError({ reason: `"_json" path key must be a non-empty string` });
123
+ if (path === "_or" || path === "_and") {
124
+ if (!Array.isArray(innerFilter)) throw new InvalidQueryError({ reason: `"_json" logical operator "${path}" must be an array` });
125
+ for (const subFilter of innerFilter) validateJsonFilter(subFilter);
126
+ continue;
127
+ }
128
+ parseJsonPath(path);
129
+ if (!isPlainObject(innerFilter)) throw new InvalidQueryError({ reason: `"_json" inner filter for path "${path}" must be an object` });
130
+ const nestedPathKey = Object.keys(innerFilter).find((k) => !k.startsWith("_"));
131
+ if (nestedPathKey) throw new InvalidQueryError({ reason: `"_json" path "${path}" cannot contain a nested path "${nestedPathKey}"; use a single flat path like "${path}.${nestedPathKey}"` });
132
+ validateFilter(innerFilter);
133
+ }
134
+ }
113
135
  function validateAlias(alias) {
114
136
  if (isPlainObject(alias) === false) throw new InvalidQueryError({ reason: `"alias" has to be an object` });
115
137
  for (const [key, value] of Object.entries(alias)) {
116
138
  if (typeof key !== "string") throw new InvalidQueryError({ reason: `"alias" key has to be a string. "${typeof key}" given` });
117
139
  if (typeof value !== "string") throw new InvalidQueryError({ reason: `"alias" value has to be a string. "${typeof key}" given` });
118
- if (key.includes(".") || value.includes(".")) throw new InvalidQueryError({ reason: `"alias" key/value can't contain a period character \`.\`` });
140
+ if (key.includes(".")) throw new InvalidQueryError({ reason: `"alias" key can't contain a period character \`.\`` });
141
+ if (extractFunctionName(value) === "json") parseJsonFunction(value);
142
+ else if (value.includes(".")) throw new InvalidQueryError({ reason: `"alias" value can't contain a period character \`.\`` });
143
+ }
144
+ }
145
+ function validateSort(sort) {
146
+ for (const sortField of sort) {
147
+ const field = sortField.startsWith("-") ? sortField.slice(1) : sortField;
148
+ if (extractFunctionName(field) === "json") parseJsonFunction(field);
119
149
  }
120
150
  }
121
151
  function validateRelationalDepth(query) {
@@ -133,12 +163,13 @@ function validateRelationalDepth(query) {
133
163
  */
134
164
  if (query.group) fields = query.group;
135
165
  fields = uniq(fields);
136
- for (const field of fields) if (getFieldRelationalDepth(field) > maxRelationalDepth) throw new InvalidQueryError({ reason: "Max relational depth exceeded" });
166
+ for (const field of fields) if (getFieldRelationalDepth(query.alias?.[field] ?? field) > maxRelationalDepth) throw new InvalidQueryError({ reason: "Max relational depth exceeded" });
137
167
  if (query.filter) {
138
168
  if (calculateFieldDepth(query.filter) > maxRelationalDepth) throw new InvalidQueryError({ reason: "Max relational depth exceeded" });
139
169
  }
140
- if (query.sort) {
141
- for (const sort of query.sort) if (sort.split(".").length > maxRelationalDepth) throw new InvalidQueryError({ reason: "Max relational depth exceeded" });
170
+ if (query.sort) for (const sort of query.sort) {
171
+ const field = sort.startsWith("-") ? sort.slice(1) : sort;
172
+ if (getFieldRelationalDepth(query.alias?.[field] ?? field) > maxRelationalDepth) throw new InvalidQueryError({ reason: "Max relational depth exceeded" });
142
173
  }
143
174
  if (query.deep) {
144
175
  if (calculateFieldDepth(query.deep, ["_sort"]) > maxRelationalDepth) throw new InvalidQueryError({ reason: "Max relational depth exceeded" });
@@ -3,21 +3,44 @@ import { validateRemainingAdminCount } from "../permissions/modules/validate-rem
3
3
  import { fetchUserCount } from "./fetch-user-count/fetch-user-count.js";
4
4
  import { checkUserLimits } from "../telemetry/utils/check-user-limits.js";
5
5
  import { shouldCheckUserLimits } from "../telemetry/utils/should-check-user-limits.js";
6
+ import { LimitExceededError } from "@directus/errors";
6
7
 
7
8
  //#region src/utils/validate-user-count-integrity.ts
8
9
  async function validateUserCountIntegrity(options) {
9
10
  const validateUserLimits = (options.flags & UserIntegrityCheckFlag.UserLimits) !== 0;
10
11
  const validateRemainingAdminUsers = (options.flags & UserIntegrityCheckFlag.RemainingAdmins) !== 0;
11
- const limitCheck = validateUserLimits && shouldCheckUserLimits();
12
- if (!validateRemainingAdminUsers && !limitCheck) return;
13
- const adminOnly = validateRemainingAdminUsers && !limitCheck;
12
+ const envLimitCheck = validateUserLimits && shouldCheckUserLimits();
13
+ if (!validateRemainingAdminUsers && !validateUserLimits) return;
14
+ const adminOnly = validateRemainingAdminUsers && !validateUserLimits;
14
15
  const userCounts = await fetchUserCount({
15
16
  ...options,
16
17
  adminOnly
17
18
  });
18
- if (limitCheck) await checkUserLimits(userCounts);
19
+ if (validateUserLimits) await checkSeatsCount(userCounts, options.previousSeatCount);
20
+ if (envLimitCheck) await checkUserLimits(userCounts);
19
21
  if (validateRemainingAdminUsers) validateRemainingAdminCount(userCounts.admin);
22
+ if (validateUserLimits && options.previousSeatCount !== userCounts.admin + userCounts.app) {
23
+ const { getEntitlementManager } = await import("../license/entitlements/manager.js");
24
+ await getEntitlementManager().clearCache("seats", "sso_enabled");
25
+ }
26
+ }
27
+ async function checkSeatsCount(userCounts, previousSeatCount) {
28
+ const { getEntitlementManager } = await import("../license/entitlements/manager.js");
29
+ const seatLimit = getEntitlementManager().getEntitlementLimit("seats");
30
+ const newCount = userCounts.admin + userCounts.app;
31
+ if (seatLimit === -1 || newCount <= seatLimit) return;
32
+ if (previousSeatCount === void 0 || newCount > previousSeatCount) throw new LimitExceededError({ category: "seats" });
33
+ }
34
+ /**
35
+ * Must be called at the top of a mutation transaction, before any writes, using
36
+ * the transactional `knex` — that's the only way to read pre-state correctly on
37
+ * every database.
38
+ */
39
+ async function captureSeatCount(knex, flags = UserIntegrityCheckFlag.None) {
40
+ if ((flags & UserIntegrityCheckFlag.UserLimits) === 0) return void 0;
41
+ const { countActiveSeats } = await import("../license/entitlements/lib/seats.js");
42
+ return await countActiveSeats({ knex });
20
43
  }
21
44
 
22
45
  //#endregion
23
- export { validateUserCountIntegrity };
46
+ export { captureSeatCount, validateUserCountIntegrity };
@@ -5,14 +5,17 @@ import { InvalidCredentialsError } from "@directus/errors";
5
5
  /**
6
6
  * Verifies the associated session is still available and valid.
7
7
  *
8
+ * @returns The oauth_client for the session, or null for regular sessions.
8
9
  * @throws If session not found.
9
10
  */
10
11
  async function verifySessionJWT(payload) {
11
- if (!await database_default().select(1).from("directus_sessions").where({
12
+ const session = await database_default().select("oauth_client").from("directus_sessions").where({
12
13
  token: payload["session"],
13
14
  user: payload["id"] || null,
14
15
  share: payload["share"] || null
15
- }).andWhere("expires", ">=", /* @__PURE__ */ new Date()).first()) throw new InvalidCredentialsError();
16
+ }).andWhere("expires", ">=", /* @__PURE__ */ new Date()).first();
17
+ if (!session) throw new InvalidCredentialsError();
18
+ return { oauth_client: session.oauth_client ?? null };
16
19
  }
17
20
 
18
21
  //#endregion
@@ -1,77 +1,159 @@
1
1
  import { transaction } from "../transaction.js";
2
+ import { removeCircular } from "./remove-circular.js";
2
3
  import { splitRecursive } from "./split-recursive.js";
4
+ import { useEnv } from "@directus/env";
3
5
  import { ForbiddenError } from "@directus/errors";
4
- import { deepMapWithSchema } from "@directus/utils";
6
+ import { deepMapWithSchema, getRelationInfo } from "@directus/utils";
7
+ import { cloneDeep, intersection, pick, uniq } from "lodash-es";
8
+ import { getNodeEnv } from "@directus/utils/node";
5
9
 
6
10
  //#region src/utils/versioning/handle-version.ts
7
- async function handleVersion(self, key, queryWithKey, opts) {
11
+ async function handleVersion(self, key, query, opts) {
8
12
  const { VersionsService } = await import("../../services/versions.js");
9
13
  const { ItemsService } = await import("../../services/items.js");
10
- if (queryWithKey.versionRaw) {
11
- const originalData = await self.readByQuery(queryWithKey, opts);
14
+ if (key && query.versionRaw) {
15
+ const version = query.version;
16
+ delete query.version;
17
+ delete query.versionRaw;
18
+ const originalData = await self.readByQuery(query, opts);
12
19
  if (originalData.length === 0) throw new ForbiddenError();
13
- const version$1 = await new VersionsService({
20
+ const versions$1 = await new VersionsService({
14
21
  schema: self.schema,
15
22
  accountability: self.accountability,
16
23
  knex: self.knex
17
- }).getVersionSave(queryWithKey.version, self.collection, key);
18
- return Object.assign(originalData[0], version$1?.delta);
24
+ }).getVersionSaves(version, self.collection, key);
25
+ return [Object.assign(originalData[0], versions$1?.[0]?.delta)];
19
26
  }
20
- let result;
21
- const versionsService = new VersionsService({
27
+ const versions = await new VersionsService({
22
28
  schema: self.schema,
23
29
  accountability: self.accountability,
24
30
  knex: self.knex
25
- });
31
+ }).getVersionSaves(query.version, self.collection, key, false);
32
+ if (key && versions.length === 0) throw new ForbiddenError();
33
+ if (versions.length === 0) return [];
34
+ let results = [];
26
35
  const createdIDs = {};
27
- const version = await versionsService.getVersionSave(queryWithKey.version, self.collection, key, false);
28
- if (!version) throw new ForbiddenError();
29
- const { delta } = version;
36
+ const itemlessErrors = [];
37
+ const itemMeta = {};
38
+ const primaryKeyField = self.schema.collections[self.collection].primary;
39
+ const hasPrimaryKeyInQuery = query.fields?.includes(primaryKeyField) || query.fields?.includes("*") || query.fields?.length === 0;
30
40
  await transaction(self.knex, async (trx) => {
31
- const itemsServiceAdmin = new ItemsService(self.collection, {
32
- schema: self.schema,
33
- accountability: { admin: true },
34
- knex: trx
35
- });
36
- if (delta) {
41
+ for (const version of versions) {
42
+ const { id, item } = version;
43
+ let delta = version.delta;
44
+ if (!delta && item) {
45
+ itemMeta[item] = {
46
+ version_id: id,
47
+ delta: {}
48
+ };
49
+ continue;
50
+ }
51
+ delta = delta ?? {};
37
52
  const { rawDelta, defaultOverwrites } = splitRecursive(delta);
38
- await itemsServiceAdmin.updateOne(key, rawDelta, {
39
- emitEvents: false,
40
- autoPurgeCache: false,
41
- skipTracking: true,
42
- overwriteDefaults: defaultOverwrites,
43
- onItemCreate: (collection, pk) => {
44
- if (collection in createdIDs === false) createdIDs[collection] = [];
45
- createdIDs[collection].push(pk);
46
- }
47
- });
53
+ try {
54
+ await transaction(trx, async (trxInner) => {
55
+ const sudoItemsService = new ItemsService(self.collection, {
56
+ schema: self.schema,
57
+ knex: trxInner
58
+ });
59
+ if (!item) {
60
+ const item$1 = await sudoItemsService.createOne(rawDelta, {
61
+ emitEvents: false,
62
+ autoPurgeCache: false,
63
+ skipTracking: true,
64
+ overwriteDefaults: defaultOverwrites,
65
+ onItemCreate: (collection, pk) => {
66
+ if (collection in createdIDs === false) createdIDs[collection] = [];
67
+ createdIDs[collection].push(pk);
68
+ }
69
+ });
70
+ itemMeta[item$1] = { version_id: id };
71
+ } else {
72
+ await sudoItemsService.updateOne(item, rawDelta, {
73
+ emitEvents: false,
74
+ autoPurgeCache: false,
75
+ skipTracking: true,
76
+ overwriteDefaults: defaultOverwrites,
77
+ onItemCreate: (collection, pk) => {
78
+ if (collection in createdIDs === false) createdIDs[collection] = [];
79
+ createdIDs[collection].push(pk);
80
+ }
81
+ });
82
+ itemMeta[item] = { version_id: id };
83
+ }
84
+ }, true);
85
+ } catch (error) {
86
+ if (key) throw error;
87
+ sanitizeError(error);
88
+ if (!item) itemlessErrors.push({
89
+ error,
90
+ version_id: id,
91
+ delta
92
+ });
93
+ else itemMeta[item] = {
94
+ error,
95
+ version_id: id,
96
+ delta
97
+ };
98
+ }
48
99
  }
49
- result = (await new ItemsService(self.collection, {
100
+ const itemsServiceUser = new ItemsService(self.collection, {
50
101
  schema: self.schema,
51
102
  accountability: self.accountability,
52
103
  knex: trx
53
- }).readByQuery(queryWithKey, opts))[0];
104
+ });
105
+ query = cloneDeep(query);
106
+ delete query.version;
107
+ const ids = uniq([...createdIDs[self.collection] ?? [], ...versions.map((version) => version.item).filter(Boolean)]);
108
+ query.filter = { _and: [...query.filter ? [query.filter] : [], { [primaryKeyField]: { _in: ids } }] };
109
+ if (!hasPrimaryKeyInQuery) query.fields = [primaryKeyField, ...query.fields ?? []];
110
+ results = await itemsServiceUser.readByQuery(query, { ...opts });
54
111
  await trx.rollback();
55
112
  });
56
- if (!result) throw new ForbiddenError();
57
- return deepMapWithSchema(result, ([key$1, value], context) => {
58
- if (context.relationType === "m2o" || context.relationType === "a2o") {
59
- if (createdIDs[context.relation.related_collection]?.find((id) => String(id) === String(value))) return [key$1, null];
60
- } else if (context.relationType === "o2m" && Array.isArray(value)) {
61
- const ids = createdIDs[context.relation.collection];
62
- return [key$1, value.map((val) => {
63
- if (ids?.find((id) => String(id) === String(val))) return null;
64
- return val;
65
- })];
66
- }
67
- if (context.field.field === context.collection.primary) {
68
- if (createdIDs[context.collection.collection]?.find((id) => String(id) === String(value))) return [key$1, null];
113
+ let requestedFields = Object.values(self.schema.collections[self.collection].fields).filter((field) => {
114
+ return getRelationInfo(self.schema.relations, self.collection, field.field).relationType === null;
115
+ }).map((field) => field.field);
116
+ const queryFields = query.fields?.map((field) => field.split(".")[0]) ?? [];
117
+ if (!queryFields?.includes("*")) requestedFields = intersection(requestedFields, queryFields);
118
+ const defaultItem = Object.fromEntries(requestedFields.map((field) => [field, null]));
119
+ results = results.map((result) => {
120
+ const meta = itemMeta[result[primaryKeyField]];
121
+ if (!hasPrimaryKeyInQuery) delete result[primaryKeyField];
122
+ result = deepMapWithSchema(result, ([key$1, value], context) => {
123
+ if (context.relationType === "m2o" || context.relationType === "a2o") {
124
+ if (createdIDs[context.relation.related_collection]?.find((id) => String(id) === String(value))) return [key$1, null];
125
+ } else if (context.relationType === "o2m" && Array.isArray(value)) {
126
+ const ids = createdIDs[context.relation.collection];
127
+ return [key$1, value.map((val) => {
128
+ if (ids?.find((id) => String(id) === String(val))) return null;
129
+ return val;
130
+ })];
131
+ }
132
+ if (context.field.field === context.collection.primary) {
133
+ if (createdIDs[context.collection.collection]?.find((id) => String(id) === String(value))) return [key$1, null];
134
+ }
135
+ return [key$1, value];
136
+ }, {
137
+ collection: self.collection,
138
+ schema: self.schema
139
+ });
140
+ if (meta) {
141
+ result["$meta"] = meta;
142
+ if (meta.error) result = Object.assign({}, defaultItem, result, pick(meta.delta, requestedFields));
69
143
  }
70
- return [key$1, value];
71
- }, {
72
- collection: self.collection,
73
- schema: self.schema
144
+ return result;
74
145
  });
146
+ const env = useEnv();
147
+ if (results.length < (query.limit ?? Number(env["QUERY_LIMIT_DEFAULT"]))) results.push(...itemlessErrors.map((errorMeta) => {
148
+ let item = { $meta: errorMeta };
149
+ if (errorMeta.error) item = Object.assign({}, defaultItem, item, pick(errorMeta.delta, requestedFields));
150
+ return item;
151
+ }));
152
+ return results;
153
+ }
154
+ function sanitizeError(error) {
155
+ if (getNodeEnv() !== "development") delete error.stack;
156
+ removeCircular(error);
75
157
  }
76
158
 
77
159
  //#endregion
@@ -0,0 +1,17 @@
1
+ //#region src/utils/versioning/remove-circular.ts
2
+ function removeCircular(obj, seen = /* @__PURE__ */ new WeakSet()) {
3
+ if (assertIsRecord(obj)) {
4
+ if (seen.has(obj)) return null;
5
+ seen.add(obj);
6
+ for (const key in obj) if (typeof obj[key] === "object") {
7
+ if (removeCircular(obj[key], seen) === null) delete obj[key];
8
+ }
9
+ }
10
+ return obj;
11
+ }
12
+ function assertIsRecord(val) {
13
+ return typeof val === "object" && val !== null;
14
+ }
15
+
16
+ //#endregion
17
+ export { removeCircular };
@@ -1,8 +1,8 @@
1
1
  import { DEFAULT_AUTH_PROVIDER } from "../constants.js";
2
2
  import database_default from "../database/index.js";
3
3
  import emitter_default from "../emitter.js";
4
- import { getSchema } from "../utils/get-schema.js";
5
4
  import { createDefaultAccountability } from "../permissions/utils/create-default-accountability.js";
5
+ import { getSchema } from "../utils/get-schema.js";
6
6
  import { AuthenticationService } from "../services/authentication.js";
7
7
  import { getAccountabilityForToken } from "../utils/get-accountability-for-token.js";
8
8
  import { WebSocketError } from "./errors.js";
@@ -39,6 +39,7 @@ async function authenticateConnection(message, accountabilityOverrides) {
39
39
  });
40
40
  if (customAccountability && isEqual(customAccountability, defaultAccountability) === false) authenticationState.accountability = customAccountability;
41
41
  else authenticationState.accountability = await getAccountabilityForToken(access_token, defaultAccountability);
42
+ if (authenticationState.accountability.oauth) throw new Error("OAuth sessions are not allowed on WebSocket connections");
42
43
  return authenticationState;
43
44
  } catch {
44
45
  throw new WebSocketError("auth", "AUTH_FAILED", "Authentication failed.", message["uid"]);
@@ -3,8 +3,8 @@ import { ClientMessage, TYPE } from "../../packages/types/dist/index.js";
3
3
  import database_default from "../../database/index.js";
4
4
  import { validateItemAccess } from "../../permissions/modules/validate-access/lib/validate-item-access.js";
5
5
  import emitter_default from "../../emitter.js";
6
- import { getSchema } from "../../utils/get-schema.js";
7
6
  import { scheduleSynchronizedJob } from "../../utils/schedule.js";
7
+ import { getSchema } from "../../utils/get-schema.js";
8
8
  import { SettingsService } from "../../services/settings.js";
9
9
  import { getMessageType } from "../utils/message.js";
10
10
  import { isFieldAllowed } from "../../utils/is-field-allowed.js";
@@ -1,8 +1,8 @@
1
1
  import { useLogger } from "../../logger/index.js";
2
2
  import { ACTION, COLORS, TYPE } from "../../packages/types/dist/index.js";
3
3
  import database_default from "../../database/index.js";
4
- import { getSchema } from "../../utils/get-schema.js";
5
4
  import { getService } from "../../utils/get-service.js";
5
+ import { getSchema } from "../../utils/get-schema.js";
6
6
  import { isFieldAllowed } from "../../utils/is-field-allowed.js";
7
7
  import { useStore } from "./store.js";
8
8
  import { Messenger } from "./messenger.js";
@@ -9,6 +9,7 @@ import { authenticateConnection, authenticationSuccess } from "../authenticate.j
9
9
  import { AuthMode, WebSocketAuthMessage } from "../messages.js";
10
10
  import { getMessageType } from "../utils/message.js";
11
11
  import { waitForAnyMessage, waitForMessageType } from "../utils/wait-for-message.js";
12
+ import { getLicenseManager } from "../../license/manager.js";
12
13
  import { useEnv } from "@directus/env";
13
14
  import { InvalidProviderConfigError, TokenExpiredError } from "@directus/errors";
14
15
  import { parseJSON, toBoolean } from "@directus/utils";
@@ -81,6 +82,12 @@ var SocketController = class {
81
82
  async handleUpgrade(request, socket, head) {
82
83
  const { pathname, query } = parse(request.url, true);
83
84
  if (pathname !== this.endpoint) return;
85
+ if (await getLicenseManager().isLocked()) {
86
+ logger.debug("WebSocket upgrade denied - License is in a locked state and must be resolved");
87
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
88
+ socket.destroy();
89
+ return;
90
+ }
84
91
  if (this.clients.size >= this.maxConnections) {
85
92
  logger.debug("WebSocket upgrade denied - max connections reached");
86
93
  socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
@@ -186,6 +193,11 @@ var SocketController = class {
186
193
  logger.debug(`WebSocket#${client.uid} is rate limited`);
187
194
  return;
188
195
  }
196
+ if (await getLicenseManager().isLocked()) {
197
+ handleWebSocketError(client, new WebSocketError("license", "SERVICE_UNAVAILABLE", `License is in a locked state and must be resolved`), "server");
198
+ logger.debug(`WebSocket#${client.uid} closed due to license in locked state`);
199
+ return;
200
+ }
189
201
  let message;
190
202
  try {
191
203
  message = this.parseMessage(data.toString());
@@ -1,6 +1,6 @@
1
1
  import { useLogger } from "../../logger/index.js";
2
- import { getSchema } from "../../utils/get-schema.js";
3
2
  import { createDefaultAccountability } from "../../permissions/utils/create-default-accountability.js";
3
+ import { getSchema } from "../../utils/get-schema.js";
4
4
  import { getAddress } from "../../utils/get-address.js";
5
5
  import { bindPubSub } from "../../services/graphql/subscription.js";
6
6
  import { handleWebSocketError } from "../errors.js";
@@ -2,8 +2,8 @@ import { useBus } from "../../bus/lib/use-bus.js";
2
2
  import "../../bus/index.js";
3
3
  import emitter_default from "../../emitter.js";
4
4
  import { getSchema } from "../../utils/get-schema.js";
5
- import { sanitizeQuery } from "../../utils/sanitize-query.js";
6
5
  import { getPayload } from "../utils/items.js";
6
+ import { sanitizeQuery } from "../../utils/sanitize-query.js";
7
7
  import { WebSocketError, handleWebSocketError } from "../errors.js";
8
8
  import { WebSocketSubscribeMessage } from "../messages.js";
9
9
  import { fmtMessage, getMessageType } from "../utils/message.js";