@directus/api 32.2.0 → 33.1.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 (297) hide show
  1. package/dist/ai/chat/controllers/chat.post.js +19 -4
  2. package/dist/ai/chat/lib/create-ui-stream.d.ts +8 -7
  3. package/dist/ai/chat/lib/create-ui-stream.js +28 -25
  4. package/dist/ai/chat/middleware/load-settings.js +31 -7
  5. package/dist/ai/chat/models/chat-request.d.ts +135 -2
  6. package/dist/ai/chat/models/chat-request.js +56 -2
  7. package/dist/ai/chat/models/providers.d.ts +16 -2
  8. package/dist/ai/chat/models/providers.js +16 -2
  9. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
  10. package/dist/ai/chat/utils/format-context.d.ts +5 -0
  11. package/dist/ai/chat/utils/format-context.js +122 -0
  12. package/dist/ai/mcp/server.d.ts +27 -1
  13. package/dist/ai/providers/index.d.ts +3 -0
  14. package/dist/ai/providers/index.js +3 -0
  15. package/dist/ai/providers/options.d.ts +14 -0
  16. package/dist/ai/providers/options.js +26 -0
  17. package/dist/ai/providers/registry.d.ts +6 -0
  18. package/dist/ai/providers/registry.js +65 -0
  19. package/dist/ai/providers/types.d.ts +34 -0
  20. package/dist/ai/providers/types.js +1 -0
  21. package/dist/ai/tools/assets/index.js +1 -1
  22. package/dist/ai/tools/collections/index.js +2 -2
  23. package/dist/ai/tools/fields/index.js +2 -2
  24. package/dist/ai/tools/files/index.js +1 -1
  25. package/dist/ai/tools/flows/index.js +1 -1
  26. package/dist/ai/tools/folders/index.js +1 -1
  27. package/dist/ai/tools/items/index.js +6 -3
  28. package/dist/ai/tools/items/prompt.md +7 -9
  29. package/dist/ai/tools/relations/index.js +1 -1
  30. package/dist/ai/tools/schema.js +1 -1
  31. package/dist/ai/tools/trigger-flow/index.js +1 -1
  32. package/dist/app.js +12 -8
  33. package/dist/auth/drivers/ldap.d.ts +1 -1
  34. package/dist/auth/drivers/ldap.js +144 -139
  35. package/dist/auth/drivers/local.js +1 -1
  36. package/dist/auth/drivers/oauth2.d.ts +1 -2
  37. package/dist/auth/drivers/oauth2.js +22 -17
  38. package/dist/auth/drivers/openid.d.ts +1 -2
  39. package/dist/auth/drivers/openid.js +18 -13
  40. package/dist/auth/drivers/saml.js +3 -3
  41. package/dist/auth/utils/generate-callback-url.d.ts +11 -0
  42. package/dist/auth/utils/generate-callback-url.js +40 -0
  43. package/dist/auth/utils/is-login-redirect-allowed.d.ts +7 -0
  44. package/dist/{utils → auth/utils}/is-login-redirect-allowed.js +12 -9
  45. package/dist/cache.d.ts +12 -0
  46. package/dist/cache.js +27 -3
  47. package/dist/cli/commands/bootstrap/index.js +2 -2
  48. package/dist/cli/commands/database/install.js +1 -1
  49. package/dist/cli/commands/database/migrate.js +1 -1
  50. package/dist/cli/commands/init/index.js +2 -2
  51. package/dist/cli/commands/roles/create.js +4 -4
  52. package/dist/cli/commands/schema/apply.js +3 -3
  53. package/dist/cli/commands/schema/snapshot.js +1 -1
  54. package/dist/cli/utils/create-db-connection.d.ts +1 -1
  55. package/dist/cli/utils/create-db-connection.js +1 -1
  56. package/dist/cli/utils/create-env/env-stub.liquid +3 -0
  57. package/dist/cli/utils/create-env/index.js +1 -1
  58. package/dist/constants.d.ts +7 -3
  59. package/dist/constants.js +7 -3
  60. package/dist/controllers/access.js +1 -1
  61. package/dist/controllers/assets.js +1 -1
  62. package/dist/controllers/deployment.js +481 -0
  63. package/dist/controllers/extensions.js +1 -1
  64. package/dist/controllers/fields.js +8 -6
  65. package/dist/controllers/files.js +1 -1
  66. package/dist/controllers/items.js +1 -1
  67. package/dist/controllers/not-found.js +1 -1
  68. package/dist/controllers/relations.js +1 -1
  69. package/dist/database/errors/dialects/mysql.d.ts +1 -1
  70. package/dist/database/errors/dialects/postgres.d.ts +1 -1
  71. package/dist/database/errors/dialects/sqlite.d.ts +1 -1
  72. package/dist/database/errors/translate.d.ts +1 -1
  73. package/dist/database/errors/translate.js +1 -1
  74. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
  75. package/dist/database/helpers/date/dialects/mssql.js +1 -1
  76. package/dist/database/helpers/date/dialects/mysql.js +1 -1
  77. package/dist/database/helpers/date/types.js +1 -1
  78. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -0
  79. package/dist/database/helpers/schema/dialects/cockroachdb.js +24 -1
  80. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  81. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  82. package/dist/database/helpers/schema/dialects/mysql.js +16 -3
  83. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  84. package/dist/database/helpers/schema/types.d.ts +13 -0
  85. package/dist/database/helpers/schema/types.js +24 -0
  86. package/dist/database/index.js +4 -4
  87. package/dist/database/migrations/20220429A-add-flows.js +1 -1
  88. package/dist/database/migrations/20230526A-migrate-translation-strings.js +1 -1
  89. package/dist/database/migrations/20231009A-update-csv-fields-to-text.js +1 -1
  90. package/dist/database/migrations/20240204A-marketplace.js +9 -7
  91. package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +15 -0
  92. package/dist/database/migrations/20240311A-deprecate-webhooks.js +1 -1
  93. package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
  94. package/dist/database/migrations/20240924A-migrate-legacy-comments.js +1 -1
  95. package/dist/database/migrations/20251014A-add-project-owner.js +1 -1
  96. package/dist/database/migrations/20251224A-remove-webhooks.d.ts +3 -0
  97. package/dist/database/migrations/20251224A-remove-webhooks.js +19 -0
  98. package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
  99. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
  100. package/dist/database/migrations/20260113A-add-revisions-index.d.ts +3 -0
  101. package/dist/database/migrations/20260113A-add-revisions-index.js +41 -0
  102. package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
  103. package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
  104. package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
  105. package/dist/database/migrations/20260204A-add-deployment.js +32 -0
  106. package/dist/database/migrations/run.js +3 -3
  107. package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
  108. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  109. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.js +1 -1
  110. package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
  111. package/dist/database/run-ast/lib/apply-query/filter/operator.js +1 -1
  112. package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
  113. package/dist/database/run-ast/utils/get-column-pre-processor.js +2 -2
  114. package/dist/database/run-ast/utils/get-column.js +1 -1
  115. package/dist/database/seeds/run.js +3 -3
  116. package/dist/deployment/deployment.d.ts +94 -0
  117. package/dist/deployment/deployment.js +29 -0
  118. package/dist/deployment/drivers/index.d.ts +1 -0
  119. package/dist/deployment/drivers/index.js +1 -0
  120. package/dist/deployment/drivers/vercel.d.ts +32 -0
  121. package/dist/deployment/drivers/vercel.js +208 -0
  122. package/dist/deployment/index.d.ts +2 -0
  123. package/dist/deployment/index.js +2 -0
  124. package/dist/deployment.d.ts +24 -0
  125. package/dist/deployment.js +39 -0
  126. package/dist/extensions/lib/get-extensions-path.js +1 -1
  127. package/dist/extensions/lib/get-extensions-settings.js +1 -1
  128. package/dist/extensions/lib/get-extensions.js +1 -1
  129. package/dist/extensions/lib/get-shared-deps-mapping.js +3 -3
  130. package/dist/extensions/lib/installation/manager.js +3 -3
  131. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -1
  132. package/dist/extensions/lib/sync/status.js +1 -1
  133. package/dist/extensions/lib/sync/sync.js +7 -7
  134. package/dist/extensions/lib/sync/utils.js +2 -2
  135. package/dist/extensions/manager.d.ts +1 -1
  136. package/dist/extensions/manager.js +8 -8
  137. package/dist/flows.d.ts +1 -1
  138. package/dist/logger/index.js +1 -1
  139. package/dist/logger/logs-stream.d.ts +1 -1
  140. package/dist/logger/logs-stream.js +1 -1
  141. package/dist/mailer.js +1 -1
  142. package/dist/metrics/lib/create-metrics.js +2 -2
  143. package/dist/middleware/authenticate.js +3 -3
  144. package/dist/middleware/collection-exists.js +1 -1
  145. package/dist/middleware/extract-token.js +1 -1
  146. package/dist/middleware/graphql.js +2 -2
  147. package/dist/middleware/respond.js +27 -14
  148. package/dist/middleware/validate-batch.js +1 -1
  149. package/dist/operations/exec/index.js +2 -1
  150. package/dist/operations/mail/index.js +1 -1
  151. package/dist/operations/mail/rate-limiter.js +2 -2
  152. package/dist/permissions/cache.js +5 -0
  153. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -1
  154. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -1
  155. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -2
  156. package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -1
  157. package/dist/permissions/modules/process-ast/process-ast.js +1 -1
  158. package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
  159. package/dist/permissions/modules/process-payload/process-payload.js +1 -1
  160. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +14 -2
  161. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +72 -13
  162. package/dist/permissions/modules/validate-access/validate-access.js +3 -2
  163. package/dist/rate-limiter.js +1 -1
  164. package/dist/request/is-denied-ip.js +1 -1
  165. package/dist/schedules/project.js +1 -1
  166. package/dist/schedules/telemetry.js +1 -1
  167. package/dist/schedules/tus.js +1 -1
  168. package/dist/server.js +6 -5
  169. package/dist/services/assets.d.ts +2 -1
  170. package/dist/services/assets.js +35 -8
  171. package/dist/services/authentication.js +2 -2
  172. package/dist/services/collections.js +1 -1
  173. package/dist/services/deployment-projects.d.ts +20 -0
  174. package/dist/services/deployment-projects.js +34 -0
  175. package/dist/services/deployment-runs.d.ts +13 -0
  176. package/dist/services/deployment-runs.js +6 -0
  177. package/dist/services/deployment.d.ts +40 -0
  178. package/dist/services/deployment.js +202 -0
  179. package/dist/services/extensions.d.ts +1 -1
  180. package/dist/services/files/utils/get-metadata.d.ts +1 -1
  181. package/dist/services/files/utils/get-metadata.js +1 -1
  182. package/dist/services/files.d.ts +1 -1
  183. package/dist/services/files.js +4 -4
  184. package/dist/services/graphql/index.d.ts +1 -1
  185. package/dist/services/graphql/index.js +1 -1
  186. package/dist/services/graphql/resolvers/mutation.js +1 -1
  187. package/dist/services/graphql/resolvers/system-admin.js +2 -3
  188. package/dist/services/graphql/schema/get-types.d.ts +1 -1
  189. package/dist/services/graphql/schema/read.js +1 -1
  190. package/dist/services/graphql/subscription.d.ts +1 -1
  191. package/dist/services/graphql/types/date.js +1 -1
  192. package/dist/services/graphql/types/hash.js +1 -1
  193. package/dist/services/graphql/utils/add-path-to-validation-error.js +1 -1
  194. package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
  195. package/dist/services/import-export.d.ts +1 -1
  196. package/dist/services/import-export.js +2 -2
  197. package/dist/services/index.d.ts +3 -1
  198. package/dist/services/index.js +3 -1
  199. package/dist/services/mail/index.js +2 -2
  200. package/dist/services/mail/rate-limiter.js +2 -2
  201. package/dist/services/payload.js +2 -2
  202. package/dist/services/schema.js +1 -1
  203. package/dist/services/server.js +13 -4
  204. package/dist/services/settings.js +2 -2
  205. package/dist/services/specifications.js +2 -2
  206. package/dist/services/tfa.js +1 -1
  207. package/dist/services/translations.js +1 -1
  208. package/dist/services/tus/data-store.d.ts +1 -3
  209. package/dist/services/tus/data-store.js +2 -5
  210. package/dist/services/tus/server.js +6 -6
  211. package/dist/services/users.js +4 -4
  212. package/dist/services/versions.js +1 -1
  213. package/dist/telemetry/lib/get-report.js +2 -0
  214. package/dist/telemetry/lib/send-report.d.ts +1 -1
  215. package/dist/telemetry/lib/send-report.js +1 -1
  216. package/dist/telemetry/lib/track.js +1 -1
  217. package/dist/telemetry/types/report.d.ts +8 -0
  218. package/dist/telemetry/utils/get-settings.d.ts +2 -0
  219. package/dist/telemetry/utils/get-settings.js +5 -0
  220. package/dist/test-utils/knex.js +1 -1
  221. package/dist/types/collection.d.ts +1 -1
  222. package/dist/utils/async-handler.d.ts +1 -1
  223. package/dist/utils/calculate-field-depth.js +1 -1
  224. package/dist/utils/compress.js +1 -1
  225. package/dist/utils/deep-map-response.d.ts +1 -1
  226. package/dist/utils/deep-map-response.js +2 -2
  227. package/dist/utils/get-cache-key.js +1 -1
  228. package/dist/utils/get-column-path.js +1 -1
  229. package/dist/utils/get-field-system-rows.js +1 -1
  230. package/dist/utils/get-ip-from-req.d.ts +1 -1
  231. package/dist/utils/get-ip-from-req.js +1 -1
  232. package/dist/utils/get-local-type.js +7 -3
  233. package/dist/utils/get-service.js +7 -3
  234. package/dist/utils/get-snapshot-diff.js +1 -1
  235. package/dist/utils/is-field-allowed.d.ts +4 -0
  236. package/dist/utils/is-field-allowed.js +9 -0
  237. package/dist/utils/is-url-allowed.js +1 -1
  238. package/dist/utils/jwt.js +1 -1
  239. package/dist/utils/sanitize-schema.d.ts +1 -1
  240. package/dist/utils/should-clear-cache.d.ts +1 -1
  241. package/dist/utils/should-skip-cache.js +2 -2
  242. package/dist/utils/validate-diff.js +1 -1
  243. package/dist/utils/validate-snapshot.js +3 -3
  244. package/dist/utils/validate-storage.js +2 -2
  245. package/dist/utils/verify-session-jwt.js +1 -1
  246. package/dist/utils/versioning/handle-version.js +1 -1
  247. package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
  248. package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
  249. package/dist/websocket/collab/collab.d.ts +63 -0
  250. package/dist/websocket/collab/collab.js +481 -0
  251. package/dist/websocket/collab/constants.d.ts +1 -0
  252. package/dist/websocket/collab/constants.js +13 -0
  253. package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
  254. package/dist/websocket/collab/filter-to-fields.js +11 -0
  255. package/dist/websocket/collab/messenger.d.ts +43 -0
  256. package/dist/websocket/collab/messenger.js +225 -0
  257. package/dist/websocket/collab/payload-permissions.d.ts +18 -0
  258. package/dist/websocket/collab/payload-permissions.js +158 -0
  259. package/dist/websocket/collab/permissions-cache.d.ts +52 -0
  260. package/dist/websocket/collab/permissions-cache.js +204 -0
  261. package/dist/websocket/collab/room.d.ts +125 -0
  262. package/dist/websocket/collab/room.js +593 -0
  263. package/dist/websocket/collab/store.d.ts +7 -0
  264. package/dist/websocket/collab/store.js +33 -0
  265. package/dist/websocket/collab/types.d.ts +21 -0
  266. package/dist/websocket/collab/types.js +1 -0
  267. package/dist/websocket/collab/verify-permissions.d.ts +11 -0
  268. package/dist/websocket/collab/verify-permissions.js +100 -0
  269. package/dist/websocket/controllers/base.d.ts +2 -2
  270. package/dist/websocket/controllers/base.js +3 -3
  271. package/dist/websocket/controllers/graphql.d.ts +1 -1
  272. package/dist/websocket/controllers/graphql.js +1 -1
  273. package/dist/websocket/controllers/logs.d.ts +1 -1
  274. package/dist/websocket/controllers/rest.d.ts +1 -1
  275. package/dist/websocket/controllers/rest.js +2 -2
  276. package/dist/websocket/handlers/heartbeat.js +1 -1
  277. package/dist/websocket/handlers/index.d.ts +2 -0
  278. package/dist/websocket/handlers/index.js +9 -0
  279. package/dist/websocket/handlers/items.js +2 -2
  280. package/dist/websocket/handlers/subscribe.js +1 -1
  281. package/dist/websocket/types.d.ts +1 -1
  282. package/dist/websocket/utils/items.d.ts +2 -2
  283. package/dist/websocket/utils/message.d.ts +1 -1
  284. package/dist/websocket/utils/message.js +2 -2
  285. package/dist/websocket/utils/wait-for-message.js +1 -1
  286. package/package.json +35 -33
  287. package/dist/controllers/webhooks.js +0 -74
  288. package/dist/services/webhooks.d.ts +0 -14
  289. package/dist/services/webhooks.js +0 -32
  290. package/dist/utils/get-relation-info.d.ts +0 -6
  291. package/dist/utils/get-relation-info.js +0 -43
  292. package/dist/utils/get-relation-type.d.ts +0 -6
  293. package/dist/utils/get-relation-type.js +0 -18
  294. package/dist/utils/is-login-redirect-allowed.d.ts +0 -4
  295. package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
  296. package/dist/utils/versioning/deep-map-with-schema.js +0 -81
  297. /package/dist/controllers/{webhooks.d.ts → deployment.d.ts} +0 -0
