@directus/api 13.0.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,10 +4,12 @@ import * as dateHelpers from './date/index.js';
4
4
  import * as fnHelpers from './fn/index.js';
5
5
  import * as geometryHelpers from './geometry/index.js';
6
6
  import * as schemaHelpers from './schema/index.js';
7
+ import * as sequenceHelpers from './sequence/index.js';
7
8
  export declare function getHelpers(database: Knex): {
8
9
  date: dateHelpers.mysql | dateHelpers.postgres | dateHelpers.mssql | dateHelpers.sqlite | dateHelpers.oracle;
9
10
  st: geometryHelpers.mysql | geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
10
11
  schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle;
12
+ sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
11
13
  };
12
14
  export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.mysql | fnHelpers.postgres | fnHelpers.mssql | fnHelpers.sqlite | fnHelpers.oracle;
13
15
  export type Helpers = ReturnType<typeof getHelpers>;
@@ -3,12 +3,14 @@ import * as dateHelpers from './date/index.js';
3
3
  import * as fnHelpers from './fn/index.js';
4
4
  import * as geometryHelpers from './geometry/index.js';
5
5
  import * as schemaHelpers from './schema/index.js';
6
+ import * as sequenceHelpers from './sequence/index.js';
6
7
  export function getHelpers(database) {
7
8
  const client = getDatabaseClient(database);
8
9
  return {
9
10
  date: new dateHelpers[client](database),
10
11
  st: new geometryHelpers[client](database),
11
12
  schema: new schemaHelpers[client](database),
13
+ sequence: new sequenceHelpers[client](database),
12
14
  };
13
15
  }
