@directus/api 32.1.0 → 32.2.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 (175) hide show
  1. package/dist/ai/chat/constants/system-prompt.d.ts +1 -0
  2. package/dist/ai/chat/constants/system-prompt.js +51 -0
  3. package/dist/ai/chat/controllers/chat.post.d.ts +2 -0
  4. package/dist/ai/chat/controllers/chat.post.js +47 -0
  5. package/dist/ai/chat/lib/create-ui-stream.d.ts +15 -0
  6. package/dist/ai/chat/lib/create-ui-stream.js +42 -0
  7. package/dist/ai/chat/middleware/load-settings.d.ts +2 -0
  8. package/dist/ai/chat/middleware/load-settings.js +18 -0
  9. package/dist/ai/chat/models/chat-request.d.ts +34 -0
  10. package/dist/ai/chat/models/chat-request.js +26 -0
  11. package/dist/ai/chat/models/providers.d.ts +9 -0
  12. package/dist/ai/chat/models/providers.js +9 -0
  13. package/dist/ai/chat/router.d.ts +1 -0
  14. package/dist/ai/chat/router.js +5 -0
  15. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.d.ts +9 -0
  16. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +38 -0
  17. package/dist/ai/chat/utils/fix-error-tool-calls.d.ts +12 -0
  18. package/dist/ai/chat/utils/fix-error-tool-calls.js +30 -0
  19. package/dist/ai/chat/utils/parse-json-schema-7.d.ts +13 -0
  20. package/dist/ai/chat/utils/parse-json-schema-7.js +75 -0
  21. package/dist/{mcp → ai/mcp}/server.d.ts +13 -16
  22. package/dist/{mcp → ai/mcp}/server.js +4 -13
  23. package/dist/ai/mcp/types.d.ts +15 -0
  24. package/dist/{mcp/tools/assets.js → ai/tools/assets/index.js} +8 -5
  25. package/dist/{mcp/tools/collections.js → ai/tools/collections/index.js} +7 -4
  26. package/dist/{mcp/tools/fields.js → ai/tools/fields/index.js} +12 -9
  27. package/dist/{mcp/tools/files.js → ai/tools/files/index.js} +11 -5
  28. package/dist/{mcp/tools/flows.js → ai/tools/flows/index.js} +11 -5
  29. package/dist/{mcp/tools/folders.js → ai/tools/folders/index.js} +12 -5
  30. package/dist/ai/tools/index.d.ts +15 -0
  31. package/dist/ai/tools/index.js +29 -0
  32. package/dist/{mcp/tools/items.js → ai/tools/items/index.js} +13 -6
  33. package/dist/{mcp/tools/prompts/items.md → ai/tools/items/prompt.md} +19 -15
  34. package/dist/{mcp/tools/operations.d.ts → ai/tools/operations/index.d.ts} +46 -0
  35. package/dist/{mcp/tools/operations.js → ai/tools/operations/index.js} +12 -5
  36. package/dist/{mcp/tools/relations.js → ai/tools/relations/index.js} +7 -4
  37. package/dist/{mcp/tools/schema.d.ts → ai/tools/schema/index.d.ts} +1 -1
  38. package/dist/{mcp/tools/schema.js → ai/tools/schema/index.js} +9 -6
  39. package/dist/{mcp/tools/system.js → ai/tools/system/index.js} +7 -4
  40. package/dist/{mcp/tools/trigger-flow.js → ai/tools/trigger-flow/index.js} +8 -5
  41. package/dist/{mcp → ai/tools}/types.d.ts +1 -17
  42. package/dist/ai/tools/utils.d.ts +9 -0
  43. package/dist/ai/tools/utils.js +17 -0
  44. package/dist/app.js +5 -0
  45. package/dist/auth/drivers/oauth2.d.ts +2 -1
  46. package/dist/auth/drivers/oauth2.js +17 -22
  47. package/dist/auth/drivers/openid.d.ts +2 -1
  48. package/dist/auth/drivers/openid.js +13 -18
  49. package/dist/auth/drivers/saml.js +6 -3
  50. package/dist/controllers/assets.js +39 -2
  51. package/dist/controllers/mcp.js +1 -1
  52. package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
  53. package/dist/database/migrations/20251103A-add-ai-settings.d.ts +3 -0
  54. package/dist/database/migrations/20251103A-add-ai-settings.js +14 -0
  55. package/dist/database/run-ast/run-ast.js +1 -1
  56. package/dist/extensions/lib/installation/manager.js +5 -9
  57. package/dist/extensions/lib/sync/status.d.ts +11 -0
  58. package/dist/extensions/lib/sync/status.js +34 -0
  59. package/dist/extensions/lib/sync/sync.d.ts +6 -0
  60. package/dist/extensions/lib/sync/sync.js +90 -0
  61. package/dist/extensions/lib/sync/tracker.d.ts +18 -0
  62. package/dist/extensions/lib/sync/tracker.js +71 -0
  63. package/dist/extensions/lib/sync/utils.d.ts +24 -0
  64. package/dist/extensions/lib/sync/utils.js +62 -0
  65. package/dist/extensions/manager.d.ts +8 -4
  66. package/dist/extensions/manager.js +30 -13
  67. package/dist/middleware/respond.js +2 -2
  68. package/dist/permissions/lib/fetch-policies.d.ts +1 -1
  69. package/dist/permissions/lib/fetch-roles-tree.d.ts +6 -3
  70. package/dist/permissions/lib/fetch-roles-tree.js +5 -27
  71. package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +9 -7
  72. package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +17 -9
  73. package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +1 -1
  74. package/dist/permissions/utils/fetch-raw-permissions.d.ts +1 -1
  75. package/dist/permissions/utils/fetch-share-info.d.ts +1 -1
  76. package/dist/permissions/utils/fetch-share-info.js +1 -1
  77. package/dist/permissions/utils/filter-policies-by-ip.js +1 -1
  78. package/dist/permissions/utils/get-permissions-for-share.js +8 -8
  79. package/dist/permissions/utils/with-cache.d.ts +8 -6
  80. package/dist/permissions/utils/with-cache.js +12 -10
  81. package/dist/request/is-denied-ip.js +2 -2
  82. package/dist/services/assets/name-deduper.d.ts +7 -0
  83. package/dist/services/assets/name-deduper.js +23 -0
  84. package/dist/services/assets.d.ts +15 -2
  85. package/dist/services/assets.js +98 -5
  86. package/dist/services/authentication.js +4 -4
  87. package/dist/services/comments.js +2 -2
  88. package/dist/services/extensions.js +4 -0
  89. package/dist/services/folders.d.ts +27 -2
  90. package/dist/services/folders.js +75 -0
  91. package/dist/services/graphql/resolvers/query.js +1 -1
  92. package/dist/services/import-export.d.ts +1 -1
  93. package/dist/services/import-export.js +4 -5
  94. package/dist/services/notifications.js +2 -2
  95. package/dist/services/payload.js +20 -0
  96. package/dist/services/roles.js +2 -2
  97. package/dist/services/tus/server.js +3 -3
  98. package/dist/telemetry/utils/get-settings.d.ts +15 -0
  99. package/dist/telemetry/utils/get-settings.js +25 -9
  100. package/dist/test-utils/README.md +95 -24
  101. package/dist/test-utils/cache.d.ts +2 -2
  102. package/dist/test-utils/cache.js +2 -2
  103. package/dist/test-utils/{fields-service.d.ts → services/fields-service.d.ts} +1 -1
  104. package/dist/test-utils/{fields-service.js → services/fields-service.js} +3 -2
  105. package/dist/test-utils/services/files-service.d.ts +28 -0
  106. package/dist/test-utils/services/files-service.js +34 -0
  107. package/dist/test-utils/services/folders-service.d.ts +28 -0
  108. package/dist/test-utils/services/folders-service.js +33 -0
  109. package/dist/utils/encrypt.d.ts +2 -0
  110. package/dist/utils/encrypt.js +64 -0
  111. package/dist/utils/get-accountability-for-role.js +2 -2
  112. package/dist/utils/get-accountability-for-token.js +4 -4
  113. package/dist/utils/get-cache-key.js +2 -2
  114. package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
  115. package/dist/{auth/utils → utils}/is-login-redirect-allowed.js +8 -16
  116. package/dist/utils/require-text.d.ts +1 -0
  117. package/dist/utils/require-text.js +4 -0
  118. package/dist/utils/require-yaml.js +2 -2
  119. package/package.json +31 -25
  120. package/dist/auth/utils/generate-callback-url.d.ts +0 -8
  121. package/dist/auth/utils/generate-callback-url.js +0 -11
  122. package/dist/auth/utils/is-login-redirect-allowed.d.ts +0 -8
  123. package/dist/extensions/lib/sync-extensions.d.ts +0 -3
  124. package/dist/extensions/lib/sync-extensions.js +0 -70
  125. package/dist/extensions/lib/sync-status.d.ts +0 -10
  126. package/dist/extensions/lib/sync-status.js +0 -27
  127. package/dist/mcp/tools/index.d.ts +0 -15
  128. package/dist/mcp/tools/index.js +0 -29
  129. package/dist/mcp/tools/prompts/index.d.ts +0 -16
  130. package/dist/mcp/tools/prompts/index.js +0 -19
  131. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +0 -5
  132. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +0 -7
  133. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +0 -5
  134. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +0 -10
  135. package/dist/permissions/modules/fetch-global-access/types.d.ts +0 -4
  136. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +0 -4
  137. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +0 -27
  138. package/dist/utils/get-date-formatted.d.ts +0 -1
  139. package/dist/utils/get-date-formatted.js +0 -10
  140. package/dist/utils/ip-in-networks.d.ts +0 -6
  141. package/dist/utils/ip-in-networks.js +0 -13
  142. /package/dist/{mcp → ai/mcp}/index.d.ts +0 -0
  143. /package/dist/{mcp → ai/mcp}/index.js +0 -0
  144. /package/dist/{mcp → ai/mcp}/transport.d.ts +0 -0
  145. /package/dist/{mcp → ai/mcp}/transport.js +0 -0
  146. /package/dist/{mcp → ai/mcp}/types.js +0 -0
  147. /package/dist/{mcp/tools/assets.d.ts → ai/tools/assets/index.d.ts} +0 -0
  148. /package/dist/{mcp/tools/prompts/assets.md → ai/tools/assets/prompt.md} +0 -0
  149. /package/dist/{mcp/tools/collections.d.ts → ai/tools/collections/index.d.ts} +0 -0
  150. /package/dist/{mcp/tools/prompts/collections.md → ai/tools/collections/prompt.md} +0 -0
  151. /package/dist/{mcp/define.d.ts → ai/tools/define-tool.d.ts} +0 -0
  152. /package/dist/{mcp/define.js → ai/tools/define-tool.js} +0 -0
  153. /package/dist/{mcp/tools/fields.d.ts → ai/tools/fields/index.d.ts} +0 -0
  154. /package/dist/{mcp/tools/prompts/fields.md → ai/tools/fields/prompt.md} +0 -0
  155. /package/dist/{mcp/tools/files.d.ts → ai/tools/files/index.d.ts} +0 -0
  156. /package/dist/{mcp/tools/prompts/files.md → ai/tools/files/prompt.md} +0 -0
  157. /package/dist/{mcp/tools/flows.d.ts → ai/tools/flows/index.d.ts} +0 -0
  158. /package/dist/{mcp/tools/prompts/flows.md → ai/tools/flows/prompt.md} +0 -0
  159. /package/dist/{mcp/tools/folders.d.ts → ai/tools/folders/index.d.ts} +0 -0
  160. /package/dist/{mcp/tools/prompts/folders.md → ai/tools/folders/prompt.md} +0 -0
  161. /package/dist/{mcp/tools/items.d.ts → ai/tools/items/index.d.ts} +0 -0
  162. /package/dist/{mcp/tools/prompts/operations.md → ai/tools/operations/prompt.md} +0 -0
  163. /package/dist/{mcp/tools/relations.d.ts → ai/tools/relations/index.d.ts} +0 -0
  164. /package/dist/{mcp/tools/prompts/relations.md → ai/tools/relations/prompt.md} +0 -0
  165. /package/dist/{mcp/tools/prompts/schema.md → ai/tools/schema/prompt.md} +0 -0
  166. /package/dist/{mcp → ai/tools}/schema.d.ts +0 -0
  167. /package/dist/{mcp → ai/tools}/schema.js +0 -0
  168. /package/dist/{mcp/tools/system.d.ts → ai/tools/system/index.d.ts} +0 -0
  169. /package/dist/{mcp/tools/prompts/system-prompt-description.md → ai/tools/system/prompt-description.md} +0 -0
  170. /package/dist/{mcp/tools/prompts/system-prompt.md → ai/tools/system/prompt.md} +0 -0
  171. /package/dist/{mcp/tools/trigger-flow.d.ts → ai/tools/trigger-flow/index.d.ts} +0 -0
  172. /package/dist/{mcp/tools/prompts/trigger-flow.md → ai/tools/trigger-flow/prompt.md} +0 -0
  173. /package/dist/{permissions/modules/fetch-global-access → ai/tools}/types.js +0 -0
  174. /package/dist/test-utils/{items-service.d.ts → services/items-service.d.ts} +0 -0
  175. /package/dist/test-utils/{items-service.js → services/items-service.js} +0 -0
