@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
@@ -0,0 +1,11 @@
1
+ import { deepMapFilter } from '@directus/utils';
2
+ export function filterToFields(filter, collection, schema) {
3
+ const fields = new Set();
4
+ deepMapFilter(filter, ([key, _value], context) => {
5
+ if (context.leaf && context.field) {
6
+ fields.add([...context.path, key].join('.'));
7
+ }
8
+ return undefined;
9
+ }, { collection, schema });
10
+ return Array.from(fields);
11
+ }
@@ -0,0 +1,43 @@
1
+ import type { Bus } from '@directus/memory';
2
+ import { type WebSocketClient } from '@directus/types';
3
+ import { type BroadcastMessage, type ClientID, type ServerError, type ServerMessage } from '@directus/types/collab';
4
+ type Instance = {
5
+ clients: ClientID[];
6
+ rooms: string[];
7
+ };
8
+ type Registry = Record<string, Instance>;
9
+ type RegistrySnapshot = {
10
+ inactive: Instance;
11
+ active: ClientID[];
12
+ };
13
+ type RoomMessage = Extract<BroadcastMessage, {
14
+ type: 'room';
15
+ }>;
16
+ export type RoomListener = (message: RoomMessage) => void;
17
+ export declare class Messenger {
18
+ uid: `${string}-${string}-${string}-${string}-${string}`;
19
+ store: <T>(callback: (store: import("./store.js").RedisStore<{
20
+ instances: Registry;
21
+ }>) => Promise<T>) => Promise<T>;
22
+ clients: Record<ClientID, WebSocketClient>;
23
+ orders: Record<ClientID, number>;
24
+ messenger: Bus;
25
+ roomListeners: Record<string, RoomListener>;
26
+ constructor();
27
+ hasClient(client: ClientID): boolean;
28
+ setRoomListener(room: string, callback: RoomListener): void;
29
+ removeRoomListener(room: string): void;
30
+ addClient(client: WebSocketClient): void;
31
+ removeClient(uid: ClientID): void;
32
+ registerRoom(uid: string): Promise<void>;
33
+ unregisterRoom(uid: string): Promise<void>;
34
+ getLocalClients(): Promise<ClientID[]>;
35
+ getGlobalClients(): Promise<ClientID[]>;
36
+ pruneDeadInstances(): Promise<RegistrySnapshot>;
37
+ sendRoom(room: string, message: Omit<RoomMessage, 'type' | 'room'>): void;
38
+ sendClient(client: ClientID, message: Omit<ServerMessage, 'order'>): void;
39
+ terminateClient(client: ClientID): void;
40
+ sendError(client: ClientID, error: ServerError): void;
41
+ handleError(client: ClientID, error: unknown, action?: ServerError['trigger']): void;
42
+ }
43
+ export {};
@@ -0,0 +1,225 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { useEnv } from '@directus/env';
3
+ import { isDirectusError } from '@directus/errors';
4
+ import { WS_TYPE } from '@directus/types';
5
+ import { COLLAB_BUS, } from '@directus/types/collab';
6
+ import { useBus } from '../../bus/index.js';
7
+ import { useLogger } from '../../logger/index.js';
8
+ import { useStore } from './store.js';
9
+ const env = useEnv();
10
+ const INSTANCE_TIMEOUT = Number(env['WEBSOCKETS_COLLAB_INSTANCE_TIMEOUT']);
11
+ export class Messenger {
12
+ uid;
13
+ store;
14
+ clients = {};
15
+ orders = {};
16
+ messenger = useBus();
17
+ roomListeners = {};
18
+ constructor() {
19
+ this.uid = randomUUID();
20
+ this.store = useStore('registry', { instances: {} });
21
+ this.store(async (store) => {
22
+ const instances = await store.get('instances');
23
+ instances[this.uid] = { clients: [], rooms: [] };
24
+ await store.set('instances', instances);
25
+ }).catch((err) => {
26
+ useLogger().error(err, '[Collab] Failed to register instance in registry');
27
+ });
28
+ this.messenger.subscribe(COLLAB_BUS, (message) => {
29
+ if (message.type === 'send') {
30
+ const client = this.clients[message.client];
31
+ if (client) {
32
+ const order = this.orders[client.uid] ?? 0;
33
+ this.orders[client.uid] = order + 1;
34
+ client.send(JSON.stringify({ ...message.message, order }));
35
+ }
36
+ }
37
+ else if (message.type === 'error') {
38
+ const client = this.clients[message.client];
39
+ if (client) {
40
+ client.send(JSON.stringify(message.message));
41
+ }
42
+ }
43
+ else if (message.type === 'terminate') {
44
+ this.clients[message.client]?.close();
45
+ }
46
+ else if (message.type === 'room') {
47
+ this.roomListeners[message.room]?.(message);
48
+ }
49
+ else if (message.type === 'ping' && message.instance === this.uid) {
50
+ this.messenger.publish(COLLAB_BUS, { type: 'pong', instance: this.uid });
51
+ }
52
+ });
53
+ }
54
+ hasClient(client) {
55
+ return client in this.clients;
56
+ }
57
+ setRoomListener(room, callback) {
58
+ this.roomListeners[room] = callback;
59
+ }
60
+ removeRoomListener(room) {
61
+ delete this.roomListeners[room];
62
+ }
63
+ addClient(client) {
64
+ if (client.uid in this.clients)
65
+ return;
66
+ this.clients[client.uid] = client;
67
+ this.orders[client.uid] = 0;
68
+ this.store(async (store) => {
69
+ const instances = await store.get('instances');
70
+ if (!instances[this.uid])
71
+ instances[this.uid] = { clients: [], rooms: [] };
72
+ instances[this.uid].clients = [...(instances[this.uid].clients ?? []), client.uid];
73
+ await store.set('instances', instances);
74
+ }).catch((err) => {
75
+ useLogger().error(err, `[Collab] Failed to add client ${client.uid} to registry`);
76
+ });
77
+ client.on('close', () => {
78
+ this.removeClient(client.uid);
79
+ });
80
+ }
81
+ removeClient(uid) {
82
+ delete this.clients[uid];
83
+ delete this.orders[uid];
84
+ this.store(async (store) => {
85
+ const instances = await store.get('instances');
86
+ if (instances[this.uid]) {
87
+ instances[this.uid].clients = (instances[this.uid].clients ?? []).filter((clientId) => clientId !== uid);
88
+ await store.set('instances', instances);
89
+ }
90
+ }).catch((err) => {
91
+ useLogger().error(err, `[Collab] Failed to remove client ${uid} from registry`);
92
+ });
93
+ }
94
+ async registerRoom(uid) {
95
+ await this.store(async (store) => {
96
+ const instances = await store.get('instances');
97
+ if (!instances[this.uid])
98
+ instances[this.uid] = { clients: [], rooms: [] };
99
+ if (!instances[this.uid].rooms.includes(uid)) {
100
+ instances[this.uid].rooms.push(uid);
101
+ await store.set('instances', instances);
102
+ }
103
+ });
104
+ }
105
+ async unregisterRoom(uid) {
106
+ await this.store(async (store) => {
107
+ const instances = await store.get('instances');
108
+ if (instances[this.uid]) {
109
+ instances[this.uid].rooms = (instances[this.uid].rooms ?? []).filter((roomUid) => roomUid !== uid);
110
+ await store.set('instances', instances);
111
+ }
112
+ });
113
+ }
114
+ async getLocalClients() {
115
+ return Object.keys(this.clients);
116
+ }
117
+ async getGlobalClients() {
118
+ const instances = await this.store(async (store) => await store.get('instances'));
119
+ return Object.values(instances)
120
+ .map((instance) => instance.clients)
121
+ .flat();
122
+ }
123
+ async pruneDeadInstances() {
124
+ const instances = await this.store(async (store) => await store.get('instances'));
125
+ const inactiveInstances = new Set(Object.keys(instances));
126
+ inactiveInstances.delete(this.uid);
127
+ const pongCollector = (message) => {
128
+ if (message.type === 'pong') {
129
+ inactiveInstances.delete(message.instance);
130
+ }
131
+ };
132
+ this.messenger.subscribe(COLLAB_BUS, pongCollector);
133
+ for (const instance of inactiveInstances) {
134
+ this.messenger.publish(COLLAB_BUS, { type: 'ping', instance });
135
+ }
136
+ await new Promise((resolve) => {
137
+ setTimeout(resolve, INSTANCE_TIMEOUT);
138
+ });
139
+ this.messenger.unsubscribe(COLLAB_BUS, pongCollector);
140
+ const dead = { clients: [], rooms: [] };
141
+ if (inactiveInstances.size === 0) {
142
+ return {
143
+ inactive: dead,
144
+ active: Object.values(instances)
145
+ .map((instance) => instance.clients)
146
+ .flat(),
147
+ };
148
+ }
149
+ // Reread state to avoid overwriting updates during the timeout phase
150
+ const current = await this.store(async (store) => {
151
+ const current = await store.get('instances');
152
+ let changed = false;
153
+ for (const deadId of inactiveInstances) {
154
+ if (current[deadId]) {
155
+ dead.clients.push(...(current[deadId].clients ?? []));
156
+ dead.rooms.push(...(current[deadId].rooms ?? []));
157
+ delete current[deadId];
158
+ changed = true;
159
+ }
160
+ }
161
+ if (changed) {
162
+ await store.set('instances', current);
163
+ }
164
+ return current;
165
+ });
166
+ return {
167
+ inactive: dead,
168
+ active: Object.values(current)
169
+ .map((instance) => instance.clients)
170
+ .flat(),
171
+ };
172
+ }
173
+ sendRoom(room, message) {
174
+ this.messenger.publish(COLLAB_BUS, { type: 'room', room, ...message });
175
+ }
176
+ sendClient(client, message) {
177
+ const localClient = this.clients[client];
178
+ if (localClient) {
179
+ const order = this.orders[client] ?? 0;
180
+ this.orders[client] = order + 1;
181
+ localClient.send(JSON.stringify({ ...message, order }));
182
+ }
183
+ else {
184
+ this.messenger.publish(COLLAB_BUS, { type: 'send', client, message });
185
+ }
186
+ }
187
+ terminateClient(client) {
188
+ const localClient = this.clients[client];
189
+ if (localClient) {
190
+ // Allow message to flush before closing
191
+ setTimeout(() => {
192
+ localClient.close();
193
+ }, 250);
194
+ }
195
+ else {
196
+ this.messenger.publish(COLLAB_BUS, { type: 'terminate', client });
197
+ }
198
+ }
199
+ sendError(client, error) {
200
+ const localClient = this.clients[client];
201
+ if (localClient) {
202
+ localClient.send(JSON.stringify(error));
203
+ }
204
+ else {
205
+ this.messenger.publish(COLLAB_BUS, { type: 'error', client, message: error });
206
+ }
207
+ }
208
+ handleError(client, error, action) {
209
+ let message;
210
+ if (isDirectusError(error)) {
211
+ message = {
212
+ action: 'error',
213
+ type: WS_TYPE.COLLAB,
214
+ code: error.code,
215
+ trigger: action,
216
+ message: error.message,
217
+ };
218
+ }
219
+ else {
220
+ useLogger().error(`WebSocket unhandled exception ${JSON.stringify({ type: WS_TYPE.COLLAB, error })}`);
221
+ return;
222
+ }
223
+ this.sendError(client, message);
224
+ }
225
+ }
@@ -0,0 +1,18 @@
1
+ import type { Accountability, PrimaryKey, SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ type PermissionContext = {
4
+ knex: Knex;
5
+ schema: SchemaOverview;
6
+ accountability: Accountability | null;
7
+ itemId?: PrimaryKey | null;
8
+ direction?: 'inbound' | 'outbound';
9
+ };
10
+ /**
11
+ * Validates a changes payload against the user's update/create permissions and errors if unauthorized field is encountered
12
+ */
13
+ export declare function validateChanges(payload: any, collection: string, itemId: PrimaryKey | null, context: PermissionContext): Promise<any>;
14
+ /**
15
+ * Sanitizes a payload based on the recipient's read permissions and the schema
16
+ */
17
+ export declare function sanitizePayload(payload: any, collection: string, context: PermissionContext): Promise<any>;
18
+ export {};
@@ -0,0 +1,158 @@
1
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
+ import { deepMapWithSchema, isDetailedUpdateSyntax } from '@directus/utils';
3
+ import { verifyPermissions } from './verify-permissions.js';
4
+ /**
5
+ * Validates a changes payload against the user's update/create permissions and errors if unauthorized field is encountered
6
+ */
7
+ export async function validateChanges(payload, collection, itemId, context) {
8
+ return processPermissions(payload, collection, { ...context, itemId, direction: 'inbound' });
9
+ }
10
+ /**
11
+ * Sanitizes a payload based on the recipient's read permissions and the schema
12
+ */
13
+ export async function sanitizePayload(payload, collection, context) {
14
+ return processPermissions(payload, collection, { ...context, direction: 'outbound' });
15
+ }
16
+ /**
17
+ * Core utility to walk a payload and apply permissions
18
+ */
19
+ async function processPermissions(payload, collection, context) {
20
+ const { direction, accountability, schema, knex, itemId } = context;
21
+ // Local cache for permissions to avoid redundant verifyPermissions calls for the same item:action pair
22
+ // The promise is cached, so concurrent field lookups for the same item wait for the same result
23
+ const permissionsCache = new Map();
24
+ const getPermissions = (col, id, action) => {
25
+ const cacheKey = `${col}:${id}:${action}`;
26
+ let cached = permissionsCache.get(cacheKey);
27
+ if (!cached) {
28
+ cached = verifyPermissions(accountability, col, id, action, { knex, schema });
29
+ permissionsCache.set(cacheKey, cached);
30
+ }
31
+ return cached;
32
+ };
33
+ return deepMapWithSchema(payload, async (entry, deepMapContext) => {
34
+ const [key, value] = entry;
35
+ if (direction === 'outbound') {
36
+ // Strip sensitive fields
37
+ if (deepMapContext.field?.special?.some((v) => v === 'conceal' || v === 'hash' || v === 'encrypt')) {
38
+ return undefined;
39
+ }
40
+ // Strip unknown leaf fields
41
+ if (deepMapContext.leaf && !deepMapContext.relation && !deepMapContext.field) {
42
+ return undefined;
43
+ }
44
+ }
45
+ if (value === undefined)
46
+ return undefined;
47
+ // Resolve the action (CRUD) and the ID to check against
48
+ const currentCollection = deepMapContext.collection.collection;
49
+ const pkField = deepMapContext.collection.primary;
50
+ const primaryKeyInObject = (deepMapContext.object[pkField] ?? null);
51
+ let action = direction === 'inbound' ? 'update' : 'read';
52
+ let effectiveItemId = primaryKeyInObject;
53
+ if (direction === 'inbound') {
54
+ const isTopLevel = deepMapContext.object === payload;
55
+ // At the top level, we use the ID from the request context (itemId)
56
+ // Deeply nested objects must provide their own ID for update checks
57
+ if (isTopLevel) {
58
+ effectiveItemId = itemId ?? null;
59
+ action = itemId ? 'update' : 'create';
60
+ }
61
+ else if (!primaryKeyInObject) {
62
+ action = 'create';
63
+ }
64
+ if (deepMapContext.action) {
65
+ action = deepMapContext.action;
66
+ }
67
+ }
68
+ else {
69
+ // sanitizePayload uses context.itemId as a fallback for the root item
70
+ if (deepMapContext.object === payload) {
71
+ effectiveItemId = primaryKeyInObject ?? itemId ?? null;
72
+ }
73
+ }
74
+ // Ensure no unexpected fields sneak into a delete operation
75
+ if (direction === 'inbound' && action === 'delete') {
76
+ if (key !== pkField) {
77
+ throw new InvalidPayloadError({ reason: `Unexpected field ${key} in delete payload` });
78
+ }
79
+ const allowed = await getPermissions(currentCollection, primaryKeyInObject, 'delete');
80
+ if (allowed === null || (allowed.length === 0 && !accountability?.admin)) {
81
+ throw new ForbiddenError({ reason: `No permission to delete item in collection ${currentCollection}` });
82
+ }
83
+ return;
84
+ }
85
+ // Allow PK field for identification on updates
86
+ if (direction === 'inbound' && action === 'update' && key === pkField) {
87
+ return;
88
+ }
89
+ let allowedFields = await getPermissions(currentCollection, effectiveItemId, action);
90
+ // Fallbacks
91
+ if (!allowedFields) {
92
+ if (direction === 'inbound' && action === 'update') {
93
+ // Toggle to create if update fails due to non-existence
94
+ action = 'create';
95
+ allowedFields = await getPermissions(currentCollection, effectiveItemId, action);
96
+ }
97
+ else if (direction === 'outbound') {
98
+ // Fall back to collection-wide read
99
+ allowedFields = (await getPermissions(currentCollection, null, 'read')) ?? [];
100
+ }
101
+ }
102
+ const isAllowed = allowedFields && (accountability?.admin || allowedFields.includes('*') || allowedFields.includes(String(key)));
103
+ if (!isAllowed) {
104
+ if (direction === 'inbound') {
105
+ throw new ForbiddenError({ reason: `No permission to ${action} field ${key} or field does not exist` });
106
+ }
107
+ return undefined;
108
+ }
109
+ // Remove the relation field entirely from the payload if it's empty after sanitizing its children
110
+ if (direction === 'outbound' && deepMapContext.relationType) {
111
+ if (Array.isArray(value)) {
112
+ const items = value.filter(isVisible);
113
+ if (items.length === 0)
114
+ return undefined;
115
+ return [key, items];
116
+ }
117
+ else if (isDetailedUpdateSyntax(value)) {
118
+ const filtered = {
119
+ ...value,
120
+ create: value.create.filter(isVisible),
121
+ update: value.update.filter(isVisible),
122
+ delete: value.delete.filter(isVisible),
123
+ };
124
+ if (filtered.create.length === 0 && filtered.update.length === 0 && filtered.delete.length === 0) {
125
+ return undefined;
126
+ }
127
+ return [key, filtered];
128
+ }
129
+ else if (!isVisible(value)) {
130
+ return undefined;
131
+ }
132
+ }
133
+ return [key, value];
134
+ }, {
135
+ schema,
136
+ collection,
137
+ }, {
138
+ detailedUpdateSyntax: true,
139
+ omitUnknownFields: direction === 'outbound',
140
+ mapPrimaryKeys: true,
141
+ processAsync: true,
142
+ iterateOnly: direction === 'inbound', // Validation only needs to check permissions, not rebuild the payload
143
+ onUnknownField: (entry) => {
144
+ const [key] = entry;
145
+ // Allow Directus internal metadata keys like $type
146
+ if (String(key).startsWith('$'))
147
+ return entry;
148
+ if (direction === 'inbound') {
149
+ throw new ForbiddenError({ reason: `No permission to update field ${key} or field does not exist` });
150
+ }
151
+ return undefined;
152
+ },
153
+ });
154
+ }
155
+ // Identifies non-empty or defined actionable content to avoid processing invalid relation links
156
+ function isVisible(item) {
157
+ return item !== undefined && !(typeof item === 'object' && item !== null && Object.keys(item).length === 0);
158
+ }
@@ -0,0 +1,52 @@
1
+ import type { Accountability } from '@directus/types';
2
+ /**
3
+ * Caches permission check results for collaborative editing clients.
4
+ * Supports granular invalidation based on collection, item, and relational dependencies.
5
+ */
6
+ export declare class PermissionCache {
7
+ private cache;
8
+ private tags;
9
+ private keyTags;
10
+ private timers;
11
+ private bus;
12
+ private invalidationCount;
13
+ constructor(maxSize: number);
14
+ /**
15
+ * Used for race condition protection during async permission fetches.
16
+ */
17
+ getInvalidationCount(): number;
18
+ /**
19
+ * Clears entire cache for system collections, or performs granular invalidation for user data.
20
+ */
21
+ private handleInvalidation;
22
+ /**
23
+ * Get cached allowed fields for a given accountability and collection/item.
24
+ * LRUMap automatically updates access order on get().
25
+ */
26
+ get(accountability: Accountability, collection: string, item: string | null, action: string): string[] | null | undefined;
27
+ /**
28
+ * Store allowed fields in the cache with optional TTL and dependencies.
29
+ */
30
+ set(accountability: Accountability, collection: string, item: string | null, action: string, fields: string[] | null, dependencies?: string[], ttlMs?: number): void;
31
+ /**
32
+ * Called before LRU eviction or explicit invalidation to prevent orphaned metadata.
33
+ */
34
+ private cleanupKeyMetadata;
35
+ /**
36
+ * Maintains bidirectional mappings: tag → keys and key → tags.
37
+ */
38
+ private addTag;
39
+ /**
40
+ * Cleans up metadata first, then removes from cache.
41
+ */
42
+ private invalidateKey;
43
+ /**
44
+ * Cache key format: user:collection:item:action
45
+ */
46
+ private getCacheKey;
47
+ /**
48
+ * Clear the entire cache.
49
+ */
50
+ clear(): void;
51
+ }
52
+ export declare const permissionCache: PermissionCache;