@directus/api 35.2.0 → 36.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/ai/chat/models/chat-request.js +48 -48
  2. package/dist/ai/chat/models/object-request.js +6 -6
  3. package/dist/ai/chat/models/providers.js +14 -14
  4. package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
  5. package/dist/ai/mcp/server.js +44 -6
  6. package/dist/ai/mcp/utils.js +31 -0
  7. package/dist/ai/tools/assets/index.js +3 -3
  8. package/dist/ai/tools/collections/index.js +18 -18
  9. package/dist/ai/tools/fields/index.js +18 -18
  10. package/dist/ai/tools/files/index.js +18 -18
  11. package/dist/ai/tools/flows/index.js +16 -16
  12. package/dist/ai/tools/folders/index.js +18 -18
  13. package/dist/ai/tools/items/index.js +17 -17
  14. package/dist/ai/tools/operations/index.js +16 -16
  15. package/dist/ai/tools/relations/index.js +22 -22
  16. package/dist/ai/tools/schema/index.js +3 -3
  17. package/dist/ai/tools/schema.js +159 -159
  18. package/dist/ai/tools/system/index.js +3 -3
  19. package/dist/ai/tools/trigger-flow/index.js +3 -3
  20. package/dist/app.js +33 -9
  21. package/dist/auth/drivers/ldap.js +3 -1
  22. package/dist/auth/drivers/local.js +2 -0
  23. package/dist/auth/drivers/oauth2.js +3 -1
  24. package/dist/auth/drivers/openid.js +3 -1
  25. package/dist/auth/drivers/saml.js +2 -0
  26. package/dist/auth/utils/check-local-disabled.js +16 -0
  27. package/dist/auth/utils/check-sso-enabled.js +14 -0
  28. package/dist/auth.js +8 -5
  29. package/dist/cli/commands/bootstrap/index.js +3 -0
  30. package/dist/cli/commands/cache/clear.js +6 -1
  31. package/dist/cli/commands/roles/create.js +4 -1
  32. package/dist/cli/commands/users/create.js +3 -0
  33. package/dist/constants.js +8 -1
  34. package/dist/controllers/access.js +1 -1
  35. package/dist/controllers/activity.js +2 -1
  36. package/dist/controllers/assets.js +2 -0
  37. package/dist/controllers/auth.js +13 -5
  38. package/dist/controllers/collections.js +1 -1
  39. package/dist/controllers/comments.js +1 -1
  40. package/dist/controllers/dashboards.js +1 -1
  41. package/dist/controllers/fields.js +1 -1
  42. package/dist/controllers/files.js +3 -1
  43. package/dist/controllers/flows.js +6 -5
  44. package/dist/controllers/folders.js +1 -1
  45. package/dist/controllers/graphql.js +2 -0
  46. package/dist/controllers/items.js +3 -1
  47. package/dist/controllers/license.js +119 -0
  48. package/dist/controllers/mcp/index.js +38 -0
  49. package/dist/controllers/mcp/oauth-clients.js +68 -0
  50. package/dist/controllers/mcp/oauth-consent-page.js +316 -0
  51. package/dist/controllers/mcp/oauth.js +381 -0
  52. package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
  53. package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
  54. package/dist/controllers/notifications.js +1 -1
  55. package/dist/controllers/operations.js +1 -1
  56. package/dist/controllers/panels.js +1 -1
  57. package/dist/controllers/permissions.js +1 -1
  58. package/dist/controllers/policies.js +1 -1
  59. package/dist/controllers/presets.js +1 -1
  60. package/dist/controllers/revisions.js +3 -2
  61. package/dist/controllers/roles.js +1 -1
  62. package/dist/controllers/server.js +38 -9
  63. package/dist/controllers/shares.js +1 -1
  64. package/dist/controllers/translations.js +1 -1
  65. package/dist/controllers/users.js +1 -1
  66. package/dist/controllers/utils.js +2 -2
  67. package/dist/controllers/versions.js +12 -5
  68. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
  69. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
  70. package/dist/database/helpers/fn/dialects/mysql.js +7 -12
  71. package/dist/database/helpers/fn/dialects/oracle.js +3 -4
  72. package/dist/database/helpers/fn/dialects/postgres.js +4 -26
  73. package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
  74. package/dist/database/helpers/fn/json/parse-function.js +14 -6
  75. package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
  76. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
  77. package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
  78. package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
  79. package/dist/database/migrations/20260507A-add-licensing.js +22 -0
  80. package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
  81. package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
  82. package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
  83. package/dist/database/run-ast/lib/apply-query/index.js +4 -1
  84. package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
  85. package/dist/database/run-ast/lib/get-db-query.js +21 -9
  86. package/dist/database/run-ast/lib/parse-current-level.js +2 -1
  87. package/dist/database/run-ast/run-ast.js +2 -1
  88. package/dist/database/run-ast/utils/get-column.js +2 -1
  89. package/dist/extensions/lib/installation/manager.js +1 -1
  90. package/dist/extensions/lib/sandbox/register/operation.js +1 -1
  91. package/dist/extensions/lib/sync/sync.js +1 -1
  92. package/dist/extensions/manager.js +3 -3
  93. package/dist/flows.js +5 -5
  94. package/dist/license/entitlements/lib/collections.js +37 -0
  95. package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
  96. package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
  97. package/dist/license/entitlements/lib/flows.js +29 -0
  98. package/dist/license/entitlements/lib/seats.js +103 -0
  99. package/dist/license/entitlements/lib/sso-enabled.js +45 -0
  100. package/dist/license/entitlements/manager.js +256 -0
  101. package/dist/license/index.js +4 -0
  102. package/dist/license/manager.js +505 -0
  103. package/dist/license/utils/compute-license-status.js +27 -0
  104. package/dist/license/utils/get-core-grace-expires-at.js +38 -0
  105. package/dist/license/utils/get-license-key.js +23 -0
  106. package/dist/license/utils/get-license-token.js +23 -0
  107. package/dist/license/utils/handle-license-error.js +41 -0
  108. package/dist/license/utils/is-in-core-grace-period.js +11 -0
  109. package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
  110. package/dist/license/utils/use-rpc.js +33 -0
  111. package/dist/middleware/cache.js +4 -1
  112. package/dist/middleware/error-handler.js +11 -0
  113. package/dist/middleware/extract-token.js +11 -2
  114. package/dist/middleware/is-admin.js +16 -0
  115. package/dist/middleware/is-locked.js +16 -0
  116. package/dist/middleware/mcp-oauth-guard.js +23 -0
  117. package/dist/middleware/request-counter.js +5 -2
  118. package/dist/packages/types/dist/index.js +117 -122
  119. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
  120. package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
  121. package/dist/request/is-denied-ip.js +2 -0
  122. package/dist/schedules/license.js +31 -0
  123. package/dist/schedules/oauth-cleanup.js +26 -0
  124. package/dist/schedules/retention.js +1 -1
  125. package/dist/schedules/telemetry.js +4 -1
  126. package/dist/schedules/tus.js +1 -1
  127. package/dist/schedules/utils/duration-to-cron.js +36 -0
  128. package/dist/services/activity.js +15 -0
  129. package/dist/services/authentication.js +12 -5
  130. package/dist/services/collections.js +40 -10
  131. package/dist/services/fields.js +6 -6
  132. package/dist/services/flows.js +12 -0
  133. package/dist/services/graphql/resolvers/system-admin.js +2 -2
  134. package/dist/services/graphql/resolvers/system-global.js +1 -1
  135. package/dist/services/graphql/resolvers/system.js +34 -18
  136. package/dist/services/graphql/schema/get-types.js +23 -2
  137. package/dist/services/graphql/schema/parse-query.js +8 -0
  138. package/dist/services/graphql/schema/read.js +12 -0
  139. package/dist/services/graphql/types/json-filter.js +30 -0
  140. package/dist/services/index.js +6 -6
  141. package/dist/services/items.js +32 -14
  142. package/dist/services/mcp-oauth/cimd.js +307 -0
  143. package/dist/services/mcp-oauth/index.js +1185 -0
  144. package/dist/services/mcp-oauth/types/error.js +22 -0
  145. package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
  146. package/dist/services/mcp-oauth/utils/domain.js +21 -0
  147. package/dist/services/mcp-oauth/utils/loopback.js +11 -0
  148. package/dist/services/mcp-oauth/utils/redirect.js +84 -0
  149. package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
  150. package/dist/services/payload.js +2 -1
  151. package/dist/services/permissions.js +31 -9
  152. package/dist/services/revisions.js +15 -0
  153. package/dist/services/server.js +21 -4
  154. package/dist/services/settings.js +37 -3
  155. package/dist/services/users.js +13 -6
  156. package/dist/services/utils.js +6 -1
  157. package/dist/services/versions.js +137 -69
  158. package/dist/utils/calculate-field-depth.js +1 -0
  159. package/dist/utils/deep-freeze.js +24 -0
  160. package/dist/utils/extract-function-name.js +13 -0
  161. package/dist/utils/generate-translations.js +5 -5
  162. package/dist/utils/get-accountability-for-token.js +13 -1
  163. package/dist/utils/get-cache-key.js +1 -1
  164. package/dist/utils/get-history-filter-query.js +22 -0
  165. package/dist/utils/get-schema.js +2 -2
  166. package/dist/utils/get-service.js +3 -3
  167. package/dist/utils/is-admin.js +9 -0
  168. package/dist/utils/parse-oauth-scope.js +12 -0
  169. package/dist/utils/sanitize-query.js +1 -1
  170. package/dist/utils/split-field-path.js +29 -0
  171. package/dist/utils/transaction.js +2 -2
  172. package/dist/utils/translations-validation.js +2 -2
  173. package/dist/utils/validate-query.js +35 -4
  174. package/dist/utils/validate-user-count-integrity.js +28 -5
  175. package/dist/utils/verify-session-jwt.js +5 -2
  176. package/dist/utils/versioning/handle-version.js +130 -48
  177. package/dist/utils/versioning/remove-circular.js +17 -0
  178. package/dist/websocket/authenticate.js +2 -1
  179. package/dist/websocket/collab/collab.js +1 -1
  180. package/dist/websocket/collab/room.js +1 -1
  181. package/dist/websocket/controllers/base.js +12 -0
  182. package/dist/websocket/controllers/graphql.js +1 -1
  183. package/dist/websocket/handlers/subscribe.js +1 -1
  184. package/dist/websocket/messages.js +64 -64
  185. package/dist/websocket/utils/items.js +2 -2
  186. package/license +90 -80
  187. package/package.json +33 -32
  188. package/dist/controllers/mcp.js +0 -31