@@ -1,8 +1,8 @@
1
- import { useEnv } from '@directus/env';
2
- import { toArray } from '@directus/utils';
3
1
  import { randomUUID } from 'node:crypto';
4
2
  import { Readable } from 'node:stream';
5
3
  import { promisify } from 'node:util';
4
+ import { useEnv } from '@directus/env';
5
+ import { toArray } from '@directus/utils';
6
6
  import pm2 from 'pm2';
7
7
  import { AggregatorRegistry, Counter, Histogram, register } from 'prom-client';
8
8
  import { getCache } from '../../cache.js';
@@ -1,13 +1,13 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ErrorCode, isDirectusError } from '@directus/errors';
1
3
  import { isEqual } from 'lodash-es';
4
+ import { SESSION_COOKIE_OPTIONS } from '../constants.js';
2
5
  import getDatabase from '../database/index.js';
3
6
  import emitter from '../emitter.js';
4
7
  import { createDefaultAccountability } from '../permissions/utils/create-default-accountability.js';
5
8
  import asyncHandler from '../utils/async-handler.js';
6
9
  import { getAccountabilityForToken } from '../utils/get-accountability-for-token.js';
7
10
  import { getIPFromReq } from '../utils/get-ip-from-req.js';
8
- import { ErrorCode, isDirectusError } from '@directus/errors';
9
- import { useEnv } from '@directus/env';
10
- import { SESSION_COOKIE_OPTIONS } from '../constants.js';
11
11
  /**
12
12
  * Verify the passed JWT and assign the user ID and role to `req`
13
13
  */
