@directus/api 35.2.0 → 36.0.0-rc.1

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 (192) hide show
  1. package/dist/ai/chat/models/chat-request.js +48 -48
  2. package/dist/ai/chat/models/object-request.js +6 -6
  3. package/dist/ai/chat/models/providers.js +14 -14
  4. package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
  5. package/dist/ai/mcp/server.js +44 -6
  6. package/dist/ai/mcp/utils.js +31 -0
  7. package/dist/ai/tools/assets/index.js +3 -3
  8. package/dist/ai/tools/collections/index.js +18 -18
  9. package/dist/ai/tools/fields/index.js +18 -18
  10. package/dist/ai/tools/files/index.js +18 -18
  11. package/dist/ai/tools/flows/index.js +16 -16
  12. package/dist/ai/tools/folders/index.js +18 -18
  13. package/dist/ai/tools/items/index.js +17 -17
  14. package/dist/ai/tools/operations/index.js +16 -16
  15. package/dist/ai/tools/relations/index.js +22 -22
  16. package/dist/ai/tools/schema/index.js +3 -3
  17. package/dist/ai/tools/schema.js +159 -159
  18. package/dist/ai/tools/system/index.js +3 -3
  19. package/dist/ai/tools/trigger-flow/index.js +3 -3
  20. package/dist/app.js +35 -11
  21. package/dist/auth/drivers/ldap.js +3 -1
  22. package/dist/auth/drivers/local.js +2 -0
  23. package/dist/auth/drivers/oauth2.js +3 -1
  24. package/dist/auth/drivers/openid.js +3 -1
  25. package/dist/auth/drivers/saml.js +2 -0
  26. package/dist/auth/utils/check-local-disabled.js +16 -0
  27. package/dist/auth/utils/check-sso-enabled.js +14 -0
  28. package/dist/auth.js +8 -5
  29. package/dist/cli/commands/bootstrap/index.js +3 -0
  30. package/dist/cli/commands/cache/clear.js +6 -1
  31. package/dist/cli/commands/roles/create.js +4 -1
  32. package/dist/cli/commands/users/create.js +3 -0
  33. package/dist/constants.js +8 -1
  34. package/dist/controllers/access.js +1 -1
  35. package/dist/controllers/activity.js +2 -1
  36. package/dist/controllers/assets.js +2 -0
  37. package/dist/controllers/auth.js +13 -5
  38. package/dist/controllers/collections.js +1 -1
  39. package/dist/controllers/comments.js +1 -1
  40. package/dist/controllers/dashboards.js +1 -1
  41. package/dist/controllers/fields.js +1 -1
  42. package/dist/controllers/files.js +3 -1
  43. package/dist/controllers/flows.js +6 -5
  44. package/dist/controllers/folders.js +1 -1
  45. package/dist/controllers/graphql.js +2 -0
  46. package/dist/controllers/items.js +3 -1
  47. package/dist/controllers/license.js +119 -0
  48. package/dist/controllers/mcp/index.js +38 -0
  49. package/dist/controllers/mcp/oauth-clients.js +68 -0
  50. package/dist/controllers/mcp/oauth-consent-page.js +316 -0
  51. package/dist/controllers/mcp/oauth.js +381 -0
  52. package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
  53. package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
  54. package/dist/controllers/notifications.js +1 -1
  55. package/dist/controllers/operations.js +1 -1
  56. package/dist/controllers/panels.js +1 -1
  57. package/dist/controllers/permissions.js +1 -1
  58. package/dist/controllers/policies.js +1 -1
  59. package/dist/controllers/presets.js +1 -1
  60. package/dist/controllers/revisions.js +3 -2
  61. package/dist/controllers/roles.js +1 -1
  62. package/dist/controllers/server.js +38 -10
  63. package/dist/controllers/shares.js +1 -1
  64. package/dist/controllers/translations.js +1 -1
  65. package/dist/controllers/users.js +1 -1
  66. package/dist/controllers/utils.js +2 -2
  67. package/dist/controllers/versions.js +12 -5
  68. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
  69. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
  70. package/dist/database/helpers/fn/dialects/mysql.js +7 -12
  71. package/dist/database/helpers/fn/dialects/oracle.js +3 -4
  72. package/dist/database/helpers/fn/dialects/postgres.js +4 -26
  73. package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
  74. package/dist/database/helpers/fn/json/parse-function.js +14 -6
  75. package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
  76. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
  77. package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
  78. package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
  79. package/dist/database/migrations/20260507A-add-licensing.js +22 -0
  80. package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
  81. package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
  82. package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
  83. package/dist/database/run-ast/lib/apply-query/index.js +4 -1
  84. package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
  85. package/dist/database/run-ast/lib/get-db-query.js +21 -9
  86. package/dist/database/run-ast/lib/parse-current-level.js +2 -1
  87. package/dist/database/run-ast/run-ast.js +2 -1
  88. package/dist/database/run-ast/utils/get-column.js +2 -1
  89. package/dist/database/run-ast/utils/merge-with-parent-items.js +5 -3
  90. package/dist/extensions/lib/installation/manager.js +1 -1
  91. package/dist/extensions/lib/sandbox/register/operation.js +1 -1
  92. package/dist/extensions/lib/sync/sync.js +1 -1
  93. package/dist/extensions/manager.js +3 -3
  94. package/dist/flows.js +5 -5
  95. package/dist/license/entitlements/lib/collections.js +37 -0
  96. package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
  97. package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
  98. package/dist/license/entitlements/lib/flows.js +29 -0
  99. package/dist/license/entitlements/lib/seats.js +103 -0
  100. package/dist/license/entitlements/lib/sso-enabled.js +45 -0
  101. package/dist/license/entitlements/manager.js +256 -0
  102. package/dist/license/index.js +4 -0
  103. package/dist/license/manager.js +505 -0
  104. package/dist/license/utils/compute-license-status.js +27 -0
  105. package/dist/license/utils/get-core-grace-expires-at.js +38 -0
  106. package/dist/license/utils/get-license-key.js +23 -0
  107. package/dist/license/utils/get-license-token.js +23 -0
  108. package/dist/license/utils/handle-license-error.js +41 -0
  109. package/dist/license/utils/is-in-core-grace-period.js +11 -0
  110. package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
  111. package/dist/license/utils/use-rpc.js +33 -0
  112. package/dist/middleware/cache.js +4 -1
  113. package/dist/middleware/error-handler.js +11 -0
  114. package/dist/middleware/extract-token.js +11 -2
  115. package/dist/middleware/is-admin.js +16 -0
  116. package/dist/middleware/is-locked.js +16 -0
  117. package/dist/middleware/mcp-oauth-guard.js +23 -0
  118. package/dist/middleware/request-counter.js +5 -2
  119. package/dist/packages/types/dist/index.js +117 -122
  120. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
  121. package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
  122. package/dist/request/is-denied-ip.js +2 -0
  123. package/dist/schedules/license.js +31 -0
  124. package/dist/schedules/oauth-cleanup.js +26 -0
  125. package/dist/schedules/retention.js +1 -1
  126. package/dist/schedules/telemetry.js +4 -1
  127. package/dist/schedules/tus.js +1 -1
  128. package/dist/schedules/utils/duration-to-cron.js +36 -0
  129. package/dist/services/activity.js +15 -0
  130. package/dist/services/authentication.js +12 -5
  131. package/dist/services/collections.js +40 -10
  132. package/dist/services/fields.js +6 -6
  133. package/dist/services/flows.js +12 -0
  134. package/dist/services/graphql/resolvers/system-admin.js +2 -2
  135. package/dist/services/graphql/resolvers/system-global.js +1 -1
  136. package/dist/services/graphql/resolvers/system.js +43 -27
  137. package/dist/services/graphql/schema/get-types.js +28 -7
  138. package/dist/services/graphql/schema/parse-query.js +8 -0
  139. package/dist/services/graphql/schema/read.js +12 -0
  140. package/dist/services/graphql/types/json-filter.js +30 -0
  141. package/dist/services/index.js +6 -6
  142. package/dist/services/items.js +32 -14
  143. package/dist/services/mcp-oauth/cimd.js +307 -0
  144. package/dist/services/mcp-oauth/index.js +1185 -0
  145. package/dist/services/mcp-oauth/types/error.js +22 -0
  146. package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
  147. package/dist/services/mcp-oauth/utils/domain.js +21 -0
  148. package/dist/services/mcp-oauth/utils/loopback.js +11 -0
  149. package/dist/services/mcp-oauth/utils/redirect.js +84 -0
  150. package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
  151. package/dist/services/payload.js +2 -1
  152. package/dist/services/permissions.js +31 -9
  153. package/dist/services/revisions.js +15 -0
  154. package/dist/services/server.js +66 -68
  155. package/dist/services/settings.js +37 -3
  156. package/dist/services/users.js +23 -6
  157. package/dist/services/utils.js +6 -1
  158. package/dist/services/versions.js +160 -70
  159. package/dist/utils/calculate-field-depth.js +1 -0
  160. package/dist/utils/create-admin.js +3 -3
  161. package/dist/utils/deep-freeze.js +24 -0
  162. package/dist/utils/extract-function-name.js +13 -0
  163. package/dist/utils/generate-translations.js +5 -5
  164. package/dist/utils/get-accountability-for-token.js +13 -1
  165. package/dist/utils/get-cache-key.js +1 -1
  166. package/dist/utils/get-history-filter-query.js +22 -0
  167. package/dist/utils/get-schema.js +2 -2
  168. package/dist/utils/get-service.js +3 -3
  169. package/dist/utils/is-admin.js +9 -0
  170. package/dist/utils/is-unauthenticated.js +15 -0
  171. package/dist/utils/parse-oauth-scope.js +12 -0
  172. package/dist/utils/sanitize-query.js +2 -2
  173. package/dist/utils/split-field-path.js +29 -0
  174. package/dist/utils/store.js +1 -1
  175. package/dist/utils/transaction.js +2 -2
  176. package/dist/utils/translations-validation.js +2 -2
  177. package/dist/utils/validate-query.js +35 -4
  178. package/dist/utils/validate-user-count-integrity.js +28 -5
  179. package/dist/utils/verify-session-jwt.js +5 -2
  180. package/dist/utils/versioning/handle-version.js +131 -48
  181. package/dist/utils/versioning/remove-circular.js +17 -0
  182. package/dist/websocket/authenticate.js +2 -1
  183. package/dist/websocket/collab/collab.js +1 -1
  184. package/dist/websocket/collab/room.js +1 -1
  185. package/dist/websocket/controllers/base.js +12 -0
  186. package/dist/websocket/controllers/graphql.js +1 -1
  187. package/dist/websocket/handlers/subscribe.js +1 -1
  188. package/dist/websocket/messages.js +64 -64
  189. package/dist/websocket/utils/items.js +2 -2
  190. package/license +90 -80
  191. package/package.json +33 -32
  192. package/dist/controllers/mcp.js +0 -31