14
16
  export function getFunctions(database, schema) {
@@ -0,0 +1,3 @@
1
+ import { AutoSequenceHelper } from '../types.js';
2
+ export declare class AutoIncrementHelperDefault extends AutoSequenceHelper {
3
+ }
@@ -0,0 +1,3 @@
1
+ import { AutoSequenceHelper } from '../types.js';
2
+ export class AutoIncrementHelperDefault extends AutoSequenceHelper {
3
+ }
@@ -0,0 +1,9 @@
1
+ import type { Knex } from 'knex';
2
+ import { AutoSequenceHelper } from '../types.js';
3
+ export declare class AutoIncrementHelperPostgres extends AutoSequenceHelper {
4
+ /**
5
+ * Resets the auto increment sequence for a table based on the max value of the PK column.
6
+ * The sequence name of determined using a sub query.
7
+ */
8
+ resetAutoIncrementSequence(table: string, column: string): Promise<Knex.Raw | void>;
9
+ }
@@ -0,0 +1,10 @@
1
+ import { AutoSequenceHelper } from '../types.js';
2
+ export class AutoIncrementHelperPostgres extends AutoSequenceHelper {
3
+ /**
4
+ * Resets the auto increment sequence for a table based on the max value of the PK column.
5
+ * The sequence name of determined using a sub query.
6
+ */
7
+ async resetAutoIncrementSequence(table, column) {
8
+ return await this.knex.raw(`WITH sequence_infos AS (SELECT pg_get_serial_sequence('${table}', '${column}') AS seq_name, MAX(${column}) as max_val FROM ${table}) SELECT SETVAL(seq_name, max_val) FROM sequence_infos;`);
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ export { AutoIncrementHelperPostgres as postgres } from './dialects/postgres.js';
2
+ export { AutoIncrementHelperDefault as mysql } from './dialects/default.js';
3
+ export { AutoIncrementHelperDefault as cockroachdb } from './dialects/default.js';
4
+ export { AutoIncrementHelperDefault as redshift } from './dialects/default.js';
5
+ export { AutoIncrementHelperDefault as oracle } from './dialects/default.js';
6
+ export { AutoIncrementHelperDefault as sqlite } from './dialects/default.js';
7
+ export { AutoIncrementHelperDefault as mssql } from './dialects/default.js';
@@ -0,0 +1,7 @@
1
+ export { AutoIncrementHelperPostgres as postgres } from './dialects/postgres.js';
2
+ export { AutoIncrementHelperDefault as mysql } from './dialects/default.js';
3
+ export { AutoIncrementHelperDefault as cockroachdb } from './dialects/default.js';
4
+ export { AutoIncrementHelperDefault as redshift } from './dialects/default.js';
5
+ export { AutoIncrementHelperDefault as oracle } from './dialects/default.js';
6
+ export { AutoIncrementHelperDefault as sqlite } from './dialects/default.js';
7
+ export { AutoIncrementHelperDefault as mssql } from './dialects/default.js';
@@ -0,0 +1,5 @@
1
+ import type { Knex } from 'knex';
2
+ import { DatabaseHelper } from '../types.js';
3
+ export declare class AutoSequenceHelper extends DatabaseHelper {
4
+ resetAutoIncrementSequence(_table: string, _column: string): Promise<Knex.Raw | void>;
5
+ }
@@ -0,0 +1,6 @@
1
+ import { DatabaseHelper } from '../types.js';
2
+ export class AutoSequenceHelper extends DatabaseHelper {
3
+ async resetAutoIncrementSequence(_table, _column) {
4
+ return;
5
+ }
6
+ }
@@ -43,6 +43,14 @@ export default function getDatabase() {
43
43
  requiredEnvVars.push('DB_CONNECTION_STRING');
44
44
  }
45
45
  break;
46
+ case 'mysql':
47
+ if (!env['DB_SOCKET_PATH']) {
48
+ requiredEnvVars.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
49
+ }
50
+ else {
51
+ requiredEnvVars.push('DB_DATABASE', 'DB_USER', 'DB_PASSWORD', 'DB_SOCKET_PATH');
52
+ }
53
+ break;
46
54
  case 'mssql':
47
55
  if (!env['DB_TYPE'] || env['DB_TYPE'] === 'default') {
48
56
  requiredEnvVars.push('DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USER', 'DB_PASSWORD');
@@ -1,28 +1,57 @@
1
+ import { createInspector } from '@directus/schema';
2
+ import logger from '../../logger.js';
3
+ import { getHelpers } from '../helpers/index.js';
1
4
  export async function up(knex) {
5
+ const helper = getHelpers(knex).schema;
6
+ const isMysql = helper.isOneOfClients(['mysql']);
7
+ if (isMysql) {
8
+ await dropConstraint(knex);
9
+ }
2
10
  await knex.schema.alterTable('directus_shares', (table) => {
3
- if (knex.client.constructor.name === 'Client_MySQL') {
4
- // Temporary drop foreign key constraint, see https://github.com/directus/directus/issues/19399
5
- table.dropForeign('collection', 'directus_shares_collection_foreign');
6
- }
7
11
  table.dropNullable('collection');
8
- if (knex.client.constructor.name === 'Client_MySQL') {
9
- // Recreate foreign key constraint, from 20211211A-add-shares.ts
10
- table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
11
- }
12
12
  table.dropNullable('item');
13
13
  });
14
+ if (isMysql) {
15
+ await recreateConstraint(knex);
16
+ }
14
17
  }
15
18
  export async function down(knex) {
19
+ const helper = getHelpers(knex).schema;
20
+ const isMysql = helper.isOneOfClients(['mysql']);
21
+ if (isMysql) {
22
+ await dropConstraint(knex);
23
+ }
16
24
  await knex.schema.alterTable('directus_shares', (table) => {
17
- if (knex.client.constructor.name === 'Client_MySQL') {
18
- // Temporary drop foreign key constraint, see https://github.com/directus/directus/issues/19399
19
- table.dropForeign('collection', 'directus_shares_collection_foreign');
20
- }
21
25
  table.setNullable('collection');
22
- if (knex.client.constructor.name === 'Client_MySQL') {
23
- // Recreate foreign key constraint, from 20211211A-add-shares.ts
24
- table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
25
- }
26
26
  table.setNullable('item');
27
27
  });
28
+ if (isMysql) {
29
+ await recreateConstraint(knex);
30
+ }
31
+ }
32
+ /**
33
+ * Temporarily drop foreign key constraint for MySQL instances, see https://github.com/directus/directus/issues/19399
34
+ */
35
+ async function dropConstraint(knex) {
36
+ const inspector = createInspector(knex);
37
+ const foreignKeys = await inspector.foreignKeys('directus_shares');
38
+ const collectionForeignKeys = foreignKeys.filter((fk) => fk.column === 'collection');
39
+ const constraintName = collectionForeignKeys[0]?.constraint_name;
40
+ if (constraintName && collectionForeignKeys.length === 1) {
41
+ await knex.schema.alterTable('directus_shares', (table) => {
42
+ table.dropForeign('collection', constraintName);
43
+ });
44
+ }
45
+ else {
46
+ logger.warn(`Unexpected number of foreign key constraints on 'directus_shares.collection':`);
47
+ logger.warn(JSON.stringify(collectionForeignKeys, null, 4));
48
+ }
49
+ }
50
+ /**
51
+ * Recreate foreign key constraint for MySQL instances, from 20211211A-add-shares.ts
52
+ */
53
+ async function recreateConstraint(knex) {
54
+ return knex.schema.alterTable('directus_shares', async (table) => {
55
+ table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
56
+ });
28
57
  }
package/dist/env.d.ts CHANGED
@@ -12,4 +12,4 @@ export declare const getEnv: () => Record<string, any>;
12
12
  * When changes have been made during runtime, like in the CLI, we can refresh the env object with
13
13
  * the newly created variables
14
14
  */
15
- export declare function refreshEnv(): void;
15
+ export declare function refreshEnv(): Promise<void>;
package/dist/env.js CHANGED
@@ -3,11 +3,14 @@
3
3
  * For all possible keys, see: https://docs.directus.io/self-hosted/config-options/
4
4
  */
5
5
  import { parseJSON, toArray } from '@directus/utils';
6
+ import { JAVASCRIPT_FILE_EXTS } from '@directus/constants';
6
7
  import dotenv from 'dotenv';
7
8
  import fs from 'fs';
8
9
  import { clone, toNumber, toString } from 'lodash-es';
9
10
  import { createRequire } from 'node:module';
11
+ import { pathToFileURL } from 'node:url';
10
12
  import path from 'path';
13
+ import getModuleDefault from './utils/get-module-default.js';
11
14
  import { requireYAML } from './utils/require-yaml.js';
12
15
  import { toBoolean } from './utils/to-boolean.js';
13
16
  const require = createRequire(import.meta.url);
@@ -317,7 +320,7 @@ const typeMap = {
317
320
  let env = {
318
321
  ...defaults,
319
322
  ...process.env,
320
- ...processConfiguration(),
323
+ ...(await processConfiguration()),
321
324
  };
322
325
  process.env = env;
323
326
  env = processValues(env);
@@ -330,30 +333,30 @@ export const getEnv = () => env;
330
333
  * When changes have been made during runtime, like in the CLI, we can refresh the env object with
331
334
  * the newly created variables
332
335
  */
333
- export function refreshEnv() {
336
+ export async function refreshEnv() {
334
337
  env = {
335
338
  ...defaults,
336
339
  ...process.env,
337
- ...processConfiguration(),
340
+ ...(await processConfiguration()),
338
341
  };
339
342
  process.env = env;
340
343
  env = processValues(env);
341
344
  }
342
- function processConfiguration() {
345
+ async function processConfiguration() {
343
346
  const configPath = path.resolve(process.env['CONFIG_PATH'] || defaults['CONFIG_PATH']);
344
347
  if (fs.existsSync(configPath) === false)
345
348
  return {};
346
- const fileExt = path.extname(configPath).toLowerCase();
347
- if (fileExt === '.js') {
348
- const module = require(configPath);
349
- const exported = module.default || module;
350
- if (typeof exported === 'function') {
351
- return exported(process.env);
349
+ const fileExt = path.extname(configPath).toLowerCase().substring(1);
350
+ if (JAVASCRIPT_FILE_EXTS.includes(fileExt)) {
351
+ const data = await import(pathToFileURL(configPath).toString());
352
+ const config = getModuleDefault(data);
353
+ if (typeof config === 'function') {
354
+ return config(process.env);
352
355
  }
353
- else if (typeof exported === 'object') {
354
- return exported;
356
+ else if (typeof config === 'object') {
357
+ return config;
355
358
  }
356
- throw new Error(`Invalid JS configuration file export type. Requires one of "function", "object", received: "${typeof exported}"`);
359
+ throw new Error(`Invalid JS configuration file export type. Requires one of "function", "object", received: "${typeof config}"`);
357
360
  }
358
361
  if (fileExt === '.json') {
359
362
  return require(configPath);
@@ -60,6 +60,9 @@ export class ActivityService extends ItemsService {
60
60
  comment = comment.replace(new RegExp(mention, 'gm'), userPreviews[uuid] ?? '@Unknown User');
61
61
  }
62
62
  comment = `> ${comment.replace(/\n+/gm, '\n> ')}`;
63
+ const href = new Url(env['PUBLIC_URL'])
64
+ .addPath('admin', 'content', data['collection'], data['item'])
65
+ .toString();
63
66
  const message = `
64
67
  Hello ${userName(user)},
65
68
 
@@ -67,9 +70,7 @@ ${userName(sender)} has mentioned you in a comment:
67
70
 
68
71
  ${comment}
69
72
 
70
- <a href="${new Url(env['PUBLIC_URL'])
71
- .addPath('admin', 'content', data['collection'], data['item'])
72
- .toString()}">Click here to view.</a>
73
+ <a href="${href}">Click here to view.</a>
73
74
  `;
74
75
  await this.notificationsService.createOne({
75
76
  recipient: userID,
@@ -129,7 +129,23 @@ export class AssetsService {
129
129
  logger.error(e, `Couldn't transform file ${file.id}`);
130
130
  readStream.unpipe(transformer);
131
131
  });
132
- await storage.location(file.storage).write(assetFilename, readStream.pipe(transformer), type);
132
+ try {
133
+ await storage.location(file.storage).write(assetFilename, readStream.pipe(transformer), type);
134
+ }
135
+ catch (error) {
136
+ try {
137
+ await storage.location(file.storage).delete(assetFilename);
138
+ }
139
+ catch {
140
+ // Ignored to prevent original error from being overwritten
141
+ }
142
+ if (error?.message?.includes('timeout')) {
143
+ throw new ServiceUnavailableError({ service: 'assets', reason: `Transformation timed out` });
144
+ }
145
+ else {
146
+ throw error;
147
+ }
148
+ }
133
149
  return {
134
150
  stream: await storage.location(file.storage).read(assetFilename, range),
135
151
  stat: await storage.location(file.storage).stat(assetFilename),
@@ -5,7 +5,9 @@ import exif from 'exif-reader';
5
5
  import { parse as parseIcc } from 'icc';
6
6
  import { clone, pick } from 'lodash-es';
7
7
  import { extension } from 'mime-types';
8
+ import { PassThrough as PassThroughStream, Transform as TransformStream } from 'node:stream';
8
9
  import { pipeline } from 'node:stream/promises';
10
+ import zlib from 'node:zlib';
9
11
  import path from 'path';
10
12
  import sharp from 'sharp';
11
13
  import url from 'url';
@@ -209,6 +211,7 @@ export class FilesService extends ItemsService {
209
211
  const axios = await getAxios();
210
212
  fileResponse = await axios.get(encodeURL(importURL), {
211
213
  responseType: 'stream',
214
+ decompress: false,
212
215
  });
213
216
  }
214
217
  catch (err) {
@@ -227,7 +230,7 @@ export class FilesService extends ItemsService {
227
230
  title: formatTitle(filename),
228
231
  ...(body || {}),
229
232
  };
230
- return await this.uploadOne(fileResponse.data, payload);
233
+ return await this.uploadOne(decompressResponse(fileResponse.data, fileResponse.headers), payload);
231
234
  }
232
235
  /**
233
236
  * Create a file (only applicable when it is not a multipart/data POST request)
@@ -267,3 +270,57 @@ export class FilesService extends ItemsService {
267
270
  return keys;
268
271
  }
269
272
  }
273
+ function decompressResponse(stream, headers) {
274
+ const contentEncoding = (headers['content-encoding'] || '').toLowerCase();
275
+ if (!['gzip', 'deflate', 'br'].includes(contentEncoding)) {
276
+ return stream;
277
+ }
278
+ let isEmpty = true;
279
+ const checker = new TransformStream({
280
+ transform(data, _encoding, callback) {
281
+ if (isEmpty === false) {
282
+ callback(null, data);
283
+ return;
284
+ }
285
+ isEmpty = false;
286
+ handleContentEncoding(data);
287
+ callback(null, data);
288
+ },
289
+ flush(callback) {
290
+ callback();
291
+ },
292
+ });
293
+ const finalStream = new PassThroughStream({
294
+ autoDestroy: false,
295
+ destroy(error, callback) {
296
+ stream.destroy();
297
+ callback(error);
298
+ },
299
+ });
300
+ stream.pipe(checker);
301
+ return finalStream;
302
+ function handleContentEncoding(data) {
303
+ let decompressStream;
304
+ if (contentEncoding === 'br') {
305
+ decompressStream = zlib.createBrotliDecompress();
306
+ }
307
+ else if (contentEncoding === 'deflate' && isDeflateAlgorithm(data)) {
308
+ decompressStream = zlib.createInflateRaw();
309
+ }
310
+ else {
311
+ decompressStream = zlib.createUnzip();
312
+ }
313
+ decompressStream.once('error', (error) => {
314
+ if (isEmpty && !stream.readable) {
315
+ finalStream.end();
316
+ return;
317
+ }
318
+ finalStream.destroy(error);
319
+ });
320
+ checker.pipe(decompressStream).pipe(finalStream);
321
+ }
322
+ function isDeflateAlgorithm(data) {
323
+ const DEFLATE_ALGORITHM_HEADER = 0x08;
324
+ return data.length > 0 && (data[0] & DEFLATE_ALGORITHM_HEADER) === 0;
325
+ }
326
+ }
@@ -17,9 +17,12 @@ import env from '../env.js';
17
17
  import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '../errors/index.js';
18
18
  import logger from '../logger.js';
19
19
  import { getDateFormatted } from '../utils/get-date-formatted.js';
20
+ import { userName } from '../utils/user-name.js';
20
21
  import { FilesService } from './files.js';
21
22
  import { ItemsService } from './items.js';
22
23
  import { NotificationsService } from './notifications.js';
24
+ import { UsersService } from './users.js';
25
+ import { Url } from '../utils/url.js';
23
26
  export class ImportService {
24
27
  knex;
25
28
  accountability;
@@ -226,10 +229,23 @@ export class ExportService {
226
229
  accountability: this.accountability,
227
230
  schema: this.schema,
228
231
  });
232
+ const usersService = new UsersService({
233
+ schema: this.schema,
234
+ });
235
+ const user = await usersService.readOne(this.accountability.user, {
236
+ fields: ['first_name', 'last_name', 'email'],
237
+ });
238
+ const href = new Url(env['PUBLIC_URL']).addPath('admin', 'files', savedFile).toString();
239
+ const message = `
240
+ Hello ${userName(user)},
241
+
242
+ Your export of ${collection} is ready. <a href="${href}">Click here to view.</a>
243
+ `;
229
244
  await notificationsService.createOne({
230
245
  recipient: this.accountability.user,
231
246
  sender: this.accountability.user,
232
247
  subject: `Your export of ${collection} is ready`,
248
+ message,
233
249
  collection: `directus_files`,
234
250
  item: savedFile,
235
251
  });
@@ -116,8 +116,20 @@ export class ItemsService {
116
116
  const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
117
117
  const payloadWithoutAliases = pick(payloadWithA2O, without(fields, ...aliases));
118
118
  const payloadWithTypeCasting = await payloadService.processValues('create', payloadWithoutAliases);
119
- // In case of manual string / UUID primary keys, the PK already exists in the object we're saving.
119
+ // The primary key can already exist in the payload.
120
+ // In case of manual string / UUID primary keys it's always provided at this point.
121
+ // In case of an integer primary key, it might be provided as the user can specify the value manually.
120
122
  let primaryKey = payloadWithTypeCasting[primaryKeyField];
123
+ // If a PK of type number was provided, although the PK is set the auto_increment,
124
+ // depending on the database, the sequence might need to be reset to protect future PK collisions.
125
+ let autoIncrementSequenceNeedsToBeReset = false;
126
+ const pkField = this.schema.collections[this.collection].fields[primaryKeyField];
127
+ if (primaryKey &&
128
+ !opts.bypassAutoIncrementSequenceReset &&
129
+ pkField.type === 'integer' &&
130
+ pkField.defaultValue === 'AUTO_INCREMENT') {
131
+ autoIncrementSequenceNeedsToBeReset = true;
132
+ }
121
133
  try {
122
134
  const result = await trx
123
135
  .insert(payloadWithoutAliases)
@@ -125,7 +137,7 @@ export class ItemsService {
125
137
  .returning(primaryKeyField)
126
138
  .then((result) => result[0]);
127
139
  const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
128
- if (this.schema.collections[this.collection].fields[primaryKeyField].type === 'uuid') {
140
+ if (pkField.type === 'uuid') {
129
141
  primaryKey = getHelpers(trx).schema.formatUUID(primaryKey ?? returnedKey);
130
142
  }
131
143
  else {
@@ -146,6 +158,8 @@ export class ItemsService {
146
158
  // to read from it
147
159
  payload[primaryKeyField] = primaryKey;
148
160
  }
161
+ // At this point, the primary key is guaranteed to be set.
162
+ primaryKey = primaryKey;
149
163
  const { revisions: revisionsO2M, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(payloadWithPresets, primaryKey, opts);
150
164
  nestedActionEvents.push(...nestedActionEventsM2O);
151
165
  nestedActionEvents.push(...nestedActionEventsA2O);
@@ -189,6 +203,9 @@ export class ItemsService {
189
203
  }
190
204
  }
191
205
  }
206
+ if (autoIncrementSequenceNeedsToBeReset) {
207
+ await getHelpers(trx).sequence.resetAutoIncrementSequence(this.collection, primaryKeyField);
208
+ }
192
209
  return primaryKey;
193
210
  });
194
211
  if (opts.emitEvents !== false) {
@@ -241,12 +258,20 @@ export class ItemsService {
241
258
  });
242
259
  const primaryKeys = [];
243
260
  const nestedActionEvents = [];
244
- for (const payload of data) {
261
+ const pkField = this.schema.collections[this.collection].primary;
262
+ for (const [index, payload] of data.entries()) {
263
+ let bypassAutoIncrementSequenceReset = true;
264
+ // the auto_increment sequence needs to be reset if the current item contains a manual PK and
265
+ // if it's the last item of the batch or if the next item doesn't include a PK and hence one needs to be generated
266
+ if (payload[pkField] && (index === data.length - 1 || !data[index + 1]?.[pkField])) {
267
+ bypassAutoIncrementSequenceReset = false;
268
+ }
245
269
  const primaryKey = await service.createOne(payload, {
246
270
  ...(opts || {}),
247
271
  autoPurgeCache: false,
248
272
  bypassEmitAction: (params) => nestedActionEvents.push(params),
249
273
  mutationTracker: opts.mutationTracker,
274
+ bypassAutoIncrementSequenceReset,
250
275
  });
251
276
  primaryKeys.push(primaryKey);
252
277
  }
@@ -47,6 +47,7 @@ export type MutationOptions = {
47
47
  */
48
48
  mutationTracker?: MutationTracker | undefined;
49
49
  preMutationError?: DirectusError | undefined;
50
+ bypassAutoIncrementSequenceReset?: boolean;
50
51
  };
51
52
  export type ActionEventParams = {
52
53
  event: string | string[];
@@ -30,6 +30,7 @@ export default abstract class SocketController {
30
30
  maxConnections: number;
31
31
  };
32
32
  protected getRateLimiter(): RateLimiterAbstract | null;
33
+ private catchInvalidMessages;
33
34
  protected handleUpgrade(request: IncomingMessage, socket: internal.Duplex, head: Buffer): Promise<void>;
34
35
  protected handleStrictUpgrade({ request, socket, head }: UpgradeContext, query: ParsedUrlQuery): Promise<void>;
35
36
  protected handleHandshakeUpgrade({ request, socket, head }: UpgradeContext): Promise<void>;
@@ -67,6 +67,19 @@ export default class SocketController {
67
67
  }
68
68
  return null;
69
69
  }
70
+ catchInvalidMessages(ws) {
71
+ /**
72
+ * This fix was done to prevent the API from crashing on receiving invalid WebSocket frames
73
+ * https://github.com/directus/directus/security/advisories/GHSA-hmgw-9jrg-hf2m
74
+ * https://github.com/websockets/ws/issues/2098
75
+ */
76
+ // @ts-ignore <- required because "_socket" is not typed on WS
77
+ ws._socket.prependListener('data', (data) => data.toString());
78
+ ws.on('error', (error) => {
79
+ if (error.message)
80
+ logger.debug(error.message);
81
+ });
82
+ }
70
83
  async handleUpgrade(request, socket, head) {
71
84
  const { pathname, query } = parse(request.url, true);
72
85
  if (pathname !== this.endpoint)
@@ -87,6 +100,7 @@ export default class SocketController {
87
100
  return;
88
101
  }
89
102
  this.server.handleUpgrade(request, socket, head, async (ws) => {
103
+ this.catchInvalidMessages(ws);
90
104
  const state = { accountability: null, expires_at: null };
91
105
  this.server.emit('connection', ws, state);
92
106
  });
@@ -109,12 +123,14 @@ export default class SocketController {
109
123
  return;
110
124
  }
111
125
  this.server.handleUpgrade(request, socket, head, async (ws) => {
126
+ this.catchInvalidMessages(ws);
112
127
  const state = { accountability, expires_at };
113
128
  this.server.emit('connection', ws, state);
114
129
  });
115
130
  }
116
131
  async handleHandshakeUpgrade({ request, socket, head }) {
117
132
  this.server.handleUpgrade(request, socket, head, async (ws) => {
133
+ this.catchInvalidMessages(ws);
118
134
  try {
119
135
  const payload = await waitForAnyMessage(ws, this.authentication.timeout);
120
136
  if (getMessageType(payload) !== 'auth')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "13.0.0",
3
+ "version": "13.1.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -131,7 +131,7 @@
131
131
  "rollup": "3.22.0",
132
132
  "samlify": "2.8.10",
133
133
  "sanitize-html": "2.10.0",
134
- "sharp": "0.32.1",
134
+ "sharp": "0.32.5",
135
135
  "snappy": "7.2.2",
136
136
  "stream-json": "1.7.5",
137
137
  "strip-bom-stream": "5.0.0",
@@ -143,22 +143,22 @@
143
143
  "ws": "8.12.1",
144
144
  "zod": "3.21.4",
145
145
  "zod-validation-error": "1.0.1",
146
- "@directus/app": "10.7.0",
146
+ "@directus/app": "10.8.0",
147
147
  "@directus/constants": "10.2.3",
148
148
  "@directus/errors": "0.0.2",
149
- "@directus/extensions-sdk": "10.1.9",
150
- "@directus/pressure": "1.0.8",
149
+ "@directus/extensions-sdk": "10.1.10",
150
+ "@directus/pressure": "1.0.9",
151
151
  "@directus/schema": "10.0.2",
152
152
  "@directus/specs": "10.2.0",
153
153
  "@directus/storage": "10.0.5",
154
- "@directus/storage-driver-azure": "10.0.9",
155
- "@directus/storage-driver-cloudinary": "10.0.9",
156
- "@directus/storage-driver-gcs": "10.0.9",
157
- "@directus/storage-driver-local": "10.0.9",
158
- "@directus/storage-driver-s3": "10.0.9",
159
- "@directus/storage-driver-supabase": "1.0.1",
160
- "@directus/utils": "10.0.9",
161
- "@directus/validation": "0.0.4"
154
+ "@directus/storage-driver-azure": "10.0.10",
155
+ "@directus/storage-driver-cloudinary": "10.0.10",
156
+ "@directus/storage-driver-gcs": "10.0.10",
157
+ "@directus/storage-driver-local": "10.0.10",
158
+ "@directus/storage-driver-s3": "10.0.10",
159
+ "@directus/storage-driver-supabase": "1.0.2",
160
+ "@directus/utils": "10.0.10",
161
+ "@directus/validation": "0.0.5"
162
162
  },
163
163
  "devDependencies": {
164
164
  "@ngneat/falso": "6.4.0",
@@ -207,7 +207,7 @@
207
207
  "vitest": "0.31.1",
208
208
  "@directus/random": "0.2.2",
209
209
  "@directus/tsconfig": "1.0.0",
210
- "@directus/types": "10.1.5"
210
+ "@directus/types": "10.1.6"
211
211
  },
212
212
  "optionalDependencies": {
213
213
  "@keyv/redis": "2.5.8",