@@ -2,8 +2,8 @@
2
2
  * Check if requested collection exists, and save it to req.collection
3
3
  */
4
4
  import { systemCollectionRows } from '@directus/system-data';
5
- import asyncHandler from '../utils/async-handler.js';
6
5
  import { createCollectionForbiddenError } from '../permissions/modules/process-ast/utils/validate-path/create-error.js';
6
+ import asyncHandler from '../utils/async-handler.js';
7
7
  const collectionExists = asyncHandler(async (req, _res, next) => {
8
8
  if (!req.params['collection'])
9
9
  return next();
@@ -1,5 +1,5 @@
1
- import { InvalidPayloadError } from '@directus/errors';
2
1
  import { useEnv } from '@directus/env';
2
+ import { InvalidPayloadError } from '@directus/errors';
3
3
  /**
4
4
  * Extract access token from
5
5
  *
@@ -1,9 +1,9 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { InvalidPayloadError, InvalidQueryError, MethodNotAllowedError } from '@directus/errors';
1
3
  import { parseJSON } from '@directus/utils';
2
4
  import { getOperationAST, parse, Source } from 'graphql';
3
- import { InvalidPayloadError, InvalidQueryError, MethodNotAllowedError } from '@directus/errors';
4
5
  import { GraphQLValidationError } from '../services/graphql/errors/validation.js';
5
6
  import asyncHandler from '../utils/async-handler.js';
6
- import { useEnv } from '@directus/env';
7
7
  export const parseGraphQL = asyncHandler(async (req, res, next) => {
8
8
  if (req.method !== 'GET' && req.method !== 'POST') {
9
9
  throw new MethodNotAllowedError({ allowed: ['GET', 'POST'], current: req.method });
@@ -15,6 +15,10 @@ export const respond = asyncHandler(async (req, res) => {
15
15
  const env = useEnv();
16
16
  const logger = useLogger();
17
17
  const { cache } = getCache();
18
+ // Support custom cache instance and TTL via res.locals
19
+ const cacheInstance = res.locals['cacheInstance'] || cache;
20
+ const cacheTTL = res.locals['cacheTTL'] ?? getMilliseconds(env['CACHE_TTL']);
21
+ const hasCustomCache = !!res.locals['cacheInstance'];
18
22
  let exceedsMaxSize = false;
19
23
  if (env['CACHE_VALUE_MAX_SIZE'] !== false) {
20
24
  const valueSize = res.locals['payload'] ? stringByteSize(JSON.stringify(res.locals['payload'])) : 0;
@@ -22,26 +26,35 @@ export const respond = asyncHandler(async (req, res) => {
22
26
  if (maxSize !== null)
23
27
  exceedsMaxSize = valueSize > maxSize;
24
28
  }
25
- if ((req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
26
- req.originalUrl?.startsWith('/auth') === false &&
27
- env['CACHE_ENABLED'] === true &&
28
- cache &&
29
- !req.sanitizedQuery.export &&
30
- res.locals['cache'] !== false &&
31
- exceedsMaxSize === false &&
32
- (await permissionsCacheable(req.collection, {
33
- knex: getDatabase(),
34
- schema: req.schema,
35
- }, req.accountability))) {
29
+ // Custom cache bypasses global cache settings (CACHE_ENABLED, permissionsCacheable)
30
+ const shouldCache = hasCustomCache
31
+ ? res.locals['cache'] !== false && cacheInstance && !req.sanitizedQuery.export && exceedsMaxSize === false
32
+ : (req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
33
+ req.originalUrl?.startsWith('/auth') === false &&
34
+ env['CACHE_ENABLED'] === true &&
35
+ cache &&
36
+ !req.sanitizedQuery.export &&
37
+ res.locals['cache'] !== false &&
38
+ exceedsMaxSize === false &&
39
+ (await permissionsCacheable(req.collection, {
40
+ knex: getDatabase(),
41
+ schema: req.schema,
42
+ }, req.accountability));
43
+ if (shouldCache) {
36
44
  const key = await getCacheKey(req);
37
45
  try {
38
- await setCacheValue(cache, key, res.locals['payload'], getMilliseconds(env['CACHE_TTL']));
39
- await setCacheValue(cache, `${key}__expires_at`, { exp: Date.now() + getMilliseconds(env['CACHE_TTL'], 0) });
46
+ await setCacheValue(cacheInstance, key, res.locals['payload'], cacheTTL);
47
+ await setCacheValue(cacheInstance, `${key}__expires_at`, { exp: Date.now() + cacheTTL });
40
48
  }
41
49
  catch (err) {
42
50
  logger.warn(err, `[cache] Couldn't set key ${key}. ${err}`);
43
51
  }
44
- res.setHeader('Cache-Control', getCacheControlHeader(req, getMilliseconds(env['CACHE_TTL']), true, true));
52
+ res.setHeader('Cache-Control', getCacheControlHeader(req, cacheTTL, !hasCustomCache, true));
53
+ res.setHeader('Vary', 'Origin, Cache-Control');
54
+ }
55
+ else if (res.locals['cacheTTL'] !== undefined) {
56
+ // Custom TTL for headers only (no storage) - useful when caching is handled elsewhere
57
+ res.setHeader('Cache-Control', getCacheControlHeader(req, cacheTTL, false, true));
45
58
  res.setHeader('Vary', 'Origin, Cache-Control');
46
59
  }
47
60
  else {
@@ -1,5 +1,5 @@
1
- import Joi from 'joi';
2
1
  import { InvalidPayloadError } from '@directus/errors';
2
+ import Joi from 'joi';
3
3
  import asyncHandler from '../utils/async-handler.js';
4
4
  import { sanitizeQuery } from '../utils/sanitize-query.js';
5
5
  import { validateQuery } from '../utils/validate-query.js';
@@ -1,5 +1,6 @@
1
- import { defineOperationApi } from '@directus/extensions';
2
1
  import { createRequire } from 'node:module';
2
+ import { defineOperationApi } from '@directus/extensions';
3
+ // eslint-disable-next-line import/order
3
4
  import { sieveFunctions } from '@directus/utils';
4
5
  const require = createRequire(import.meta.url);
5
6
  const ivm = require('isolated-vm');
@@ -1,7 +1,7 @@
1
1
  import { defineOperationApi } from '@directus/extensions';
2
+ import { useLogger } from '../../logger/index.js';
2
3
  import { MailService } from '../../services/mail/index.js';
3
4
  import { md } from '../../utils/md.js';
4
- import { useLogger } from '../../logger/index.js';
5
5
  import { useFlowsEmailRateLimiter } from './rate-limiter.js';
6
6
  const logger = useLogger();
7
7
  export default defineOperationApi({
@@ -1,8 +1,8 @@
1
1
  import { useEnv } from '@directus/env';
2
+ import { EmailLimitExceededError } from '@directus/errors';
3
+ import { toBoolean } from '@directus/utils';
2
4
  import { RateLimiterMemory, RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible';
3
5
  import { createRateLimiter } from '../../rate-limiter.js';
4
- import { toBoolean } from '@directus/utils';
5
- import { EmailLimitExceededError } from '@directus/errors';
6
6
  let emailRateLimiter;
7
7
  const env = useEnv();
8
8
  if (toBoolean(env['RATE_LIMITER_EMAIL_FLOWS_ENABLED']) === true) {
@@ -1,6 +1,10 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { defineCache } from '@directus/memory';
2
3
  import { redisConfigAvailable, useRedis } from '../redis/index.js';
4
+ import { getMilliseconds } from '../utils/get-milliseconds.js';
3
5
  const localOnly = redisConfigAvailable() === false;
6
+ const env = useEnv();
7
+ const ttl = getMilliseconds(env['CACHE_SYSTEM_TTL']);
4
8
  const config = localOnly
5
9
  ? {
6
10
  type: 'local',
@@ -11,6 +15,7 @@ const config = localOnly
11
15
  redis: {
12
16
  namespace: 'permissions',
13
17
  redis: useRedis(),
18
+ ...(ttl !== undefined ? { ttl } : {}),
14
19
  },
15
20
  local: {
16
21
  maxKeys: 100,
@@ -1,6 +1,6 @@
1
1
  import { uniq } from 'lodash-es';
2
- import { fetchPolicies } from '../../lib/fetch-policies.js';
3
2
  import { fetchPermissions } from '../../lib/fetch-permissions.js';
3
+ import { fetchPolicies } from '../../lib/fetch-policies.js';
4
4
  export async function fetchAllowedCollections({ action, accountability }, { knex, schema }) {
5
5
  if (accountability.admin) {
6
6
  return Object.keys(schema.collections);
@@ -1,6 +1,6 @@
1
1
  import { uniq } from 'lodash-es';
2
- import { fetchPolicies } from '../../lib/fetch-policies.js';
3
2
  import { fetchPermissions } from '../../lib/fetch-permissions.js';
3
+ import { fetchPolicies } from '../../lib/fetch-policies.js';
4
4
  export async function fetchAllowedFieldMap({ accountability, action }, { knex, schema }) {
5
5
  const fieldMap = {};
6
6
  if (accountability.admin) {
@@ -1,6 +1,6 @@
1
- import { uniq, intersection, difference } from 'lodash-es';
2
- import { fetchPolicies } from '../../lib/fetch-policies.js';
1
+ import { difference, intersection, uniq } from 'lodash-es';
3
2
  import { fetchPermissions } from '../../lib/fetch-permissions.js';
3
+ import { fetchPolicies } from '../../lib/fetch-policies.js';
4
4
  /**
5
5
  * Fetch a field map for fields that may or may not be null based on item-by-item permissions.
6
6
  */
@@ -1,5 +1,5 @@
1
- import { getUnaliasedFieldKey } from '../../../utils/get-unaliased-field-key.js';
2
1
  import { uniq } from 'lodash-es';
2
+ import { getUnaliasedFieldKey } from '../../../utils/get-unaliased-field-key.js';
3
3
  import { getCases } from './get-cases.js';
4
4
  /**
5
5
  * Mutates passed AST
@@ -3,8 +3,8 @@ import { fetchPolicies } from '../../lib/fetch-policies.js';
3
3
  import { fieldMapFromAst } from './lib/field-map-from-ast.js';
4
4
  import { injectCases } from './lib/inject-cases.js';
5
5
  import { collectionsInFieldMap } from './utils/collections-in-field-map.js';
6
- import { validatePathPermissions } from './utils/validate-path/validate-path-permissions.js';
7
6
  import { validatePathExistence } from './utils/validate-path/validate-path-existence.js';
7
+ import { validatePathPermissions } from './utils/validate-path/validate-path-permissions.js';
8
8
  export async function processAst(options, context) {
9
9
  // FieldMap is a Map of paths in the AST, with each path containing the collection and fields in
10
10
  // that collection that the AST path tries to access
@@ -1,4 +1,4 @@
1
- import { getRelationInfo } from '../../../../utils/get-relation-info.js';
1
+ import { getRelationInfo } from '@directus/utils';
2
2
  export function findRelatedCollection(collection, field, schema) {
3
3
  const { relation } = getRelationInfo(schema.relations, collection, field);
4
4
  if (!relation)
@@ -6,8 +6,8 @@ import { fetchPolicies } from '../../lib/fetch-policies.js';
6
6
  import { extractRequiredDynamicVariableContext } from '../../utils/extract-required-dynamic-variable-context.js';
7
7
  import { fetchDynamicVariableData } from '../../utils/fetch-dynamic-variable-data.js';
8
8
  import { contextHasDynamicVariables } from '../process-ast/utils/context-has-dynamic-variables.js';
9
- import { isFieldNullable } from './lib/is-field-nullable.js';
10
9
  import { createCollectionForbiddenError, createFieldsForbiddenError, } from '../process-ast/utils/validate-path/create-error.js';
10
+ import { isFieldNullable } from './lib/is-field-nullable.js';
11
11
  /**
12
12
  * @note this only validates the top-level fields. The expectation is that this function is called
13
13
  * for each level of nested insert separately
@@ -4,7 +4,19 @@ export interface ValidateItemAccessOptions {
4
4
  accountability: Accountability;
5
5
  action: PermissionsAction;
6
6
  collection: string;
7
- primaryKeys: PrimaryKey[];
7
+ primaryKeys?: PrimaryKey[];
8
8
  fields?: string[];
9
+ returnAllowedRootFields?: boolean;
9
10
  }
10
- export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<any>;
11
+ export interface ValidateItemAccessOptionsWithRootFields extends ValidateItemAccessOptions {
12
+ returnAllowedRootFields: true;
13
+ }
14
+ export interface ValidateItemAccessResult {
15
+ accessAllowed: boolean;
16
+ allowedRootFields?: string[];
17
+ }
18
+ export interface ValidateItemAccessResultWithRootFields extends ValidateItemAccessResult {
19
+ allowedRootFields: string[];
20
+ }
21
+ export declare function validateItemAccess(options: ValidateItemAccessOptionsWithRootFields, context: Context): Promise<ValidateItemAccessResultWithRootFields>;
22
+ export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<ValidateItemAccessResult>;
@@ -1,17 +1,28 @@
1
1
  import { toBoolean } from '@directus/utils';
2
2
  import { fetchPermittedAstRootFields } from '../../../../database/run-ast/modules/fetch-permitted-ast-root-fields.js';
3
+ import { fetchPermissions } from '../../../lib/fetch-permissions.js';
4
+ import { fetchPolicies } from '../../../lib/fetch-policies.js';
5
+ import { fetchAllowedFields } from '../../fetch-allowed-fields/fetch-allowed-fields.js';
6
+ import { injectCases } from '../../process-ast/lib/inject-cases.js';
3
7
  import { processAst } from '../../process-ast/process-ast.js';
4
8
  export async function validateItemAccess(options, context) {
5
- const primaryKeyField = context.schema.collections[options.collection]?.primary;
9
+ const collectionInfo = context.schema.collections[options.collection];
10
+ const primaryKeyField = collectionInfo?.primary;
6
11
  if (!primaryKeyField) {
7
12
  throw new Error(`Cannot find primary key for collection "${options.collection}"`);
8
13
  }
14
+ const isSingleton = collectionInfo?.singleton === true;
15
+ const hasPrimaryKeys = options.primaryKeys && options.primaryKeys.length > 0;
16
+ // For non-singletons, we must have PKs to validate against
17
+ if (!isSingleton && !hasPrimaryKeys) {
18
+ throw new Error(`Primary keys are required for non-singleton collection "${options.collection}"`);
19
+ }
9
20
  // When we're looking up access to specific items, we have to read them from the database to
10
21
  // make sure you are allowed to access them.
11
22
  const ast = {
12
23
  type: 'root',
13
24
  name: options.collection,
14
- query: { limit: options.primaryKeys.length },
25
+ query: { limit: isSingleton && !hasPrimaryKeys ? 1 : options.primaryKeys.length },
15
26
  // Act as if every field was a "normal" field
16
27
  children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [], alias: false })) ??
17
28
  [],
@@ -19,23 +30,71 @@ export async function validateItemAccess(options, context) {
19
30
  };
20
31
  await processAst({ ast, ...options }, context);
21
32
  // Inject the filter after the permissions have been processed, as to not require access to the primary key
22
- ast.query.filter = {
23
- [primaryKeyField]: {
24
- _in: options.primaryKeys,
25
- },
26
- };
33
+ // Skip adding filter for singletons without explicit PKs
34
+ if (hasPrimaryKeys) {
35
+ ast.query.filter = {
36
+ [primaryKeyField]: {
37
+ _in: options.primaryKeys,
38
+ },
39
+ };
40
+ }
41
+ let hasItemRules;
42
+ let permissionedFields;
43
+ // Inject the root fields after the permissions have been processed, as to not require access to all collection fields
44
+ if (options.returnAllowedRootFields) {
45
+ const allowedFields = await fetchAllowedFields({ accountability: options.accountability, action: options.action, collection: options.collection }, context);
46
+ const schemaFields = Object.keys(context.schema.collections[options.collection].fields);
47
+ const hasWildcard = allowedFields.includes('*');
48
+ permissionedFields = hasWildcard ? schemaFields : allowedFields;
49
+ const policies = await fetchPolicies(options.accountability, context);
50
+ const permissions = await fetchPermissions({ action: options.action, policies, collections: [options.collection], accountability: options.accountability }, context);
51
+ // Only inject cases if there are item-level permission rules
52
+ hasItemRules = permissions.some((p) => p.permissions && Object.keys(p.permissions).length > 0);
53
+ if (hasItemRules) {
54
+ // Create children only for fields that exist in schema and are allowed by permissions
55
+ ast.children = permissionedFields.map((field) => ({
56
+ type: 'field',
57
+ name: field,
58
+ fieldKey: field,
59
+ whenCase: [],
60
+ alias: false,
61
+ }));
62
+ injectCases(ast, permissions);
63
+ }
64
+ }
27
65
  const items = await fetchPermittedAstRootFields(ast, {
28
66
  schema: context.schema,
29
67
  accountability: options.accountability,
30
68
  knex: context.knex,
31
69
  action: options.action,
32
70
  });
33
- if (items && items.length === options.primaryKeys.length) {
34
- const { fields } = options;
35
- if (fields) {
36
- return items.every((item) => fields.every((field) => toBoolean(item[field])));
71
+ const expectedCount = isSingleton && !hasPrimaryKeys ? 1 : options.primaryKeys.length;
72
+ const hasAccess = items && items.length === expectedCount;
73
+ if (!hasAccess) {
74
+ if (options.returnAllowedRootFields) {
75
+ return { accessAllowed: false, allowedRootFields: [] };
76
+ }
77
+ return { accessAllowed: false };
78
+ }
79
+ let accessAllowed = true;
80
+ // If specific fields were requested, verify they are all accessible
81
+ if (options.fields) {
82
+ accessAllowed = items.every((item) => options.fields.every((field) => toBoolean(item[field])));
83
+ }
84
+ // If returnAllowedRootFields, return intersection of allowed fields across all items
85
+ if (options.returnAllowedRootFields) {
86
+ // If there are no item-level rules, return the permissioned fields directly
87
+ if (!hasItemRules) {
88
+ return {
89
+ accessAllowed,
90
+ allowedRootFields: permissionedFields,
91
+ };
37
92
  }
38
- return true;
93
+ const allowedRootFields = items.length > 0 ? Object.keys(items[0]).filter((field) => items.every((item) => item[field] === 1)) : [];
94
+ return {
95
+ accessAllowed,
96
+ allowedRootFields,
97
+ };
39
98
  }
40
- return false;
99
+ return { accessAllowed };
41
100
  }
@@ -1,7 +1,7 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
+ import { createCollectionForbiddenError } from '../process-ast/utils/validate-path/create-error.js';
2
3
  import { validateCollectionAccess } from './lib/validate-collection-access.js';
3
4
  import { validateItemAccess } from './lib/validate-item-access.js';
4
- import { createCollectionForbiddenError } from '../process-ast/utils/validate-path/create-error.js';
5
5
  /**
6
6
  * Validate if the current user has access to perform action against the given collection and
7
7
  * optional primary keys. This is done by reading the item from the database using the access
@@ -20,7 +20,8 @@ export async function validateAccess(options, context) {
20
20
  // from the database. If no keys are passed, we can simply check if the collection+action combo
21
21
  // exists within permissions
22
22
  if (options.primaryKeys) {
23
- access = await validateItemAccess(options, context);
23
+ const result = await validateItemAccess(options, context);
24
+ access = result.accessAllowed;
24
25
  }
25
26
  else {
26
27
  access = await validateCollectionAccess(options, context);
@@ -1,8 +1,8 @@
1
+ import { createRequire } from 'node:module';
1
2
  import { useEnv } from '@directus/env';
2
3
  import { merge } from 'lodash-es';
3
4
  import { RateLimiterMemory, RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible';
4
5
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
5
- import { createRequire } from 'node:module';
6
6
  const require = createRequire(import.meta.url);
7
7
  export function createRateLimiter(configPrefix = 'RATE_LIMITER', configOverrides) {
8
8
  const env = useEnv();
@@ -1,7 +1,7 @@
1
+ import os from 'node:os';
1
2
  import { useEnv } from '@directus/env';
2
3
  import { ipInNetworks } from '@directus/utils/node';
3
4
  import { matches } from 'ip-matching';
4
- import os from 'node:os';
5
5
  import { useLogger } from '../logger/index.js';
6
6
  export function isDeniedIp(ip) {
7
7
  const env = useEnv();
@@ -1,8 +1,8 @@
1
+ import { version } from 'directus/version';
1
2
  import { random } from 'lodash-es';
2
3
  import getDatabase from '../database/index.js';
3
4
  import { sendReport } from '../telemetry/index.js';
4
5
  import { scheduleSynchronizedJob } from '../utils/schedule.js';
5
- import { version } from 'directus/version';
6
6
  /**
7
7
  * Schedule the project status job
8
8
  */
@@ -1,8 +1,8 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { toBoolean } from '@directus/utils';
3
3
  import { getCache } from '../cache.js';
4
- import { scheduleSynchronizedJob } from '../utils/schedule.js';
5
4
  import { track } from '../telemetry/index.js';
5
+ import { scheduleSynchronizedJob } from '../utils/schedule.js';
6
6
  /**
7
7
  * Exported to be able to test the anonymous callback function
8
8
  */
@@ -1,6 +1,6 @@
1
1
  import { RESUMABLE_UPLOADS } from '../constants.js';
2
- import { getSchema } from '../utils/get-schema.js';
3
2
  import { createTusServer } from '../services/tus/index.js';
3
+ import { getSchema } from '../utils/get-schema.js';
4
4
  import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
5
5
  /**
6
6
  * Schedule the tus cleanup
package/dist/server.js CHANGED
@@ -1,21 +1,21 @@
1
+ import * as http from 'http';
2
+ import * as https from 'https';
3
+ import url from 'url';
1
4
  import { useEnv } from '@directus/env';
2
5
  import { toBoolean } from '@directus/utils';
3
6
  import { getNodeEnv } from '@directus/utils/node';
4
7
  import { createTerminus } from '@godaddy/terminus';
5
- import * as http from 'http';
6
- import * as https from 'https';
7
8
  import { once } from 'lodash-es';
8
9
  import qs from 'qs';
9
- import url from 'url';
10
10
  import createApp from './app.js';
11
11
  import getDatabase from './database/index.js';
12
12
  import emitter from './emitter.js';
13
13
  import { useLogger } from './logger/index.js';
14
+ import { getAddress } from './utils/get-address.js';
14
15
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
15
16
  import { getIPFromReq } from './utils/get-ip-from-req.js';
16
- import { getAddress } from './utils/get-address.js';
17
17
  import { createLogsController, createSubscriptionController, createWebSocketController, getLogsController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
18
- import { startWebSocketHandlers } from './websocket/handlers/index.js';
18
+ import { getCollabHandler, startWebSocketHandlers } from './websocket/handlers/index.js';
19
19
  export let SERVER_ONLINE = true;
20
20
  const env = useEnv();
21
21
  const logger = useLogger();
@@ -101,6 +101,7 @@ export async function createServer() {
101
101
  getSubscriptionController()?.terminate();
102
102
  getWebSocketController()?.terminate();
103
103
  getLogsController()?.terminate();
104
+ await getCollabHandler()?.terminate();
104
105
  const database = getDatabase();
105
106
  await database.destroy();
106
107
  logger.info('Database connections destroyed');
@@ -1,7 +1,7 @@
1
+ import type { Readable } from 'node:stream';
1
2
  import type { AbstractServiceOptions, Accountability, Range, SchemaOverview, Stat, TransformationSet } from '@directus/types';
2
3
  import archiver from 'archiver';
3
4
  import type { Knex } from 'knex';
4
- import type { Readable } from 'node:stream';
5
5
  import { FilesService } from './files.js';
6
6
  export declare class AssetsService {
7
7
  knex: Knex;
@@ -9,6 +9,7 @@ export declare class AssetsService {
9
9
  schema: SchemaOverview;
10
10
  sudoFilesService: FilesService;
11
11
  constructor(options: AbstractServiceOptions);
12
+ private sanitizeFields;
12
13
  private zip;
13
14
  zipFiles(files: string[]): Promise<{
14
15
  archive: archiver.Archiver;
@@ -1,22 +1,22 @@
1
+ import path from 'path';
1
2
  import { useEnv } from '@directus/env';
2
3
  import { ForbiddenError, IllegalAssetTransformationError, InvalidPayloadError, InvalidQueryError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
3
4
  import archiver from 'archiver';
4
5
  import { clamp } from 'lodash-es';
5
6
  import { contentType, extension } from 'mime-types';
6
7
  import hash from 'object-hash';
7
- import path from 'path';
8
8
  import sharp from 'sharp';
9
9
  import { SUPPORTED_IMAGE_TRANSFORM_FORMATS } from '../constants.js';
10
10
  import getDatabase from '../database/index.js';
11
11
  import { useLogger } from '../logger/index.js';
12
- import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
12
+ import { validateItemAccess } from '../permissions/modules/validate-access/lib/validate-item-access.js';
13
13
  import { getStorage } from '../storage/index.js';
14
14
  import { getMilliseconds } from '../utils/get-milliseconds.js';
15
15
  import { isValidUuid } from '../utils/is-valid-uuid.js';
16
16
  import * as TransformationUtils from '../utils/transformations.js';
17
17
  import { NameDeduper } from './assets/name-deduper.js';
18
- import { FilesService } from './files.js';
19
18
  import { getSharpInstance } from './files/lib/get-sharp-instance.js';
19
+ import { FilesService } from './files.js';
20
20
  import { FoldersService } from './folders.js';
21
21
  const env = useEnv();
22
22
  const logger = useLogger();
@@ -31,6 +31,20 @@ export class AssetsService {
31
31
  this.schema = options.schema;
32
32
  this.sudoFilesService = new FilesService({ ...options, accountability: null });
33
33
  }
34
+ sanitizeFields(file, allowedFields) {
35
+ if (allowedFields.includes('*')) {
36
+ return file;
37
+ }
38
+ const bypassFields = ['type', 'filesize'];
39
+ const fieldsToKeep = new Set([...allowedFields, ...bypassFields]);
40
+ const filteredFile = {};
41
+ for (const field of fieldsToKeep) {
42
+ if (field in file) {
43
+ filteredFile[field] = file[field];
44
+ }
45
+ }
46
+ return filteredFile;
47
+ }
34
48
  zip(options) {
35
49
  if (options.files.length === 0) {
36
50
  throw new InvalidPayloadError({ reason: 'No files found in the selected folders tree' });
@@ -135,13 +149,22 @@ export class AssetsService {
135
149
  */
136
150
  if (!isValidUuid(id))
137
151
  throw new ForbiddenError();
138
- if (systemPublicKeys.includes(id) === false && this.accountability) {
139
- await validateAccess({
152
+ let allowedFields = ['*'];
153
+ if (!systemPublicKeys.includes(id) && this.accountability && this.accountability.admin !== true) {
154
+ // Use validateItemAccess to check access and get allowed fields
155
+ const { allowedRootFields, accessAllowed } = await validateItemAccess({
140
156
  accountability: this.accountability,
141
157
  action: 'read',
142
158
  collection: 'directus_files',
143
159
  primaryKeys: [id],
160
+ returnAllowedRootFields: true,
144
161
  }, { knex: this.knex, schema: this.schema });
162
+ if (!accessAllowed) {
163
+ throw new ForbiddenError({
164
+ reason: `You don't have permission to perform "read" for collection "directus_files" or it does not exist.`,
165
+ });
166
+ }
167
+ allowedFields = allowedRootFields;
145
168
  }
146
169
  const file = (await this.sudoFilesService.readOne(id, { limit: 1 }));
147
170
  const exists = await storage.location(file.storage).exists(file.filename_disk);
@@ -195,7 +218,7 @@ export class AssetsService {
195
218
  const assetStream = () => storage.location(file.storage).read(assetFilename, { range });
196
219
  return {
197
220
  stream: deferStream ? assetStream : await assetStream(),
198
- file,
221
+ file: this.sanitizeFields(file, allowedFields),
199
222
  stat: await storage.location(file.storage).stat(assetFilename),
200
223
  };
201
224
  }
@@ -260,13 +283,17 @@ export class AssetsService {
260
283
  return {
261
284
  stream: deferStream ? assetStream : await assetStream(),
262
285
  stat: await storage.location(file.storage).stat(assetFilename),
263
- file,
286
+ file: this.sanitizeFields(file, allowedFields),
264
287
  };
265
288
  }
266
289
  else {
267
290
  const assetStream = () => storage.location(file.storage).read(file.filename_disk, { range, version });
268
291
  const stat = await storage.location(file.storage).stat(file.filename_disk);
269
- return { stream: deferStream ? assetStream : await assetStream(), file, stat };
292
+ return {
293
+ stream: deferStream ? assetStream : await assetStream(),
294
+ file: this.sanitizeFields(file, allowedFields),
295
+ stat,
296
+ };
270
297
  }
271
298
  }
272
299
  }
@@ -1,16 +1,16 @@
1
+ import { performance } from 'perf_hooks';
1
2
  import { Action } from '@directus/constants';
2
3
  import { useEnv } from '@directus/env';
3
4
  import { InvalidCredentialsError, InvalidOtpError, ServiceUnavailableError, UserSuspendedError, } from '@directus/errors';
4
5
  import jwt from 'jsonwebtoken';
5
6
  import { clone, cloneDeep } from 'lodash-es';
6
- import { performance } from 'perf_hooks';
7
7
  import { getAuthProvider } from '../auth.js';
8
8
  import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
9
9
  import getDatabase from '../database/index.js';
10
10
  import emitter from '../emitter.js';
11
11
  import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
12
12
  import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
13
- import { RateLimiterRes, createRateLimiter } from '../rate-limiter.js';
13
+ import { createRateLimiter, RateLimiterRes } from '../rate-limiter.js';
14
14
  import { getMilliseconds } from '../utils/get-milliseconds.js';
15
15
  import { getSecret } from '../utils/get-secret.js';
16
16
  import { stall } from '../utils/stall.js';