@@ -1,4 +1,4 @@
1
- import type { Accountability, Query, SchemaOverview } from '@directus/types';
1
+ import type { Accountability, SchemaOverview } from '@directus/types';
2
2
  import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
3
3
  import type { ZodType } from 'zod';
4
4
  export type ToolResultBase = {
@@ -18,7 +18,6 @@ export type ToolResult = TextToolResult | AssetToolResult;
18
18
  export type ToolHandler<T> = {
19
19
  (options: {
20
20
  args: T;
21
- sanitizedQuery: Query;
22
21
  schema: SchemaOverview;
23
22
  accountability: Accountability | undefined;
24
23
  }): Promise<ToolResult | undefined>;
@@ -39,18 +38,3 @@ export interface ToolConfig<T> {
39
38
  annotations?: ToolAnnotations;
40
39
  handler: ToolHandler<T>;
41
40
  }
42
- export interface Prompt {
43
- name: string;
44
- system_prompt?: string | null;
45
- description?: string;
46
- messages: {
47
- role: 'user' | 'assistant';
48
- text: string;
49
- }[];
50
- }
51
- export interface MCPOptions {
52
- promptsCollection?: string;
53
- allowDeletes?: boolean;
54
- systemPromptEnabled?: boolean;
55
- systemPrompt?: string | null;
56
- }
@@ -0,0 +1,9 @@
1
+ import type { Accountability, SchemaOverview } from '@directus/types';
2
+ /**
3
+ * Build a sanitized query object from a tool's args payload.
4
+ * - Ensures fields defaults to '*' when not provided
5
+ * - Returns an empty object when no args.query is present
6
+ */
7
+ export declare function buildSanitizedQueryFromArgs<T extends {
8
+ query?: Record<string, any> | undefined;
9
+ }>(args: T, schema: SchemaOverview, accountability?: Accountability | null): Promise<Record<string, any>>;
@@ -0,0 +1,17 @@
1
+ import { sanitizeQuery } from '../../utils/sanitize-query.js';
2
+ /**
3
+ * Build a sanitized query object from a tool's args payload.
4
+ * - Ensures fields defaults to '*' when not provided
5
+ * - Returns an empty object when no args.query is present
6
+ */
7
+ export async function buildSanitizedQueryFromArgs(args, schema, accountability) {
8
+ let sanitizedQuery = {};
9
+ if (args?.query) {
10
+ const q = args.query;
11
+ sanitizedQuery = await sanitizeQuery({
12
+ fields: q['fields'] ?? '*',
13
+ ...q,
14
+ }, schema, accountability ?? undefined);
15
+ }
16
+ return sanitizedQuery;
17
+ }
package/dist/app.js CHANGED
@@ -68,6 +68,7 @@ import projectSchedule from './schedules/project.js';
68
68
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
69
69
  import { Url } from './utils/url.js';
70
70
  import { validateStorage } from './utils/validate-storage.js';
71
+ import { aiChatRouter } from './ai/chat/router.js';
71
72
  const require = createRequire(import.meta.url);
72
73
  export default async function createApp() {
73
74
  const env = useEnv();
@@ -84,6 +85,9 @@ export default async function createApp() {
84
85
  if (!env['SECRET']) {
85
86
  logger.warn(`"SECRET" env variable is missing. Using a random value instead. Tokens will not persist between restarts. This is not appropriate for production usage.`);
86
87
  }
88
+ if (typeof env['SECRET'] === 'string' && Buffer.byteLength(env['SECRET']) < 32) {
89
+ logger.warn('"SECRET" env variable is shorter than 32 bytes which is insecure. This is not appropriate for production usage.');
90
+ }
87
91
  if (!new Url(env['PUBLIC_URL']).isAbsolute()) {
88
92
  logger.warn('"PUBLIC_URL" should be a full URL');
89
93
  }
@@ -231,6 +235,7 @@ export default async function createApp() {
231
235
  if (toBoolean(env['MCP_ENABLED']) === true) {
232
236
  app.use('/mcp', mcpRouter);
233
237
  }
238
+ app.use('/ai/chat', aiChatRouter);
234
239
  if (env['METRICS_ENABLED'] === true) {
235
240
  app.use('/metrics', metricsRouter);
236
241
  }
@@ -5,11 +5,12 @@ import type { RoleMap } from '../../types/rolemap.js';
5
5
  import { LocalAuthDriver } from './local.js';
6
6
  export declare class OAuth2AuthDriver extends LocalAuthDriver {
7
7
  client: Client;
8
+ redirectUrl: string;
8
9
  config: Record<string, any>;
9
10
  roleMap: RoleMap;
10
11
  constructor(options: AuthDriverOptions, config: Record<string, any>);
11
12
  generateCodeVerifier(): string;
12
- generateAuthUrl(codeVerifier: string, prompt?: boolean, callbackUrl?: string): string;
13
+ generateAuthUrl(codeVerifier: string, prompt?: boolean): string;
13
14
  private fetchUserId;
14
15
  getUserID(payload: Record<string, any>): Promise<string>;
15
16
  login(user: User): Promise<void>;
@@ -16,37 +16,40 @@ import { AuthenticationService } from '../../services/authentication.js';
16
16
  import asyncHandler from '../../utils/async-handler.js';
17
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
18
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
19
- import { getSchema } from '../../utils/get-schema.js';
20
19
  import { getSecret } from '../../utils/get-secret.js';
20
+ import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
21
21
  import { verifyJWT } from '../../utils/jwt.js';
22
22
  import { Url } from '../../utils/url.js';
23
- import { generateCallbackUrl } from '../utils/generate-callback-url.js';
24
- import { isLoginRedirectAllowed } from '../utils/is-login-redirect-allowed.js';
25
23
  import { LocalAuthDriver } from './local.js';
24
+ import { getSchema } from '../../utils/get-schema.js';
26
25
  export class OAuth2AuthDriver extends LocalAuthDriver {
27
26
  client;
27
+ redirectUrl;
28
28
  config;
29
29
  roleMap;
30
30
  constructor(options, config) {
31
31
  super(options, config);
32
+ const env = useEnv();
32
33
  const logger = useLogger();
33
34
  const { authorizeUrl, accessUrl, profileUrl, clientId, clientSecret, ...additionalConfig } = config;
34
35
  if (!authorizeUrl || !accessUrl || !profileUrl || !clientId || !clientSecret || !additionalConfig['provider']) {
35
36
  logger.error('Invalid provider config');
36
37
  throw new InvalidProviderConfigError({ provider: additionalConfig['provider'] });
37
38
  }
39
+ const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', additionalConfig['provider'], 'callback');
40
+ this.redirectUrl = redirectUrl.toString();
38
41
  this.config = additionalConfig;
39
42
  this.roleMap = {};
40
43
  const roleMapping = this.config['roleMapping'];
44
+ if (roleMapping) {
45
+ this.roleMap = roleMapping;
46
+ }
41
47
  // role mapping will fail on login if AUTH_<provider>_ROLE_MAPPING is an array instead of an object.
42
48
  // This happens if the 'json:' prefix is missing from the variable declaration. To save the user from exhaustive debugging, we'll try to fail early here.
43
49
  if (roleMapping instanceof Array) {
44
50
  logger.error("[OAuth2] Expected a JSON-Object as role mapping, got an Array instead. Make sure you declare the variable with 'json:' prefix.");
45
51
  throw new InvalidProviderError();
46
52
  }
47
- if (roleMapping) {
48
- this.roleMap = roleMapping;
49
- }
50
53
  const issuer = new Issuer({
51
54
  authorization_endpoint: authorizeUrl,
52
55
  token_endpoint: accessUrl,
@@ -64,6 +67,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
64
67
  this.client = new issuer.Client({
65
68
  client_id: clientId,
66
69
  client_secret: clientSecret,
70
+ redirect_uris: [this.redirectUrl],
67
71
  response_types: ['code'],
68
72
  ...clientOptionsOverrides,
69
73
  });
@@ -71,7 +75,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
71
75
  generateCodeVerifier() {
72
76
  return generators.codeVerifier();
73
77
  }
74
- generateAuthUrl(codeVerifier, prompt = false, callbackUrl) {
78
+ generateAuthUrl(codeVerifier, prompt = false) {
75
79
  const { plainCodeChallenge } = this.config;
76
80
  try {
77
81
  const codeChallenge = plainCodeChallenge ? codeVerifier : generators.codeChallenge(codeVerifier);
@@ -85,7 +89,6 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
85
89
  code_challenge_method: plainCodeChallenge ? 'plain' : 'S256',
86
90
  // Some providers require state even with PKCE
87
91
  state: codeChallenge,
88
- redirect_uri: callbackUrl,
89
92
  });
90
93
  }
91
94
  catch (e) {
@@ -113,7 +116,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
113
116
  const codeChallenge = plainCodeChallenge
114
117
  ? payload['codeVerifier']
115
118
  : generators.codeChallenge(payload['codeVerifier']);
116
- tokenSet = await this.client.oauthCallback(payload['redirectUri'], { code: payload['code'], state: payload['state'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge });
119
+ tokenSet = await this.client.oauthCallback(this.redirectUrl, { code: payload['code'], state: payload['state'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge });
117
120
  userInfo = await this.client.userinfo(tokenSet.access_token);
118
121
  }
119
122
  catch (e) {
@@ -272,19 +275,12 @@ export function createOAuth2AuthRouter(providerName) {
272
275
  const provider = getAuthProvider(providerName);
273
276
  const codeVerifier = provider.generateCodeVerifier();
274
277
  const prompt = !!req.query['prompt'];
275
- const otp = req.query['otp'];
276
278
  const redirect = req.query['redirect'];
277
- if (!isLoginRedirectAllowed(providerName, `${req.protocol}://${req.hostname}`, redirect)) {
279
+ const otp = req.query['otp'];
280
+ if (isLoginRedirectAllowed(redirect, providerName) === false) {
278
281
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
279
282
  }
280
- const callbackUrl = generateCallbackUrl(providerName, `${req.protocol}://${req.get('host')}`);
281
- const token = jwt.sign({
282
- verifier: codeVerifier,
283
- redirect,
284
- prompt,
285
- otp,
286
- callbackUrl,
287
- }, getSecret(), {
283
+ const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
288
284
  expiresIn: '5m',
289
285
  issuer: 'directus',
290
286
  });
@@ -292,7 +288,7 @@ export function createOAuth2AuthRouter(providerName) {
292
288
  httpOnly: true,
293
289
  sameSite: 'lax',
294
290
  });
295
- return res.redirect(provider.generateAuthUrl(codeVerifier, prompt, callbackUrl));
291
+ return res.redirect(provider.generateAuthUrl(codeVerifier, prompt));
296
292
  }, respond);
297
293
  router.post('/callback', express.urlencoded({ extended: false }), (req, res) => {
298
294
  res.redirect(303, `./callback?${new URLSearchParams(req.body)}`);
@@ -307,7 +303,7 @@ export function createOAuth2AuthRouter(providerName) {
307
303
  logger.warn(e, `[OAuth2] Couldn't verify OAuth2 cookie`);
308
304
  throw new InvalidCredentialsError();
309
305
  }
310
- const { verifier, prompt, otp, callbackUrl } = tokenData;
306
+ const { verifier, prompt, otp } = tokenData;
311
307
  let { redirect } = tokenData;
312
308
  const accountability = createDefaultAccountability({
313
309
  ip: getIPFromReq(req),
@@ -330,7 +326,6 @@ export function createOAuth2AuthRouter(providerName) {
330
326
  code: req.query['code'],
331
327
  codeVerifier: verifier,
332
328
  state: req.query['state'],
333
- callbackUrl,
334
329
  }, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
335
330
  }
336
331
  catch (error) {
@@ -5,12 +5,13 @@ import type { RoleMap } from '../../types/rolemap.js';
5
5
  import { LocalAuthDriver } from './local.js';
6
6
  export declare class OpenIDAuthDriver extends LocalAuthDriver {
7
7
  client: null | Client;
8
+ redirectUrl: string;
8
9
  config: Record<string, any>;
9
10
  roleMap: RoleMap;
10
11
  constructor(options: AuthDriverOptions, config: Record<string, any>);
11
12
  private getClient;
12
13
  generateCodeVerifier(): string;
13
- generateAuthUrl(codeVerifier: string, prompt?: boolean, callbackUrl?: string): Promise<string>;
14
+ generateAuthUrl(codeVerifier: string, prompt?: boolean): Promise<string>;
14
15
  private fetchUserId;
15
16
  getUserID(payload: Record<string, any>): Promise<string>;
16
17
  login(user: User): Promise<void>;
@@ -16,19 +16,20 @@ import { AuthenticationService } from '../../services/authentication.js';
16
16
  import asyncHandler from '../../utils/async-handler.js';
17
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
18
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
19
- import { getSchema } from '../../utils/get-schema.js';
20
19
  import { getSecret } from '../../utils/get-secret.js';
20
+ import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
21
21
  import { verifyJWT } from '../../utils/jwt.js';
22
22
  import { Url } from '../../utils/url.js';
23
- import { generateCallbackUrl } from '../utils/generate-callback-url.js';
24
- import { isLoginRedirectAllowed } from '../utils/is-login-redirect-allowed.js';
25
23
  import { LocalAuthDriver } from './local.js';
24
+ import { getSchema } from '../../utils/get-schema.js';
26
25
  export class OpenIDAuthDriver extends LocalAuthDriver {
27
26
  client;
27
+ redirectUrl;
28
28
  config;
29
29
  roleMap;
30
30
  constructor(options, config) {
31
31
  super(options, config);
32
+ const env = useEnv();
32
33
  const logger = useLogger();
33
34
  const { issuerUrl, clientId, clientSecret, clientPrivateKeys, clientTokenEndpointAuthMethod, provider, issuerDiscoveryMustSucceed, } = config;
34
35
  const isPrivateKeyJwtAuthMethod = clientTokenEndpointAuthMethod === 'private_key_jwt';
@@ -36,6 +37,8 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
36
37
  logger.error('Invalid provider config');
37
38
  throw new InvalidProviderConfigError({ provider });
38
39
  }
40
+ const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', provider, 'callback');
41
+ this.redirectUrl = redirectUrl.toString();
39
42
  this.config = config;
40
43
  this.roleMap = {};
41
44
  const roleMapping = this.config['roleMapping'];
@@ -95,6 +98,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
95
98
  const client = new issuer.Client({
96
99
  client_id: clientId,
97
100
  ...(!isPrivateKeyJwtAuthMethod && { client_secret: clientSecret }),
101
+ redirect_uris: [this.redirectUrl],
98
102
  response_types: ['code'],
99
103
  ...clientOptionsOverrides,
100
104
  }, isPrivateKeyJwtAuthMethod ? { keys: clientPrivateKeys } : undefined);
@@ -112,7 +116,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
112
116
  generateCodeVerifier() {
113
117
  return generators.codeVerifier();
114
118
  }
115
- async generateAuthUrl(codeVerifier, prompt = false, callbackUrl) {
119
+ async generateAuthUrl(codeVerifier, prompt = false) {
116
120
  const { plainCodeChallenge } = this.config;
117
121
  try {
118
122
  const client = await this.getClient();
@@ -128,7 +132,6 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
128
132
  // Some providers require state even with PKCE
129
133
  state: codeChallenge,
130
134
  nonce: codeChallenge,
131
- redirect_uri: callbackUrl,
132
135
  });
133
136
  }
134
137
  catch (e) {
@@ -157,7 +160,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
157
160
  const codeChallenge = plainCodeChallenge
158
161
  ? payload['codeVerifier']
159
162
  : generators.codeChallenge(payload['codeVerifier']);
160
- tokenSet = await client.callback(payload['callbackUrl'], { code: payload['code'], state: payload['state'], iss: payload['iss'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge, nonce: codeChallenge });
163
+ tokenSet = await client.callback(this.redirectUrl, { code: payload['code'], state: payload['state'], iss: payload['iss'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge, nonce: codeChallenge });
161
164
  userInfo = tokenSet.claims();
162
165
  if (client.issuer.metadata['userinfo_endpoint']) {
163
166
  userInfo = {
@@ -326,17 +329,10 @@ export function createOpenIDAuthRouter(providerName) {
326
329
  const prompt = !!req.query['prompt'];
327
330
  const redirect = req.query['redirect'];
328
331
  const otp = req.query['otp'];
329
- if (!isLoginRedirectAllowed(providerName, `${req.protocol}://${req.hostname}`, redirect)) {
332
+ if (isLoginRedirectAllowed(redirect, providerName) === false) {
330
333
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
331
334
  }
332
- const callbackUrl = generateCallbackUrl(providerName, `${req.protocol}://${req.get('host')}`);
333
- const token = jwt.sign({
334
- verifier: codeVerifier,
335
- redirect,
336
- prompt,
337
- otp,
338
- callbackUrl,
339
- }, getSecret(), {
335
+ const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
340
336
  expiresIn: (env[`AUTH_${providerName.toUpperCase()}_LOGIN_TIMEOUT`] ?? '5m'),
341
337
  issuer: 'directus',
342
338
  });
@@ -345,7 +341,7 @@ export function createOpenIDAuthRouter(providerName) {
345
341
  sameSite: 'lax',
346
342
  });
347
343
  try {
348
- return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt, callbackUrl));
344
+ return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt));
349
345
  }
350
346
  catch {
351
347
  return res.redirect(new Url(env['PUBLIC_URL'])
@@ -369,7 +365,7 @@ export function createOpenIDAuthRouter(providerName) {
369
365
  const url = new Url(env['PUBLIC_URL']).addPath('admin', 'login');
370
366
  return res.redirect(`${url.toString()}?reason=${ErrorCode.InvalidCredentials}`);
371
367
  }
372
- const { verifier, prompt, otp, callbackUrl } = tokenData;
368
+ const { verifier, prompt, otp } = tokenData;
373
369
  let { redirect } = tokenData;
374
370
  const accountability = createDefaultAccountability({ ip: getIPFromReq(req) });
375
371
  const userAgent = req.get('user-agent')?.substring(0, 1024);
@@ -391,7 +387,6 @@ export function createOpenIDAuthRouter(providerName) {
391
387
  codeVerifier: verifier,
392
388
  state: req.query['state'],
393
389
  iss: req.query['iss'],
394
- callbackUrl,
395
390
  }, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
396
391
  }
397
392
  catch (error) {
@@ -12,9 +12,9 @@ import { respond } from '../../middleware/respond.js';
12
12
  import { AuthenticationService } from '../../services/authentication.js';
13
13
  import asyncHandler from '../../utils/async-handler.js';
14
14
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
15
- import { LocalAuthDriver } from './local.js';
16
15
  import { getSchema } from '../../utils/get-schema.js';
17
- import { isLoginRedirectAllowed } from '../utils/is-login-redirect-allowed.js';
16
+ import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
17
+ import { LocalAuthDriver } from './local.js';
18
18
  // Register the samlify schema validator
19
19
  samlify.setSchemaValidator(validator);
20
20
  export class SAMLAuthDriver extends LocalAuthDriver {
@@ -95,7 +95,7 @@ export function createSAMLAuthRouter(providerName) {
95
95
  const parsedUrl = new URL(url);
96
96
  if (req.query['redirect']) {
97
97
  const redirect = req.query['redirect'];
98
- if (!isLoginRedirectAllowed(providerName, `${req.protocol}://${req.hostname}`, redirect)) {
98
+ if (isLoginRedirectAllowed(redirect, providerName) === false) {
99
99
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
100
100
  }
101
101
  parsedUrl.searchParams.append('RelayState', redirect);
@@ -117,6 +117,9 @@ export function createSAMLAuthRouter(providerName) {
117
117
  const logger = useLogger();
118
118
  const relayState = req.body?.RelayState;
119
119
  const authMode = (env[`AUTH_${providerName.toUpperCase()}_MODE`] ?? 'session');
120
+ if (relayState && isLoginRedirectAllowed(relayState, providerName) === false) {
121
+ throw new InvalidPayloadError({ reason: `URL "${relayState}" can't be used to redirect after login` });
122
+ }
120
123
  try {
121
124
  const { sp, idp } = getAuthProvider(providerName);
122
125
  const { extract } = await sp.parseLoginResponse(idp, 'post', req);
@@ -1,10 +1,12 @@
1
1
  import { useEnv } from '@directus/env';
2
- import { InvalidQueryError, RangeNotSatisfiableError } from '@directus/errors';
2
+ import { InvalidPayloadError, InvalidQueryError, RangeNotSatisfiableError } from '@directus/errors';
3
3
  import { TransformationMethods } from '@directus/types';
4
- import { parseJSON } from '@directus/utils';
4
+ import { getDateTimeFormatted, parseJSON } from '@directus/utils';
5
5
  import contentDisposition from 'content-disposition';
6
6
  import { Router } from 'express';
7
7
  import { merge, pick } from 'lodash-es';
8
+ import * as z from 'zod';
9
+ import { fromZodError } from 'zod-validation-error';
8
10
  import { ASSET_TRANSFORM_QUERY_KEYS, SYSTEM_ASSET_ALLOW_LIST } from '../constants.js';
9
11
  import getDatabase from '../database/index.js';
10
12
  import { useLogger } from '../logger/index.js';
@@ -15,9 +17,44 @@ import asyncHandler from '../utils/async-handler.js';
15
17
  import { getCacheControlHeader } from '../utils/get-cache-headers.js';
16
18
  import { getConfigFromEnv } from '../utils/get-config-from-env.js';
17
19
  import { getMilliseconds } from '../utils/get-milliseconds.js';
20
+ import { isValidUuid } from '../utils/is-valid-uuid.js';
18
21
  const router = Router();
19
22
  const env = useEnv();
20
23
  router.use(useCollection('directus_files'));
24
+ router.post('/folder/:pk', asyncHandler(async (req, res) => {
25
+ const service = new AssetsService({
26
+ accountability: req.accountability,
27
+ schema: req.schema,
28
+ });
29
+ const { archive, complete, metadata } = await service.zipFolder(req.params['pk']);
30
+ res.setHeader('Content-Type', 'application/zip');
31
+ res.setHeader('Content-Disposition', `attachment; filename="folder-${metadata['name'] ? metadata['name'] : 'unknown'}-${getDateTimeFormatted()}.zip"`);
32
+ archive.pipe(res);
33
+ await complete();
34
+ }));
35
+ router.post('/files/', asyncHandler(async (req, res) => {
36
+ const service = new AssetsService({
37
+ accountability: req.accountability,
38
+ schema: req.schema,
39
+ });
40
+ const { error, data } = z
41
+ .object({
42
+ ids: z
43
+ .array(z.string().refine((v) => isValidUuid(v), {
44
+ error: '"id" must be a uuid',
45
+ }))
46
+ .min(1),
47
+ })
48
+ .safeParse(req.body);
49
+ if (error) {
50
+ throw new InvalidPayloadError({ reason: fromZodError(error).message });
51
+ }
52
+ const { archive, complete } = await service.zipFiles(data.ids);
53
+ res.setHeader('Content-Type', 'application/zip');
54
+ res.setHeader('Content-Disposition', `attachment; filename="files-${getDateTimeFormatted()}.zip"`);
55
+ archive.pipe(res);
56
+ await complete();
57
+ }));
21
58
  router.get('/:pk/:filename?',
22
59
  // Validate query params
23
60
  asyncHandler(async (req, res, next) => {
@@ -1,6 +1,6 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
2
  import { Router } from 'express';
3
- import { DirectusMCP } from '../mcp/index.js';
3
+ import { DirectusMCP } from '../ai/mcp/index.js';
4
4
  import { SettingsService } from '../services/settings.js';
5
5
  import asyncHandler from '../utils/async-handler.js';
6
6
  const router = Router();
@@ -6,8 +6,8 @@ import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
6
6
  import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
7
7
  import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
8
8
  import { getSchema } from '../../utils/get-schema.js';
9
- import { getSchemaInspector } from '../index.js';
10
9
  import { mergePermissions } from '../../permissions/utils/merge-permissions.js';
10
+ import { getSchemaInspector } from '../index.js';
11
11
  async function fetchRoleAccess(roles, context) {
12
12
  const roleAccess = {
13
13
  admin_access: false,
@@ -184,7 +184,7 @@ export async function down(knex) {
184
184
  // role permissions to be inserted once all processing is completed
185
185
  const rolePermissions = [];
186
186
  for (const role of roles) {
187
- const roleTree = await fetchRolesTree(role.id, knex);
187
+ const roleTree = await fetchRolesTree(role.id, { knex });
188
188
  let roleAccess = null;
189
189
  if (role.id !== null) {
190
190
  roleAccess = await fetchRoleAccess(roleTree, context);
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,14 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_settings', (table) => {
3
+ table.text('ai_openai_api_key');
4
+ table.text('ai_anthropic_api_key');
5
+ table.text('ai_system_prompt');
6
+ });
7
+ }
8
+ export async function down(knex) {
9
+ await knex.schema.alterTable('directus_settings', (table) => {
10
+ table.dropColumn('ai_openai_api_key');
11
+ table.dropColumn('ai_anthropic_api_key');
12
+ table.dropColumn('ai_system_prompt');
13
+ });
14
+ }
@@ -48,7 +48,7 @@ export async function runAst(originalAST, schema, accountability, options) {
48
48
  if (!rawItems)
49
49
  return null;
50
50
  // Run the items through the special transforms
51
- const payloadService = new PayloadService(collection, { knex, schema });
51
+ const payloadService = new PayloadService(collection, { accountability, knex, schema });
52
52
  let items = await payloadService.processValues('read', rawItems, query.alias ?? {}, query.aggregate ?? {});
53
53
  if (!items || (Array.isArray(items) && items.length === 0))
54
54
  return items;
@@ -63,11 +63,9 @@ export class InstallationManager {
63
63
  }
64
64
  await queue.onIdle();
65
65
  }
66
- else {
67
- // No custom location, so save to regular local extensions folder
68
- const dest = join(this.extensionPath, '.registry', versionId);
69
- await move(join(tempDir, extractedPath), dest, { overwrite: true });
70
- }
66
+ // move to regular local extensions folder
67
+ const dest = join(this.extensionPath, '.registry', versionId);
68
+ await move(join(tempDir, extractedPath), dest, { overwrite: true });
71
69
  }
72
70
  catch (err) {
73
71
  logger.warn(err);
@@ -92,9 +90,7 @@ export class InstallationManager {
92
90
  }
93
91
  await queue.onIdle();
94
92
  }
95
- else {
96
- const path = join(this.extensionPath, '.registry', folder);
97
- await remove(path);
98
- }
93
+ const path = join(this.extensionPath, '.registry', folder);
94
+ await remove(path);
99
95
  }
100
96
  }
@@ -0,0 +1,11 @@
1
+ export declare const SyncStatus: {
2
+ readonly SYNCING: "SYNCING";
3
+ readonly IDLE: "IDLE";
4
+ };
5
+ export type SyncStatus = keyof typeof SyncStatus;
6
+ export declare function getSyncStatus(): Promise<SyncStatus>;
7
+ export declare function setSyncStatus(status: SyncStatus): Promise<void>;
8
+ /**
9
+ * Checks the filesystem lock file if we are currently synchronizing
10
+ */
11
+ export declare function isSynchronizing(): Promise<boolean>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Utility functions to write a `.status` file on the filesystem to indicate active synchronization
3
+ */
4
+ import { join } from 'node:path';
5
+ import { exists } from 'fs-extra';
6
+ import { writeFile, rm } from 'node:fs/promises';
7
+ import { getExtensionsPath } from '../get-extensions-path.js';
8
+ export const SyncStatus = {
9
+ SYNCING: 'SYNCING',
10
+ IDLE: 'IDLE',
11
+ };
12
+ export async function getSyncStatus() {
13
+ const statusFilePath = join(getExtensionsPath(), '.status');
14
+ if (await exists(statusFilePath)) {
15
+ return SyncStatus.SYNCING;
16
+ }
17
+ return SyncStatus.IDLE;
18
+ }
19
+ export async function setSyncStatus(status) {
20
+ const statusFilePath = join(getExtensionsPath(), '.status');
21
+ if (status === SyncStatus.SYNCING) {
22
+ await writeFile(statusFilePath, '');
23
+ }
24
+ else {
25
+ await rm(statusFilePath);
26
+ }
27
+ }
28
+ /**
29
+ * Checks the filesystem lock file if we are currently synchronizing
30
+ */
31
+ export async function isSynchronizing() {
32
+ const status = await getSyncStatus();
33
+ return status === SyncStatus.SYNCING;
34
+ }
@@ -0,0 +1,6 @@
1
+ export type ExtensionSyncOptions = {
2
+ forceSync?: boolean;
3
+ partialSync?: string;
4
+ skipSync?: boolean;
5
+ };
6
+ export declare function syncExtensions(options?: ExtensionSyncOptions): Promise<void>;