@directus/api 35.0.1 → 35.0.2

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.
@@ -1,4 +1,5 @@
1
1
  import "../../../packages/types/dist/index.js";
2
+ import { coerceJsonFields } from "../../tools/utils.js";
2
3
  import { ALL_TOOLS } from "../../tools/index.js";
3
4
  import { InvalidPayloadError } from "@directus/errors";
4
5
  import { jsonSchema, tool, zodSchema } from "ai";
@@ -19,7 +20,8 @@ const chatRequestToolToAiSdkTool = ({ chatRequestTool, accountability, schema, t
19
20
  inputSchema,
20
21
  needsApproval,
21
22
  execute: async (rawArgs) => {
22
- const { error, data: args } = directusTool.validateSchema?.safeParse(rawArgs) ?? { data: rawArgs };
23
+ const coercedArgs = coerceJsonFields(rawArgs);
24
+ const { error, data: args } = directusTool.validateSchema?.safeParse(coercedArgs) ?? { data: coercedArgs };
23
25
  if (error) {
24
26
  throw new InvalidPayloadError({ reason: fromZodError(error).message });
25
27
  }
@@ -1,11 +1,12 @@
1
1
  import { ItemsService } from "../../services/items.js";
2
+ import { coerceJsonFields } from "../tools/utils.js";
2
3
  import { Url } from "../../utils/url.js";
3
4
  import "../../services/index.js";
4
5
  import { findMcpTool, getAllMcpTools } from "../tools/index.js";
5
6
  import { DirectusTransport } from "./transport.js";
6
7
  import { useEnv } from "@directus/env";
7
8
  import { ForbiddenError, InvalidPayloadError, isDirectusError } from "@directus/errors";
8
- import { isObject, parseJSON, toArray } from "@directus/utils";
9
+ import { isObject, toArray } from "@directus/utils";
9
10
  import { fromZodError } from "zod-validation-error";
10
11
  import { z } from "zod";
11
12
  import { render, tokenize } from "micromustache";
@@ -166,19 +167,8 @@ var DirectusMCP = class {
166
167
  if (tool.name === "system-prompt") {
167
168
  request.params.arguments = { promptOverride: this.systemPrompt };
168
169
  }
169
- if (request.params.arguments) {
170
- for (const field of [
171
- "data",
172
- "keys",
173
- "query"
174
- ]) {
175
- const arg = request.params.arguments[field];
176
- if (typeof arg === "string") {
177
- request.params.arguments[field] = parseJSON(arg);
178
- }
179
- }
180
- }
181
- const { error, data: args } = tool.validateSchema?.safeParse(request.params.arguments) ?? { data: request.params.arguments };
170
+ const coercedArgs = request.params.arguments ? coerceJsonFields(request.params.arguments) : request.params.arguments;
171
+ const { error, data: args } = tool.validateSchema?.safeParse(coercedArgs) ?? { data: coercedArgs };
182
172
  if (error) {
183
173
  throw new InvalidPayloadError({ reason: fromZodError(error).message });
184
174
  }
@@ -1,6 +1,28 @@
1
1
  import { sanitizeQuery } from "../../utils/sanitize-query.js";
2
+ import { parseJSON } from "@directus/utils";
2
3
 
3
4
  //#region src/ai/tools/utils.ts
5
+ const JSON_COERCE_FIELDS = [
6
+ "data",
7
+ "keys",
8
+ "query",
9
+ "headers"
10
+ ];
11
+ /**
12
+ * LLMs sometimes return object/array arguments as stringified JSON.
13
+ * Coerce known fields back to native values before validation.
14
+ */
15
+ function coerceJsonFields(args) {
16
+ const coerced = { ...args };
17
+ for (const field of JSON_COERCE_FIELDS) {
18
+ if (typeof coerced[field] === "string") {
19
+ try {
20
+ coerced[field] = parseJSON(coerced[field]);
21
+ } catch {}
22
+ }
23
+ }
24
+ return coerced;
25
+ }
4
26
  /**
5
27
  * Build a sanitized query object from a tool's args payload.
6
28
  * - Ensures fields defaults to '*' when not provided
@@ -19,4 +41,4 @@ async function buildSanitizedQueryFromArgs(args, schema, accountability) {
19
41
  }
20
42
 
21
43
  //#endregion
22
- export { buildSanitizedQueryFromArgs };
44
+ export { buildSanitizedQueryFromArgs, coerceJsonFields };
@@ -24,6 +24,7 @@ const router = Router();
24
24
  const env = useEnv();
25
25
  router.use(use_collection_default("directus_files"));
26
26
  router.post("/folder/:pk", async_handler_default(async (req, res) => {
27
+ const logger = useLogger();
27
28
  const service = new AssetsService({
28
29
  accountability: req.accountability,
29
30
  schema: req.schema
@@ -32,10 +33,33 @@ router.post("/folder/:pk", async_handler_default(async (req, res) => {
32
33
  res.setHeader("Content-Type", "application/zip");
33
34
  const folderName = `folder-${metadata["name"] ? metadata["name"] : "unknown"}-${getDateTimeFormatted()}.zip`;
34
35
  res.setHeader("Content-Disposition", contentDisposition(folderName, { type: "attachment" }));
36
+ res.on("close", () => {
37
+ if (!res.writableEnded) {
38
+ archive.destroy();
39
+ archive.abort();
40
+ }
41
+ });
35
42
  archive.pipe(res);
36
- await complete();
43
+ try {
44
+ await complete();
45
+ } catch (error) {
46
+ logger.error(error, `Couldn't archive folder ${req.params["pk"]} to the client`);
47
+ archive.destroy();
48
+ if (!res.headersSent) {
49
+ res.removeHeader("Content-Type");
50
+ res.removeHeader("Content-Disposition");
51
+ res.removeHeader("Cache-Control");
52
+ res.status(500).json({ errors: [{
53
+ message: "An unexpected error occurred.",
54
+ extensions: { code: "INTERNAL_SERVER_ERROR" }
55
+ }] });
56
+ } else {
57
+ res.end();
58
+ }
59
+ }
37
60
  }));
38
61
  router.post("/files/", async_handler_default(async (req, res) => {
62
+ const logger = useLogger();
39
63
  const service = new AssetsService({
40
64
  accountability: req.accountability,
41
65
  schema: req.schema
@@ -47,8 +71,30 @@ router.post("/files/", async_handler_default(async (req, res) => {
47
71
  const { archive, complete } = await service.zipFiles(data.ids);
48
72
  res.setHeader("Content-Type", "application/zip");
49
73
  res.setHeader("Content-Disposition", `attachment; filename="files-${getDateTimeFormatted()}.zip"`);
74
+ res.on("close", () => {
75
+ if (!res.writableEnded) {
76
+ archive.destroy();
77
+ archive.abort();
78
+ }
79
+ });
50
80
  archive.pipe(res);
51
- await complete();
81
+ try {
82
+ await complete();
83
+ } catch (error$1) {
84
+ logger.error(error$1, `Couldn't archive files to the client`);
85
+ archive.destroy();
86
+ if (!res.headersSent) {
87
+ res.removeHeader("Content-Type");
88
+ res.removeHeader("Content-Disposition");
89
+ res.removeHeader("Cache-Control");
90
+ res.status(500).json({ errors: [{
91
+ message: "An unexpected error occurred.",
92
+ extensions: { code: "INTERNAL_SERVER_ERROR" }
93
+ }] });
94
+ } else {
95
+ res.end();
96
+ }
97
+ }
52
98
  }));
53
99
  router.get("/:pk/:filename?", async_handler_default(async (req, res, next) => {
54
100
  const payloadService = new PayloadService("directus_settings", { schema: req.schema });
@@ -183,8 +229,15 @@ router.get("/:pk/:filename?", async_handler_default(async (req, res, next) => {
183
229
  res.setHeader("Content-Length", stat.size);
184
230
  return res.end();
185
231
  }
186
- (await stream()).on("error", (error) => {
232
+ const sourceStream = await stream();
233
+ res.on("close", () => {
234
+ if (!res.writableEnded) {
235
+ sourceStream.destroy();
236
+ }
237
+ });
238
+ sourceStream.on("error", (error) => {
187
239
  logger.error(error, `Couldn't stream file ${file.id} to the client`);
240
+ sourceStream.destroy();
188
241
  if (!res.headersSent) {
189
242
  res.removeHeader("Content-Type");
190
243
  res.removeHeader("Content-Disposition");
@@ -59,6 +59,7 @@ var AssetsService = class {
59
59
  const deduper = new NameDeduper();
60
60
  const storage = await getStorage();
61
61
  for (const { id, folder, filename_download } of options.files) {
62
+ if (archive.destroyed) break;
62
63
  const file = await this.sudoFilesService.readOne(id, { fields: [
63
64
  "id",
64
65
  "storage",
@@ -84,6 +85,7 @@ var AssetsService = class {
84
85
  }
85
86
  if (options.folders) {
86
87
  for (const [, folder] of options.folders) {
88
+ if (archive.destroyed) break;
87
89
  archive.append("", { name: folder + "/" });
88
90
  }
89
91
  }
@@ -14,7 +14,7 @@ import { verifyJWT } from "../utils/jwt.js";
14
14
  import { stall } from "../utils/stall.js";
15
15
  import { SettingsService } from "./settings.js";
16
16
  import { useEnv } from "@directus/env";
17
- import { ForbiddenError, InvalidPayloadError, RecordNotUniqueError } from "@directus/errors";
17
+ import { ForbiddenError, InvalidInviteError, InvalidPayloadError, RecordNotUniqueError } from "@directus/errors";
18
18
  import { getSimpleHash, toArray, validatePayload } from "@directus/utils";
19
19
  import { isEmpty } from "lodash-es";
20
20
  import { performance } from "perf_hooks";
@@ -314,7 +314,7 @@ var UsersService = class UsersService extends ItemsService {
314
314
  if (scope !== "invite") throw new ForbiddenError();
315
315
  const user = await this.getUserByEmail(email);
316
316
  if (user?.status !== "invited") {
317
- throw new InvalidPayloadError({ reason: `Email address ${email} hasn't been invited` });
317
+ throw new InvalidInviteError();
318
318
  }
319
319
  const service = new UsersService({
320
320
  knex: this.knex,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "35.0.1",
3
+ "version": "35.0.2",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -122,7 +122,7 @@
122
122
  "knex": "3.1.0",
123
123
  "ldapts": "8.1.3",
124
124
  "liquidjs": "10.25.0",
125
- "lodash-es": "4.17.23",
125
+ "lodash-es": "4.18.1",
126
126
  "marked": "16.4.1",
127
127
  "micromustache": "8.0.3",
128
128
  "mime-types": "3.0.1",
@@ -151,7 +151,7 @@
151
151
  "rate-limiter-flexible": "7.2.0",
152
152
  "rolldown": "1.0.0-beta.31",
153
153
  "rollup": "4.59.0",
154
- "samlify": "2.11.0",
154
+ "samlify": "2.12.0",
155
155
  "sanitize-filename": "1.6.3",
156
156
  "sanitize-html": "2.17.0",
157
157
  "sharp": "0.34.5",
@@ -166,30 +166,30 @@
166
166
  "zod": "4.1.12",
167
167
  "zod-validation-error": "4.0.2",
168
168
  "@directus/ai": "1.3.1",
169
- "@directus/constants": "14.3.0",
170
- "@directus/app": "15.7.0",
169
+ "@directus/app": "15.8.0",
171
170
  "@directus/env": "5.7.1",
172
- "@directus/extensions": "3.0.23",
173
- "@directus/errors": "2.3.0",
174
- "@directus/extensions-registry": "3.0.23",
171
+ "@directus/constants": "14.3.0",
172
+ "@directus/errors": "2.3.1",
173
+ "@directus/extensions-registry": "3.0.24",
175
174
  "@directus/format-title": "12.1.2",
176
- "@directus/memory": "3.1.6",
177
- "@directus/extensions-sdk": "17.1.1",
175
+ "@directus/extensions": "3.0.23",
176
+ "@directus/memory": "3.1.7",
178
177
  "@directus/pressure": "3.0.21",
178
+ "@directus/extensions-sdk": "17.1.2",
179
179
  "@directus/schema": "13.0.7",
180
180
  "@directus/schema-builder": "0.0.18",
181
181
  "@directus/specs": "13.0.0",
182
182
  "@directus/storage": "12.0.4",
183
- "@directus/storage-driver-azure": "12.0.21",
184
- "@directus/storage-driver-cloudinary": "12.0.21",
185
183
  "@directus/storage-driver-gcs": "12.0.21",
184
+ "@directus/storage-driver-cloudinary": "12.0.21",
185
+ "@directus/storage-driver-azure": "12.0.21",
186
+ "@directus/storage-driver-local": "12.0.4",
186
187
  "@directus/storage-driver-s3": "12.1.7",
187
188
  "@directus/storage-driver-supabase": "3.0.21",
188
- "@directus/utils": "13.4.0",
189
- "@directus/storage-driver-local": "12.0.4",
190
189
  "@directus/system-data": "4.4.0",
191
- "directus": "11.17.1",
192
- "@directus/validation": "2.0.21"
190
+ "@directus/validation": "2.0.22",
191
+ "directus": "11.17.2",
192
+ "@directus/utils": "13.4.0"
193
193
  },
194
194
  "devDependencies": {
195
195
  "@directus/tsconfig": "4.0.0",