@@ -1,11 +1,13 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
2
  import { respond } from "../middleware/respond.js";
3
3
  import { GraphQLService } from "../services/graphql/index.js";
4
+ import is_locked_default from "../middleware/is-locked.js";
4
5
  import { parseGraphQL } from "../middleware/graphql.js";
5
6
  import { Router } from "express";
6
7
 
7
8
  //#region src/controllers/graphql.ts
8
9
  const router = Router();
10
+ router.use(is_locked_default("graphql"));
9
11
  router.use("/system", parseGraphQL, async_handler_default(async (req, res, next) => {
10
12
  const service = new GraphQLService({
11
13
  accountability: req.accountability,
@@ -1,9 +1,10 @@
1
1
  import async_handler_default from "../utils/async-handler.js";
2
2
  import { ItemsService } from "../services/items.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 { MetaService } from "../services/meta.js";
6
6
  import { validateBatch } from "../middleware/validate-batch.js";
7
+ import is_locked_default from "../middleware/is-locked.js";
7
8
  import collection_exists_default from "../middleware/collection-exists.js";
8
9
  import { ErrorCode, ForbiddenError, RouteNotFoundError, isDirectusError } from "@directus/errors";
9
10
  import express from "express";
@@ -11,6 +12,7 @@ import { isSystemCollection } from "@directus/system-data";
11
12
 
12
13
  //#region src/controllers/items.ts
13
14
  const router = express.Router();
15
+ router.use(is_locked_default("items"));
14
16
  router.post("/:collection", collection_exists_default, async_handler_default(async (req, res, next) => {
15
17
  if (isSystemCollection(req.params["collection"])) throw new ForbiddenError();
16
18
  if (req.singleton) throw new RouteNotFoundError({ path: req.path });
@@ -0,0 +1,119 @@
1
+ import async_handler_default from "../utils/async-handler.js";
2
+ import { getEntitlementManager } from "../license/entitlements/manager.js";
3
+ import { respond } from "../middleware/respond.js";
4
+ import { GRACE_PERIOD_MS, getCoreGraceExpiresAt } from "../license/utils/get-core-grace-expires-at.js";
5
+ import { getLicenseManager } from "../license/manager.js";
6
+ import "../license/index.js";
7
+ import is_admin_default from "../middleware/is-admin.js";
8
+ import { InvalidPayloadError } from "@directus/errors";
9
+ import express from "express";
10
+ import { fromZodError } from "zod-validation-error";
11
+ import { ResolveInput } from "@directus/license";
12
+
13
+ //#region src/controllers/license.ts
14
+ const router = express.Router();
15
+ router.get("/", is_admin_default, async_handler_default(async (_req, res, next) => {
16
+ const licenseManager = getLicenseManager();
17
+ const entitlementManager = getEntitlementManager();
18
+ const [license, status, downgradeReason, seatUsage, collectionUsage, flowUsage] = await Promise.all([
19
+ licenseManager.getLicense(),
20
+ licenseManager.getStatus(),
21
+ licenseManager.getDowngradeReason(),
22
+ entitlementManager.getUsage("seats"),
23
+ entitlementManager.getUsage("collections"),
24
+ entitlementManager.getUsage("flows")
25
+ ]);
26
+ const source = licenseManager.getSource();
27
+ let expiresAt = license.meta.expires_at;
28
+ let gracePeriod = license.meta.grace_period;
29
+ if (source === null && status === "grace") {
30
+ const coreGraceExpiresAt = await getCoreGraceExpiresAt();
31
+ if (coreGraceExpiresAt !== null) {
32
+ expiresAt = coreGraceExpiresAt;
33
+ gracePeriod = Math.floor(GRACE_PERIOD_MS / 1e3);
34
+ }
35
+ }
36
+ const payload = {
37
+ name: license.meta.name,
38
+ status,
39
+ source,
40
+ downgrade_reason: downgradeReason,
41
+ renews_at: license.meta.renews_at,
42
+ expires_at: expiresAt,
43
+ entitlements: license.entitlements,
44
+ grace_period: gracePeriod,
45
+ offline: license.meta.offline,
46
+ usage: {
47
+ seats: seatUsage,
48
+ collections: collectionUsage,
49
+ flows: flowUsage
50
+ }
51
+ };
52
+ res.locals["payload"] = { data: payload };
53
+ return next();
54
+ }), respond);
55
+ router.post("/", is_admin_default, async_handler_default(async (req, _res, next) => {
56
+ if (!req.body.license_key) throw new InvalidPayloadError({ reason: "A \"license_key\" is required" });
57
+ await getLicenseManager().activate(req.body.license_key);
58
+ return next();
59
+ }), respond);
60
+ router.patch("/", is_admin_default, async_handler_default(async (req, _res, next) => {
61
+ if (!req.body.license_key) throw new InvalidPayloadError({ reason: "A \"license_key\" is required" });
62
+ await getLicenseManager().update(req.body.license_key);
63
+ return next();
64
+ }), respond);
65
+ router.delete("/", is_admin_default, async_handler_default(async (_req, _res, next) => {
66
+ await getLicenseManager().deactivate();
67
+ return next();
68
+ }), respond);
69
+ router.post("/preview", async_handler_default(async (req, res, next) => {
70
+ if (!req.body.license_key) throw new InvalidPayloadError({ reason: "A \"license_key\" is required" });
71
+ const preview = await getLicenseManager().preview(req.body.license_key);
72
+ const payload = {
73
+ plan_name: preview.plan_name,
74
+ expires_at: preview.expires_at,
75
+ renews_at: preview.renews_at,
76
+ production_enabled: preview.entitlements.production_enabled.override ?? preview.entitlements.production_enabled.default
77
+ };
78
+ res.locals["payload"] = { data: payload };
79
+ return next();
80
+ }), respond);
81
+ router.get("/portal", is_admin_default, async_handler_default(async (_req, res) => {
82
+ const portal = await getLicenseManager().billingPortalUrl();
83
+ res.redirect(portal);
84
+ }));
85
+ router.get("/addons", is_admin_default, async_handler_default(async (_req, res, next) => {
86
+ const payload = await getLicenseManager().availableAddons();
87
+ res.locals["payload"] = { data: payload };
88
+ return next();
89
+ }), respond);
90
+ router.patch("/addons/:id", is_admin_default, async_handler_default(async (req, _res, next) => {
91
+ if (typeof req.body.quantity !== "number") throw new InvalidPayloadError({ reason: "A numbered \"quantity\" is required" });
92
+ await getLicenseManager().setAddonQuantity({
93
+ addonId: req.params["id"],
94
+ quantity: req.body.quantity
95
+ });
96
+ return next();
97
+ }), respond);
98
+ router.delete("/addons/:id", is_admin_default, async_handler_default(async (req, _res, next) => {
99
+ await getLicenseManager().removeAddon(req.params["id"]);
100
+ return next();
101
+ }), respond);
102
+ router.post("/pending-resolution", is_admin_default, async_handler_default(async (req, res, next) => {
103
+ const payload = await getLicenseManager().pendingResolution({
104
+ adminId: req.accountability.user,
105
+ licenseKey: req.body.license_key
106
+ });
107
+ res.locals["payload"] = { data: payload };
108
+ return next();
109
+ }), respond);
110
+ router.post("/resolve", is_admin_default, async_handler_default(async (req, _res, next) => {
111
+ const { error, data } = ResolveInput.safeParse(req.body);
112
+ if (error) throw new InvalidPayloadError({ reason: fromZodError(error).message });
113
+ await getLicenseManager().applyResolution(data, { accountability: req.accountability });
114
+ return next();
115
+ }), respond);
116
+ var license_default = router;
117
+
118
+ //#endregion
119
+ export { license_default as default };
@@ -0,0 +1,38 @@
1
+ import async_handler_default from "../../utils/async-handler.js";
2
+ import { SettingsService } from "../../services/settings.js";
3
+ import is_locked_default from "../../middleware/is-locked.js";
4
+ import { DirectusMCP } from "../../ai/mcp/server.js";
5
+ import "../../ai/mcp/index.js";
6
+ import { useEnv } from "@directus/env";
7
+ import { ForbiddenError } from "@directus/errors";
8
+ import { toBoolean } from "@directus/utils";
9
+ import { Router } from "express";
10
+
11
+ //#region src/controllers/mcp/index.ts
12
+ const router = Router();
13
+ router.use(is_locked_default("mcp"));
14
+ const mcpHandler = async_handler_default(async (req, res) => {
15
+ const env = useEnv();
16
+ const { mcp_enabled, mcp_oauth_enabled, mcp_allow_deletes, mcp_prompts_collection, mcp_system_prompt, mcp_system_prompt_enabled } = await new SettingsService({ schema: req.schema }).readSingleton({ fields: [
17
+ "mcp_enabled",
18
+ "mcp_oauth_enabled",
19
+ "mcp_allow_deletes",
20
+ "mcp_prompts_collection",
21
+ "mcp_system_prompt",
22
+ "mcp_system_prompt_enabled"
23
+ ] });
24
+ if (!mcp_enabled) throw new ForbiddenError({ reason: "MCP must be enabled" });
25
+ if (req.accountability?.oauth && (toBoolean(env["MCP_OAUTH_ENABLED"]) !== true || toBoolean(mcp_oauth_enabled) !== true)) throw new ForbiddenError({ reason: "MCP OAuth must be enabled" });
26
+ new DirectusMCP({
27
+ promptsCollection: mcp_prompts_collection,
28
+ allowDeletes: mcp_allow_deletes,
29
+ systemPromptEnabled: mcp_system_prompt_enabled,
30
+ systemPrompt: mcp_system_prompt
31
+ }).handleRequest(req, res);
32
+ });
33
+ router.get("/", mcpHandler);
34
+ router.post("/", mcpHandler);
35
+ var mcp_default = router;
36
+
37
+ //#endregion
38
+ export { mcp_default as default };
@@ -0,0 +1,68 @@
1
+ import async_handler_default from "../../utils/async-handler.js";
2
+ import { ItemsService } from "../../services/items.js";
3
+ import { respond } from "../../middleware/respond.js";
4
+ import { sanitizeQuery } from "../../utils/sanitize-query.js";
5
+ import { MetaService } from "../../services/meta.js";
6
+ import use_collection_default from "../../middleware/use-collection.js";
7
+ import { validateBatch } from "../../middleware/validate-batch.js";
8
+ import { ForbiddenError } from "@directus/errors";
9
+ import { Router } from "express";
10
+
11
+ //#region src/controllers/mcp/oauth-clients.ts
12
+ const router = Router();
13
+ router.use(use_collection_default("directus_oauth_clients"));
14
+ router.use((req, _res, next) => {
15
+ if (!req.accountability?.admin) throw new ForbiddenError();
16
+ next();
17
+ });
18
+ const readHandler = async_handler_default(async (req, res, next) => {
19
+ const service = new ItemsService("directus_oauth_clients", {
20
+ accountability: req.accountability,
21
+ schema: req.schema
22
+ });
23
+ const metaService = new MetaService({
24
+ accountability: req.accountability,
25
+ schema: req.schema
26
+ });
27
+ const records = await service.readByQuery(req.sanitizedQuery);
28
+ const meta = await metaService.getMetaForQuery("directus_oauth_clients", req.sanitizedQuery);
29
+ res.locals["payload"] = {
30
+ data: records || null,
31
+ meta
32
+ };
33
+ return next();
34
+ });
35
+ router.get("/", validateBatch("read"), readHandler, respond);
36
+ router.search("/", validateBatch("read"), readHandler, respond);
37
+ router.get("/:id", async_handler_default(async (req, res, next) => {
38
+ const record = await new ItemsService("directus_oauth_clients", {
39
+ accountability: req.accountability,
40
+ schema: req.schema
41
+ }).readOne(req.params["id"], req.sanitizedQuery);
42
+ res.locals["payload"] = { data: record || null };
43
+ return next();
44
+ }), respond);
45
+ router.delete("/", validateBatch("delete"), async_handler_default(async (req, _res, next) => {
46
+ const service = new ItemsService("directus_oauth_clients", {
47
+ accountability: req.accountability,
48
+ schema: req.schema
49
+ });
50
+ if (Array.isArray(req.body)) await service.deleteMany(req.body);
51
+ else if (req.body.keys) await service.deleteMany(req.body.keys);
52
+ else {
53
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
54
+ await service.deleteByQuery(sanitizedQuery);
55
+ }
56
+ return next();
57
+ }), respond);
58
+ router.delete("/:id", async_handler_default(async (req, _res, next) => {
59
+ await new ItemsService("directus_oauth_clients", {
60
+ accountability: req.accountability,
61
+ schema: req.schema
62
+ }).deleteOne(req.params["id"]);
63
+ return next();
64
+ }), respond);
65
+ var oauth_clients_default = router;
66
+
67
+ //#endregion
68
+ export { oauth_clients_default as default };
@@ -0,0 +1,316 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { dirname, join } from "node:path";
3
+ import { Liquid } from "liquidjs";
4
+ import { flatten } from "flat";
5
+
6
+ //#region src/controllers/mcp/oauth-consent-page.ts
7
+ const liquid = new Liquid({
8
+ root: join(dirname(fileURLToPath(import.meta.url)), "templates"),
9
+ extname: ".liquid",
10
+ outputEscape: "escape"
11
+ });
12
+ const logoDataUri = `data:image/svg+xml,${encodeURIComponent(`<svg width="64" height="39" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#FFFFFF" fill-rule="evenodd" clip-rule="evenodd" d="M51.18 24.61c-.3-.07-.55-.15-.77-.25-.16-.07-.3-.16-.42-.26a.31.31 0 0 1-.1-.27c.11-1.24-.02-2.33.1-3.56.5-5 3.64-3.41 6.46-4.23 1.62-.45 3.24-1.35 3.83-3.1.1-.3.01-.61-.19-.84a36.2 36.2 0 0 0-6.12-5.54A36.73 36.73 0 0 0 27.94.36a.46.46 0 0 0-.33.7 13.7 13.7 0 0 0 4.31 4.24c.31.19.19.6-.17.52a8.2 8.2 0 0 1-2.92-1.26.35.35 0 0 0-.33-.04l-1.64.67a.45.45 0 0 0-.12.75A13.7 13.7 0 0 0 42.8 7.3c.3-.19.8.2.7.55a27 27 0 0 0-.54 2.37c-1.26 6.37-4.9 5.87-9.4 4.27-9-3.26-14.12-.48-18.66-5.98-.31-.38-.87-.51-1.24-.19a4.25 4.25 0 0 0 .43 6.8c.14.1.33.05.44-.08.28-.35.5-.59.8-.74.3-.16.46.29.2.52-.97.85-1.25 1.87-1.88 3.87-.99 3.13-.57 6.34-5.2 7.18-2.45.12-2.4 1.78-3.29 4.25-1.03 2.98-2.39 4.3-4.9 6.9-.34.36-.36.93.01 1.25 1 .85 2.04.9 3.09.46 2.6-1.08 4.6-4.44 6.48-6.61 2.1-2.42 7.15-1.39 10.97-3.76 2.05-1.25 3.29-2.86 2.9-5.27-.07-.38.37-.62.53-.26.31.68.51 1.4.6 2.16.02.2.2.34.39.33 4.12-.23 9.46 4.3 14.44 5.53.3.08.52-.27.35-.53a9.17 9.17 0 0 1-1.3-3.02c-.1-.39.47-.5.66-.14a9.2 9.2 0 0 0 7.4 4.71c1.2.1 2.54-.05 3.93-.47 1.66-.5 3.19-1.14 5.02-.79 1.36.25 2.63.94 3.42 2.1 1.1 1.62 3.45 2.04 4.7.35a.81.81 0 0 0 .08-.8c-2.76-6.43-9.75-6.88-12.75-7.65Z"/></svg>`)}`;
13
+ const baseRules = {
14
+ borderRadius: "0.3125rem",
15
+ borderWidth: "2px",
16
+ primary: "var(--project-color)",
17
+ primaryBackground: "color-mix(in srgb, var(--theme--background), var(--theme--primary) 10%)",
18
+ primarySubdued: "color-mix(in srgb, var(--theme--background), var(--theme--primary) 50%)",
19
+ danger: "#e35169",
20
+ fonts: {
21
+ display: {
22
+ fontFamily: "\"Inter\", system-ui",
23
+ fontWeight: "700"
24
+ },
25
+ sans: {
26
+ fontFamily: "\"Inter\", system-ui",
27
+ fontWeight: "500"
28
+ },
29
+ monospace: {
30
+ fontFamily: "\"Fira Mono\", monospace",
31
+ fontWeight: "500"
32
+ }
33
+ },
34
+ form: { field: { input: {
35
+ background: "var(--theme--background)",
36
+ foreground: "var(--theme--foreground)",
37
+ foregroundSubdued: "var(--theme--foreground-subdued)",
38
+ borderColor: "var(--theme--border-color)",
39
+ borderColorHover: "var(--theme--border-color-accent)",
40
+ padding: "0.875rem"
41
+ } } },
42
+ public: {
43
+ background: "var(--theme--background)",
44
+ foreground: "var(--theme--foreground)",
45
+ foregroundAccent: "var(--theme--foreground-accent)"
46
+ }
47
+ };
48
+ const lightRules = {
49
+ ...baseRules,
50
+ foreground: "#4f5464",
51
+ foregroundAccent: "#172940",
52
+ foregroundSubdued: "#a2b5cd",
53
+ background: "#fff",
54
+ backgroundNormal: "#f0f4f9",
55
+ backgroundAccent: "#e4eaf1",
56
+ backgroundSubdued: "#f7fafc",
57
+ borderColor: "#e4eaf1",
58
+ borderColorAccent: "#d3dae4",
59
+ borderColorSubdued: "#f0f4f9",
60
+ primaryAccent: "color-mix(in srgb, var(--theme--primary), #2e3c43 25%)"
61
+ };
62
+ const darkRules = {
63
+ ...baseRules,
64
+ foreground: "#c9d1d9",
65
+ foregroundAccent: "#f0f6fc",
66
+ foregroundSubdued: "#666672",
67
+ background: "#0d1117",
68
+ backgroundNormal: "#21262e",
69
+ backgroundAccent: "#30363d",
70
+ backgroundSubdued: "#161b22",
71
+ borderColor: "#21262e",
72
+ borderColorAccent: "#30363d",
73
+ borderColorSubdued: "#21262d",
74
+ primaryAccent: "color-mix(in srgb, var(--theme--primary), #16151a 25%)"
75
+ };
76
+ function camelToKebab(str) {
77
+ return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
78
+ }
79
+ function rulesToCssVars(rules) {
80
+ const flattened = flatten(rules, { delimiter: "--" });
81
+ const result = {};
82
+ for (const [key, value] of Object.entries(flattened)) result[`--theme--${camelToKebab(key)}`] = value;
83
+ return result;
84
+ }
85
+ function varsToCss(vars) {
86
+ return Object.entries(vars).map(([key, value]) => `${key}: ${value};`).join("\n ");
87
+ }
88
+ const lightCss = varsToCss(rulesToCssVars(lightRules));
89
+ const darkCss = varsToCss(rulesToCssVars(darkRules));
90
+ /**
91
+ * Generate theme CSS with light/dark mode support.
92
+ * Inlines Directus theme tokens as CSS custom properties (same logic as @directus/themes
93
+ * `rulesToCssVars`). Uses `--project-color` as the primary color seed.
94
+ *
95
+ * @param projectColor - Hex color, already validated by `validateProjectColor`
96
+ */
97
+ function buildStyles(projectColor) {
98
+ return `
99
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
100
+
101
+ :root {
102
+ --project-color: ${projectColor};
103
+ ${lightCss}
104
+ }
105
+
106
+ [data-theme="dark"] {
107
+ ${darkCss}
108
+ }
109
+
110
+ @media (prefers-color-scheme: dark) {
111
+ :root:not([data-theme="light"]) {
112
+ ${darkCss}
113
+ }
114
+ }
115
+
116
+ body {
117
+ font-family: var(--theme--fonts--sans--font-family);
118
+ font-weight: var(--theme--fonts--sans--font-weight);
119
+ background: var(--theme--background-subdued);
120
+ color: var(--theme--public--foreground-accent);
121
+ display: flex;
122
+ justify-content: center;
123
+ align-items: center;
124
+ min-height: 100vh;
125
+ padding: 1rem;
126
+ -webkit-font-smoothing: antialiased;
127
+ }
128
+
129
+ .card {
130
+ background: var(--theme--public--background);
131
+ color: var(--theme--public--foreground);
132
+ border-radius: calc(var(--theme--border-radius) * 3);
133
+ padding: 1.5rem;
134
+ max-width: 576px;
135
+ width: 100%;
136
+ box-shadow: 0 0 40px 0 rgb(38 50 56 / 0.1);
137
+ }
138
+
139
+ @media (min-width: 480px) {
140
+ .card { padding: 2.5rem 2.5rem 2rem; }
141
+ }
142
+
143
+ .logo-row {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ gap: 0.75rem;
148
+ margin-bottom: 1.5rem;
149
+ }
150
+
151
+ .logo {
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ flex-shrink: 0;
156
+ inline-size: 3.125rem;
157
+ block-size: 3.125rem;
158
+ border-radius: calc(var(--theme--border-radius) - 0.125rem);
159
+ background: var(--theme--primary);
160
+ overflow: hidden;
161
+ }
162
+
163
+ .logo img {
164
+ object-fit: contain;
165
+ object-position: center center;
166
+ block-size: 2.25rem;
167
+ inline-size: 2.25rem;
168
+ }
169
+
170
+
171
+ h1 {
172
+ font-family: var(--theme--fonts--display--font-family);
173
+ font-weight: var(--theme--fonts--display--font-weight);
174
+ font-size: 1.25rem;
175
+ text-align: center;
176
+ margin-bottom: 1.5rem;
177
+ }
178
+
179
+ h1 strong { font-weight: 700; }
180
+
181
+ .details {
182
+ border: var(--theme--border-width) solid var(--theme--form--field--input--border-color);
183
+ border-radius: var(--theme--border-radius);
184
+ overflow: hidden;
185
+ margin-bottom: 1.25rem;
186
+ }
187
+
188
+ .details-label {
189
+ font-size: 0.75rem;
190
+ font-weight: 600;
191
+ color: var(--theme--foreground-subdued);
192
+ text-transform: uppercase;
193
+ letter-spacing: 0.05em;
194
+ padding: 0.625rem var(--theme--form--field--input--padding);
195
+ border-bottom: var(--theme--border-width) solid var(--theme--form--field--input--border-color);
196
+ background: var(--theme--background-subdued);
197
+ }
198
+
199
+ .detail-row {
200
+ display: flex;
201
+ align-items: baseline;
202
+ padding: 0.5rem var(--theme--form--field--input--padding);
203
+ font-size: 0.875rem;
204
+ border-bottom: var(--theme--border-width) solid var(--theme--border-color-subdued);
205
+ }
206
+
207
+ .detail-row:last-child { border-bottom: none; }
208
+
209
+ .detail-key {
210
+ color: var(--theme--foreground-subdued);
211
+ min-width: 7rem;
212
+ flex-shrink: 0;
213
+ }
214
+
215
+ @media (max-width: 479px) {
216
+ .detail-row { flex-direction: column; gap: 0.125rem; }
217
+ .detail-key { min-width: 0; }
218
+ }
219
+
220
+ .detail-value {
221
+ color: var(--theme--foreground-accent);
222
+ word-break: break-all;
223
+ font-family: var(--theme--fonts--monospace--font-family);
224
+ font-size: 0.8125rem;
225
+ }
226
+
227
+ .detail-value.name {
228
+ font-family: var(--theme--fonts--sans--font-family);
229
+ font-size: 0.875rem;
230
+ font-weight: 600;
231
+ }
232
+
233
+ .note {
234
+ margin-bottom: 1.5rem;
235
+ line-height: 1.6;
236
+ color: var(--theme--foreground-subdued);
237
+ font-size: 0.875rem;
238
+ }
239
+
240
+ .note strong { color: var(--theme--foreground); }
241
+
242
+ .actions {
243
+ display: flex;
244
+ justify-content: flex-end;
245
+ gap: 0.625rem;
246
+ }
247
+
248
+ button {
249
+ padding: 0.5rem 1.25rem;
250
+ border: var(--theme--border-width) solid transparent;
251
+ border-radius: var(--theme--border-radius);
252
+ font-size: 0.875rem;
253
+ font-weight: 600;
254
+ cursor: pointer;
255
+ font-family: inherit;
256
+ transition: opacity 0.15s;
257
+ }
258
+
259
+ button:hover { opacity: 0.85; }
260
+ button:active { opacity: 0.7; }
261
+
262
+ .btn-approve { background: var(--theme--primary); color: #fff; }
263
+ .btn-cancel { background: var(--theme--background); color: var(--theme--foreground); border-color: var(--theme--border-color-accent); }
264
+
265
+ .redirect-warn {
266
+ font-size: 0.75rem;
267
+ color: var(--theme--danger);
268
+ font-family: var(--theme--fonts--sans--font-family);
269
+ }`;
270
+ }
271
+ const DEFAULT_PROJECT_COLOR = "#6644ff";
272
+ function validateProjectColor(color) {
273
+ return /^#[0-9a-fA-F]{3,8}$/.test(color) ? color : DEFAULT_PROJECT_COLOR;
274
+ }
275
+ /**
276
+ * Build shared template variables from page options.
277
+ *
278
+ * Template variables:
279
+ * - `styles` (string, `| raw`): generated CSS with theme variables
280
+ * - `themeAttr` (string, `| raw`): `data-theme="dark|light"` or empty for auto
281
+ * - `logoUrl` / `logoDataUri`: custom project logo URL or default Directus logo as data URI
282
+ * - `projectName`: escaped by Liquid auto-escape, safe for text content
283
+ */
284
+ function buildTemplateData(opts) {
285
+ const customLogo = !!opts.logoUrl;
286
+ let themeAttr = "";
287
+ if (opts.appearance === "dark") themeAttr = " data-theme=\"dark\"";
288
+ else if (opts.appearance === "light") themeAttr = " data-theme=\"light\"";
289
+ const validColor = validateProjectColor(opts.projectColor);
290
+ return {
291
+ projectName: opts.projectName,
292
+ styles: buildStyles(validColor),
293
+ themeAttr,
294
+ logoClass: "logo",
295
+ logoUrl: customLogo ? opts.logoUrl : null,
296
+ logoDataUri: !customLogo ? logoDataUri : null,
297
+ logoAlt: customLogo ? opts.projectName : "Directus"
298
+ };
299
+ }
300
+ /** Render the OAuth consent approval/deny page. */
301
+ async function renderConsentPage(data, opts) {
302
+ return liquid.renderFile("oauth-consent", {
303
+ ...buildTemplateData(opts),
304
+ ...data
305
+ });
306
+ }
307
+ /** Render the OAuth error page (pre-redirect errors that can't be sent to the client). */
308
+ async function renderErrorPage(description, opts) {
309
+ return liquid.renderFile("oauth-error", {
310
+ ...buildTemplateData(opts),
311
+ description
312
+ });
313
+ }
314
+
315
+ //#endregion
316
+ export { renderConsentPage, renderErrorPage };