@@ -0,0 +1,381 @@
1
+ import async_handler_default from "../../utils/async-handler.js";
2
+ import { useLogger } from "../../logger/index.js";
3
+ import database_default from "../../database/index.js";
4
+ import { Url } from "../../utils/url.js";
5
+ import { RateLimiterRes, createRateLimiter } from "../../rate-limiter.js";
6
+ import { getSchema } from "../../utils/get-schema.js";
7
+ import { SettingsService } from "../../services/settings.js";
8
+ import { getIPFromReq } from "../../utils/get-ip-from-req.js";
9
+ import { getAccountabilityForToken } from "../../utils/get-accountability-for-token.js";
10
+ import { getMcpUrls } from "../../ai/mcp/utils.js";
11
+ import { OAuthError } from "../../services/mcp-oauth/types/error.js";
12
+ import { getAllowedCustomRedirectSchemes } from "../../services/mcp-oauth/utils/redirect.js";
13
+ import { summarizeDcrRegistrationMetadata } from "../../services/mcp-oauth/utils/registration-debug.js";
14
+ import { McpOAuthService } from "../../services/mcp-oauth/index.js";
15
+ import { renderConsentPage, renderErrorPage } from "./oauth-consent-page.js";
16
+ import { useEnv } from "@directus/env";
17
+ import { toBoolean } from "@directus/utils";
18
+ import express, { Router } from "express";
19
+ import { createHash } from "node:crypto";
20
+ import { isIP } from "node:net";
21
+
22
+ //#region src/controllers/mcp/oauth.ts
23
+ function getRedirectIndicator(redirectUri, clientId, registrationType) {
24
+ try {
25
+ const redirectUrl = new URL(redirectUri);
26
+ const host = redirectUrl.hostname;
27
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]") return "localhost";
28
+ if (isIP(host) !== 0) return "ip-address";
29
+ if (registrationType === "cimd") try {
30
+ const clientUrl = new URL(clientId);
31
+ if (redirectUrl.hostname !== clientUrl.hostname) return "cross-origin";
32
+ } catch {}
33
+ } catch {}
34
+ }
35
+ /**
36
+ * RFC 6749 Section 3.1: reject requests with duplicate form parameters.
37
+ * Express parses duplicates as arrays, so any array value indicates a duplicate.
38
+ */
39
+ function rejectDuplicateParams(req, res, next) {
40
+ for (const [key, value] of Object.entries(req.body)) if (Array.isArray(value)) {
41
+ res.status(400).json({
42
+ error: "invalid_request",
43
+ error_description: `Duplicate parameter: ${key}`
44
+ });
45
+ return;
46
+ }
47
+ next();
48
+ }
49
+ /**
50
+ * Consent endpoints are browser-based (rendered HTML form), so the session must
51
+ * come from a cookie. Rejects bearer-token auth to prevent token-stealing attacks
52
+ * where a malicious client tricks the user into submitting consent via API.
53
+ */
54
+ function requireCookieAuth(req, res, next) {
55
+ if (req.tokenSource !== "cookie") {
56
+ res.status(403).json({
57
+ error: "access_denied",
58
+ error_description: "Cookie authentication required"
59
+ });
60
+ return;
61
+ }
62
+ next();
63
+ }
64
+ /**
65
+ * CSRF protection via Origin header. Only allows requests from the same origin
66
+ * as PUBLIC_URL. This guards the consent decision endpoint -- without it, a
67
+ * malicious page could POST approval on behalf of an authenticated user.
68
+ */
69
+ function requireSameOrigin(req, res, next) {
70
+ const env = useEnv();
71
+ const origin = req.headers["origin"];
72
+ if (!origin) {
73
+ res.status(403).json({
74
+ error: "access_denied",
75
+ error_description: "Origin header required"
76
+ });
77
+ return;
78
+ }
79
+ let publicUrl;
80
+ let requestOrigin;
81
+ try {
82
+ publicUrl = new URL(String(env["PUBLIC_URL"]));
83
+ requestOrigin = new URL(origin);
84
+ } catch {
85
+ res.status(403).json({
86
+ error: "access_denied",
87
+ error_description: "Malformed Origin header"
88
+ });
89
+ return;
90
+ }
91
+ if (publicUrl.origin !== requestOrigin.origin) {
92
+ res.status(403).json({
93
+ error: "access_denied",
94
+ error_description: "Cross-origin request not allowed"
95
+ });
96
+ return;
97
+ }
98
+ next();
99
+ }
100
+ /**
101
+ * Override Helmet's CSP form-action directive on this response only.
102
+ * The consent page form POSTs to 'self', but the 302 redirect targets external
103
+ * callback URIs. Chrome extends form-action to the redirect chain.
104
+ * Redirect URIs are validated at DCR/CIMD time (HTTPS, localhost, or known MCP desktop redirects only).
105
+ */
106
+ function relaxFormAction(res) {
107
+ const csp = res.getHeader("Content-Security-Policy");
108
+ if (typeof csp === "string") {
109
+ const customSchemes = getAllowedCustomRedirectSchemes();
110
+ const customSchemeSources = customSchemes.length > 0 ? ` ${customSchemes.join(" ")}` : "";
111
+ res.set("Content-Security-Policy", csp.replace(/form-action\s+([^;]+)/, `form-action $1 https: http://localhost:* http://127.0.0.1:*${customSchemeSources}`));
112
+ }
113
+ }
114
+ /**
115
+ * Convert OAuthError to RFC 6749/7591 JSON error format (`{ error, error_description }`).
116
+ * Non-OAuthError instances fall through to the default Directus error handler.
117
+ */
118
+ function oauthErrorHandler(err, _req, res, next) {
119
+ if (err instanceof OAuthError) {
120
+ for (const [key, value] of Object.entries(err.headers)) res.set(key, value);
121
+ res.status(err.status).json({
122
+ error: err.code,
123
+ error_description: err.description
124
+ });
125
+ return;
126
+ }
127
+ next(err);
128
+ }
129
+ function setCorsWildcard(_req, res, next) {
130
+ res.set("Access-Control-Allow-Origin", "*");
131
+ next();
132
+ }
133
+ function noCache(res) {
134
+ res.set("Cache-Control", "no-store");
135
+ res.set("Pragma", "no-cache");
136
+ }
137
+ function setNoCacheHeaders(_req, res, next) {
138
+ noCache(res);
139
+ next();
140
+ }
141
+ function getRegistrationRequestDebugContext(req) {
142
+ return {
143
+ content_type: req.headers["content-type"],
144
+ user_agent: req.headers["user-agent"],
145
+ registration: summarizeDcrRegistrationMetadata(req.body)
146
+ };
147
+ }
148
+ function logRegistrationBodyParseError(err, req, _res, next) {
149
+ useLogger().debug({
150
+ reason: "invalid_json",
151
+ content_type: req.headers["content-type"],
152
+ error: err instanceof Error ? err.message : void 0
153
+ }, "MCP OAuth DCR request body parsing failed");
154
+ next(err);
155
+ }
156
+ function isOAuthHtmlEndpoint(req) {
157
+ return req.path === "/mcp-oauth/authorize" || req.path === "/mcp-oauth/authorize/decision";
158
+ }
159
+ async function loadOAuthPageOpts(req, settingsService, authenticatedUserId = req.accountability?.user) {
160
+ const env = useEnv();
161
+ const [settings, user] = await Promise.all([settingsService.readSingleton({ fields: [
162
+ "project_name",
163
+ "project_color",
164
+ "project_logo",
165
+ "default_appearance"
166
+ ] }), authenticatedUserId ? database_default()("directus_users").where("id", authenticatedUserId).select("appearance").first() : null]);
167
+ const projectLogo = settings?.project_logo;
168
+ return {
169
+ projectName: settings?.project_name ?? "Directus",
170
+ projectColor: settings?.project_color ?? "#6644ff",
171
+ logoUrl: projectLogo ? new Url(env["PUBLIC_URL"]).addPath("assets", projectLogo).toString() : null,
172
+ appearance: user?.appearance ?? settings?.default_appearance ?? "auto"
173
+ };
174
+ }
175
+ /**
176
+ * Middleware: check mcp_enabled + mcp_oauth_enabled settings.
177
+ * Env vars (MCP_ENABLED, MCP_OAUTH_ENABLED) are already gated at the app.ts mount level.
178
+ */
179
+ async function checkOAuthSettings(req, res, next) {
180
+ const settingsService = new SettingsService({ schema: req.schema ?? await getSchema() });
181
+ const settings = await settingsService.readSingleton({ fields: ["mcp_enabled", "mcp_oauth_enabled"] });
182
+ if (toBoolean(settings?.mcp_enabled) !== true || toBoolean(settings?.mcp_oauth_enabled) !== true) {
183
+ if (isOAuthHtmlEndpoint(req)) {
184
+ const pageOpts = await loadOAuthPageOpts(req, settingsService);
185
+ res.set("Content-Type", "text/html; charset=utf-8");
186
+ res.status(403).send(await renderErrorPage("MCP OAuth is disabled in project settings.", pageOpts));
187
+ return;
188
+ }
189
+ res.set("Access-Control-Allow-Origin", "*");
190
+ if (req.path.includes("/token")) noCache(res);
191
+ res.status(403).json({
192
+ error: "mcp_oauth_disabled",
193
+ error_description: "MCP OAuth is disabled in project settings."
194
+ });
195
+ return;
196
+ }
197
+ next();
198
+ }
199
+ function createRateLimitMiddleware(prefix) {
200
+ if (useEnv()[`${prefix}_ENABLED`] !== true) return (_req, _res, next) => next();
201
+ const limiter = createRateLimiter(prefix);
202
+ return (req, res, next) => {
203
+ limiter.consume(getIPFromReq(req) ?? "0.0.0.0").then(() => next()).catch((rlRes) => {
204
+ if (rlRes instanceof RateLimiterRes) {
205
+ res.set("Retry-After", String(Math.ceil(rlRes.msBeforeNext / 1e3)));
206
+ res.status(429).json({
207
+ error: "rate_limit_exceeded",
208
+ error_description: "Too many requests"
209
+ });
210
+ } else next(rlRes);
211
+ });
212
+ };
213
+ }
214
+ const oauthRateLimitMiddleware = createRateLimitMiddleware("RATE_LIMITER_MCP_OAUTH");
215
+ const registrationRateLimitMiddleware = createRateLimitMiddleware("RATE_LIMITER_MCP_OAUTH_REGISTRATION");
216
+ /**
217
+ * Unauthenticated OAuth routes. Mounted before the authenticate middleware in app.ts.
218
+ *
219
+ * Routes: `/.well-known/oauth-protected-resource`, `/.well-known/oauth-authorization-server`
220
+ * (RFC 9728/8414 discovery), `/mcp-oauth/authorize` (consent page with manual session check),
221
+ * `/mcp-oauth/register` (DCR), `/mcp-oauth/token`, `/mcp-oauth/revoke`.
222
+ */
223
+ const mcpOAuthPublicRouter = Router();
224
+ mcpOAuthPublicRouter.get("/.well-known/oauth-protected-resource*", setCorsWildcard, async_handler_default(checkOAuthSettings), async_handler_default(async (_req, res) => {
225
+ const service = new McpOAuthService({ schema: await getSchema() });
226
+ res.json(service.getProtectedResourceMetadata());
227
+ }));
228
+ mcpOAuthPublicRouter.get("/.well-known/oauth-authorization-server*", setCorsWildcard, async_handler_default(checkOAuthSettings), async_handler_default(async (_req, res) => {
229
+ const service = new McpOAuthService({ schema: await getSchema() });
230
+ res.json(await service.getAuthorizationServerMetadata());
231
+ }));
232
+ mcpOAuthPublicRouter.get("/mcp-oauth/authorize", oauthRateLimitMiddleware, async_handler_default(checkOAuthSettings), async_handler_default(async (req, res) => {
233
+ const env = useEnv();
234
+ const loginUrl = new Url(env["PUBLIC_URL"]).addPath("admin", "login").toString();
235
+ const schema = await getSchema();
236
+ function redirectToLogin() {
237
+ res.redirect(302, `${loginUrl}?redirect=${encodeURIComponent(req.originalUrl)}`);
238
+ }
239
+ const cookieName = env["SESSION_COOKIE_NAME"];
240
+ const sessionToken = req.cookies?.[cookieName];
241
+ if (!sessionToken) {
242
+ redirectToLogin();
243
+ return;
244
+ }
245
+ let accountability;
246
+ try {
247
+ accountability = await getAccountabilityForToken(sessionToken, { schema });
248
+ } catch {
249
+ redirectToLogin();
250
+ return;
251
+ }
252
+ if (!accountability?.user || accountability.oauth) {
253
+ redirectToLogin();
254
+ return;
255
+ }
256
+ const pageOpts = await loadOAuthPageOpts(req, new SettingsService({ schema }), accountability.user);
257
+ const service = new McpOAuthService({ schema });
258
+ const sessionHash = createHash("sha256").update(sessionToken).digest("hex");
259
+ try {
260
+ const result = await service.validateAuthorization({
261
+ client_id: req.query["client_id"],
262
+ redirect_uri: req.query["redirect_uri"],
263
+ response_type: req.query["response_type"],
264
+ code_challenge: req.query["code_challenge"],
265
+ code_challenge_method: req.query["code_challenge_method"],
266
+ scope: req.query["scope"],
267
+ resource: req.query["resource"],
268
+ state: req.query["state"],
269
+ response_mode: req.query["response_mode"]
270
+ }, accountability.user, sessionHash);
271
+ const decisionUrl = new Url(env["PUBLIC_URL"]).addPath("mcp-oauth", "authorize", "decision").toString();
272
+ res.set("Content-Type", "text/html; charset=utf-8");
273
+ noCache(res);
274
+ relaxFormAction(res);
275
+ const clientId = req.query["client_id"];
276
+ const registrationType = result.registration_type ?? "dcr";
277
+ const consentData = {
278
+ clientName: result.client_name,
279
+ redirectUri: result.redirect_uri,
280
+ scope: result.scope,
281
+ signedParams: result.signed_params,
282
+ decisionUrl,
283
+ clientDomain: result.client_domain,
284
+ registrationType,
285
+ redirectIndicator: getRedirectIndicator(result.redirect_uri, clientId, registrationType)
286
+ };
287
+ res.send(await renderConsentPage(consentData, pageOpts));
288
+ } catch (err) {
289
+ if (!(err instanceof OAuthError)) throw err;
290
+ if (err.redirectable) {
291
+ const redirectUri = req.query["redirect_uri"];
292
+ const state = req.query["state"];
293
+ const { issuerUrl } = getMcpUrls();
294
+ const url = new URL(redirectUri);
295
+ url.searchParams.set("error", err.code);
296
+ url.searchParams.set("error_description", err.description);
297
+ if (state) url.searchParams.set("state", state);
298
+ url.searchParams.set("iss", issuerUrl);
299
+ res.redirect(302, url.toString());
300
+ return;
301
+ }
302
+ res.set("Content-Type", "text/html; charset=utf-8");
303
+ noCache(res);
304
+ res.status(err.status).send(await renderErrorPage(err.description, pageOpts));
305
+ }
306
+ }));
307
+ mcpOAuthPublicRouter.post("/mcp-oauth/register", registrationRateLimitMiddleware, async_handler_default(checkOAuthSettings), express.json(), logRegistrationBodyParseError, setCorsWildcard, async_handler_default(async (req, res) => {
308
+ const logger = useLogger();
309
+ const service = new McpOAuthService({ schema: await getSchema() });
310
+ const debugContext = getRegistrationRequestDebugContext(req);
311
+ let result;
312
+ try {
313
+ result = await service.registerClient(req.body);
314
+ } catch (err) {
315
+ if (err instanceof OAuthError) logger.debug({
316
+ ...debugContext,
317
+ status: err.status,
318
+ code: err.code,
319
+ description: err.description
320
+ }, "MCP OAuth DCR request rejected");
321
+ else logger.debug({
322
+ ...debugContext,
323
+ error: err instanceof Error ? {
324
+ name: err.name,
325
+ message: err.message
326
+ } : void 0
327
+ }, "MCP OAuth DCR request failed");
328
+ throw err;
329
+ }
330
+ logger.debug({
331
+ client_id: result.client_id,
332
+ token_endpoint_auth_method: result.token_endpoint_auth_method,
333
+ redirect_uri_count: result.redirect_uris.length,
334
+ grant_types: result.grant_types
335
+ }, "MCP OAuth DCR client registered");
336
+ res.set("Cache-Control", "no-store");
337
+ res.status(201).json(result);
338
+ }));
339
+ mcpOAuthPublicRouter.post("/mcp-oauth/token", oauthRateLimitMiddleware, async_handler_default(checkOAuthSettings), express.urlencoded({ extended: false }), rejectDuplicateParams, setCorsWildcard, setNoCacheHeaders, async_handler_default(async (req, res) => {
340
+ const service = new McpOAuthService({ schema: await getSchema() });
341
+ const context = {
342
+ ip: getIPFromReq(req) ?? "0.0.0.0",
343
+ userAgent: req.headers["user-agent"] ?? "unknown"
344
+ };
345
+ const grantType = req.body.grant_type;
346
+ let result;
347
+ const authParams = {
348
+ ...req.body,
349
+ authorization_header: req.headers.authorization
350
+ };
351
+ if (grantType === "authorization_code") result = await service.exchangeCode(authParams, context);
352
+ else if (grantType === "refresh_token") result = await service.refreshToken(authParams, context);
353
+ else if (!grantType) throw new OAuthError(400, "invalid_request", "grant_type is required");
354
+ else throw new OAuthError(400, "unsupported_grant_type", `Unsupported grant_type: ${grantType}`);
355
+ res.json(result);
356
+ }));
357
+ mcpOAuthPublicRouter.post("/mcp-oauth/revoke", oauthRateLimitMiddleware, async_handler_default(checkOAuthSettings), express.urlencoded({ extended: false }), rejectDuplicateParams, setCorsWildcard, async_handler_default(async (req, res) => {
358
+ await new McpOAuthService({ schema: await getSchema() }).revokeToken({
359
+ ...req.body,
360
+ authorization_header: req.headers.authorization
361
+ });
362
+ res.status(200).json({});
363
+ }));
364
+ mcpOAuthPublicRouter.use(oauthErrorHandler);
365
+ /**
366
+ * Authenticated OAuth routes. Mounted after the authenticate middleware in app.ts.
367
+ * Requires cookie-based auth + same-origin checks (consent is a browser interaction).
368
+ *
369
+ * Routes: `/mcp-oauth/authorize/decision` (native form POST with 302 redirect).
370
+ */
371
+ const mcpOAuthProtectedRouter = Router();
372
+ mcpOAuthProtectedRouter.post("/mcp-oauth/authorize/decision", async_handler_default(checkOAuthSettings), express.urlencoded({ extended: false }), rejectDuplicateParams, requireCookieAuth, requireSameOrigin, async_handler_default(async (req, res) => {
373
+ const redirectUrl = await new McpOAuthService({ schema: req.schema }).processDecision(req.body, req.accountability.user, req.token);
374
+ res.set("Referrer-Policy", "no-referrer");
375
+ noCache(res);
376
+ res.redirect(302, redirectUrl);
377
+ }));
378
+ mcpOAuthProtectedRouter.use(oauthErrorHandler);
379
+
380
+ //#endregion
381
+ export { getRedirectIndicator, mcpOAuthProtectedRouter, mcpOAuthPublicRouter };
@@ -0,0 +1,62 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en"{{ themeAttr | raw }}>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Authorize - {{ projectName }}</title>
7
+ <style>{{ styles | raw }}</style>
8
+ </head>
9
+ <body>
10
+ <div class="card">
11
+ <div class="logo-row">
12
+ <div class="{{ logoClass }}">
13
+ {% if logoUrl %}
14
+ <img src="{{ logoUrl }}" alt="{{ logoAlt }}">
15
+ {% elsif logoDataUri %}
16
+ <img src="{{ logoDataUri | raw }}" alt="Directus">
17
+ {% endif %}
18
+ </div>
19
+ </div>
20
+ <h1><strong>{{ clientName }}</strong> is requesting access</h1>
21
+ <div class="details">
22
+ <div class="details-label">Details</div>
23
+ <div class="detail-row">
24
+ <span class="detail-key">Name</span>
25
+ <span class="detail-value name">{{ clientName }}</span>
26
+ </div>
27
+ {% if clientDomain %}
28
+ <div class="detail-row">
29
+ <span class="detail-key">Domain</span>
30
+ <span class="detail-value">{{ clientDomain }}</span>
31
+ </div>
32
+ {% endif %}
33
+ <div class="detail-row">
34
+ <span class="detail-key">Redirect URI</span>
35
+ <span class="detail-value">{{ redirectUri }}</span>
36
+ </div>
37
+ {% if redirectIndicator == 'localhost' %}
38
+ <div class="detail-row">
39
+ <span class="detail-key"></span>
40
+ <span class="detail-value redirect-warn">Redirects to your local machine. Another application on this device could intercept this authorization.</span>
41
+ </div>
42
+ {% elsif redirectIndicator == 'cross-origin' %}
43
+ <div class="detail-row">
44
+ <span class="detail-key"></span>
45
+ <span class="detail-value redirect-warn">Redirect goes to a different domain than the application.</span>
46
+ </div>
47
+ {% elsif redirectIndicator == 'ip-address' %}
48
+ <div class="detail-row">
49
+ <span class="detail-key"></span>
50
+ <span class="detail-value redirect-warn">Redirect uses an IP address.</span>
51
+ </div>
52
+ {% endif %}
53
+ </div>
54
+ <p class="note">This MCP client is requesting to be authorized. If you approve, it will be able to access data on <strong>{{ projectName }}</strong> on your behalf. This application has not been verified by <strong>{{ projectName }}</strong>.</p>
55
+ <form action="{{ decisionUrl }}" method="POST" class="actions">
56
+ <input type="hidden" name="signed_params" value="{{ signedParams }}">
57
+ <button type="submit" name="approved" value="false" class="btn-cancel">Cancel</button>
58
+ <button type="submit" name="approved" value="true" class="btn-approve">Approve</button>
59
+ </form>
60
+ </div>
61
+ </body>
62
+ </html>
@@ -0,0 +1,28 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en"{{ themeAttr | raw }}>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Authorization Error - {{ projectName }}</title>
7
+ <style>
8
+ {{ styles | raw }}
9
+ h1 { color: var(--theme--danger); }
10
+ .note { text-align: center; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <div class="card">
15
+ <div class="logo-row">
16
+ <div class="{{ logoClass }}">
17
+ {% if logoUrl %}
18
+ <img src="{{ logoUrl }}" alt="{{ logoAlt }}">
19
+ {% elsif logoDataUri %}
20
+ <img src="{{ logoDataUri | raw }}" alt="Directus">
21
+ {% endif %}
22
+ </div>
23
+ </div>
24
+ <h1>Authorization Error</h1>
25
+ <p class="note">{{ description }}</p>
26
+ </div>
27
+ </body>
28
+ </html>
@@ -1,7 +1,7 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
- import { sanitizeQuery } from "../utils/sanitize-query.js";
3
2
  import { NotificationsService } from "../services/notifications.js";
4
3
  import { respond } from "../middleware/respond.js";
4
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
5
5
  import { MetaService } from "../services/meta.js";
6
6
  import use_collection_default from "../middleware/use-collection.js";
7
7
  import { validateBatch } from "../middleware/validate-batch.js";
@@ -1,6 +1,6 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
- import { sanitizeQuery } from "../utils/sanitize-query.js";
3
2
  import { respond } from "../middleware/respond.js";
3
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
4
4
  import { MetaService } from "../services/meta.js";
5
5
  import { OperationsService } from "../services/operations.js";
6
6
  import use_collection_default from "../middleware/use-collection.js";
@@ -1,6 +1,6 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
- import { sanitizeQuery } from "../utils/sanitize-query.js";
3
2
  import { respond } from "../middleware/respond.js";
3
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
4
4
  import { MetaService } from "../services/meta.js";
5
5
  import { PanelsService } from "../services/panels.js";
6
6
  import use_collection_default from "../middleware/use-collection.js";
@@ -1,8 +1,8 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
2
  import database_default from "../database/index.js";
3
- import { sanitizeQuery } from "../utils/sanitize-query.js";
4
3
  import { respond } from "../middleware/respond.js";
5
4
  import { fetchAccountabilityCollectionAccess } from "../permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js";
5
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
6
6
  import { MetaService } from "../services/meta.js";
7
7
  import { PermissionsService } from "../services/permissions.js";
8
8
  import use_collection_default from "../middleware/use-collection.js";
@@ -1,8 +1,8 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
2
  import database_default from "../database/index.js";
3
- import { sanitizeQuery } from "../utils/sanitize-query.js";
4
3
  import { respond } from "../middleware/respond.js";
5
4
  import { fetchAccountabilityPolicyGlobals } from "../permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js";
5
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
6
6
  import { MetaService } from "../services/meta.js";
7
7
  import { PoliciesService } from "../services/policies.js";
8
8
  import use_collection_default from "../middleware/use-collection.js";
@@ -1,7 +1,7 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
- import { sanitizeQuery } from "../utils/sanitize-query.js";
3
2
  import { respond } from "../middleware/respond.js";
4
3
  import { PresetsService } from "../services/presets.js";
4
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
5
5
  import { MetaService } from "../services/meta.js";
6
6
  import use_collection_default from "../middleware/use-collection.js";
7
7
  import { validateBatch } from "../middleware/validate-batch.js";
@@ -1,6 +1,6 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
- import { respond } from "../middleware/respond.js";
3
2
  import { RevisionsService } from "../services/revisions.js";
3
+ import { respond } from "../middleware/respond.js";
4
4
  import { MetaService } from "../services/meta.js";
5
5
  import use_collection_default from "../middleware/use-collection.js";
6
6
  import { validateBatch } from "../middleware/validate-batch.js";
@@ -19,7 +19,8 @@ const readHandler = async_handler_default(async (req, res, next) => {
19
19
  schema: req.schema
20
20
  });
21
21
  const records = await service.readByQuery(req.sanitizedQuery);
22
- const meta = await metaService.getMetaForQuery("directus_revisions", req.sanitizedQuery);
22
+ const historyQuery = service.getLimitedHistoryQuery(req.sanitizedQuery);
23
+ const meta = await metaService.getMetaForQuery("directus_revisions", historyQuery);
23
24
  res.locals["payload"] = {
24
25
  data: records || null,
25
26
  meta
@@ -1,7 +1,7 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
- import { sanitizeQuery } from "../utils/sanitize-query.js";
3
2
  import { respond } from "../middleware/respond.js";
4
3
  import { RolesService } from "../services/roles.js";
4
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
5
5
  import { MetaService } from "../services/meta.js";
6
6
  import use_collection_default from "../middleware/use-collection.js";
7
7
  import { validateBatch } from "../middleware/validate-batch.js";
@@ -4,10 +4,14 @@ import { respond } from "../middleware/respond.js";
4
4
  import { ServerService } from "../services/server.js";
5
5
  import { SpecificationService } from "../services/specifications.js";
6
6
  import "../services/index.js";
7
+ import { getLicenseManager } from "../license/manager.js";
7
8
  import { createAdmin } from "../utils/create-admin.js";
8
9
  import { useEnv } from "@directus/env";
9
- import { ErrorCode, ForbiddenError, RouteNotFoundError, isDirectusError } from "@directus/errors";
10
+ import { ErrorCode, ForbiddenError, InvalidPayloadError, RouteNotFoundError, isDirectusError } from "@directus/errors";
11
+ import { toBoolean } from "@directus/utils";
10
12
  import { Router } from "express";
13
+ import { fromZodError } from "zod-validation-error";
14
+ import z from "zod";
11
15
  import { format } from "date-fns";
12
16
 
13
17
  //#region src/controllers/server.ts
@@ -46,7 +50,7 @@ router.get("/info", async_handler_default(async (req, res, next) => {
46
50
  res.locals["payload"] = { data };
47
51
  return next();
48
52
  }), respond);
49
- router.get("/health", async_handler_default(async (req, res, next) => {
53
+ if (toBoolean(env["HEALTHCHECK_ENABLED"]) !== false) router.get("/health", async_handler_default(async (req, res, next) => {
50
54
  const data = await new ServerService({
51
55
  accountability: req.accountability,
52
56
  schema: req.schema
@@ -57,19 +61,43 @@ router.get("/health", async_handler_default(async (req, res, next) => {
57
61
  res.locals["cache"] = false;
58
62
  return next();
59
63
  }), respond);
64
+ const SetupSchema = z.object({
65
+ admin: z.object({
66
+ email: z.string(),
67
+ password: z.string(),
68
+ first_name: z.string().optional(),
69
+ last_name: z.string().optional()
70
+ }),
71
+ license_key: z.string().optional(),
72
+ owner: z.object({
73
+ project_owner: z.string().nullable(),
74
+ project_usage: z.enum([
75
+ "personal",
76
+ "commercial",
77
+ "community"
78
+ ]).nullable(),
79
+ org_name: z.string().nullable(),
80
+ product_updates: z.boolean()
81
+ }).optional()
82
+ });
60
83
  router.post("/setup", async_handler_default(async (req, _res, next) => {
61
84
  if (await new ServerService({ schema: req.schema }).isSetupCompleted()) throw new ForbiddenError();
85
+ const { error, data } = SetupSchema.safeParse(req.body);
86
+ if (error) throw new InvalidPayloadError({ reason: fromZodError(error).message });
87
+ const licenseManager = getLicenseManager();
62
88
  try {
89
+ if (data.license_key) await licenseManager.activate(data.license_key);
63
90
  await createAdmin(req.schema, {
64
- email: req.body.project_owner,
65
- password: req.body.password,
66
- first_name: req.body.first_name,
67
- last_name: req.body.last_name
91
+ email: data.admin.email,
92
+ password: data.admin.password,
93
+ first_name: data.admin.first_name,
94
+ last_name: data.admin.last_name
68
95
  });
69
- new SettingsService({ schema: req.schema }).setOwner(req.body);
70
- } catch (error) {
71
- if (isDirectusError(error, ErrorCode.Forbidden)) return next();
72
- throw error;
96
+ const settingsService = new SettingsService({ schema: req.schema });
97
+ if (data.owner) settingsService.setOwner(data.owner);
98
+ } catch (error$1) {
99
+ if (isDirectusError(error$1, ErrorCode.Forbidden)) return next();
100
+ throw error$1;
73
101
  }
74
102
  return next();
75
103
  }), respond);
@@ -1,7 +1,7 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
2
  import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS, UUID_REGEX } from "../constants.js";
3
- import { sanitizeQuery } from "../utils/sanitize-query.js";
4
3
  import { respond } from "../middleware/respond.js";
4
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
5
5
  import { SharesService } from "../services/shares.js";
6
6
  import use_collection_default from "../middleware/use-collection.js";
7
7
  import { validateBatch } from "../middleware/validate-batch.js";
@@ -1,6 +1,6 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
- import { sanitizeQuery } from "../utils/sanitize-query.js";
3
2
  import { respond } from "../middleware/respond.js";
3
+ import { sanitizeQuery } from "../utils/sanitize-query.js";
4
4
  import { MetaService } from "../services/meta.js";
5
5
  import { TranslationsService } from "../services/translations.js";
6
6
  import use_collection_default from "../middleware/use-collection.js";