@directus/api 28.0.3 → 29.0.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.
@@ -189,10 +189,10 @@ router.get('/sources/:chunk', asyncHandler(async (req, res) => {
189
189
  const extensionManager = getExtensionManager();
190
190
  let source;
191
191
  if (chunk === 'index.js') {
192
- source = extensionManager.getAppExtensionsBundle();
192
+ source = await extensionManager.getAppExtensionChunk();
193
193
  }
194
194
  else {
195
- source = extensionManager.getAppExtensionChunk(chunk);
195
+ source = await extensionManager.getAppExtensionChunk(chunk);
196
196
  }
197
197
  if (source === null) {
198
198
  throw new RouteNotFoundError({ path: req.path });
@@ -200,6 +200,6 @@ router.get('/sources/:chunk', asyncHandler(async (req, res) => {
200
200
  res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
201
201
  res.setHeader('Cache-Control', getCacheControlHeader(req, getMilliseconds(env['EXTENSIONS_CACHE_TTL']), false, false));
202
202
  res.setHeader('Vary', 'Origin, Cache-Control');
203
- res.end(source);
203
+ source.pipe(res);
204
204
  }));
205
205
  export default router;
@@ -27,10 +27,12 @@ export async function parseFields(options, context) {
27
27
  : null;
28
28
  const relationalStructure = Object.create(null);
29
29
  for (const fieldKey of fields) {
30
+ let alias = false;
30
31
  let name = fieldKey;
31
32
  if (options.query.alias) {
32
33
  // check for field alias (is one of the key)
33
34
  if (name in options.query.alias) {
35
+ alias = true;
34
36
  name = options.query.alias[fieldKey];
35
37
  }
36
38
  }
@@ -100,7 +102,7 @@ export async function parseFields(options, context) {
100
102
  }
101
103
  continue;
102
104
  }
103
- children.push({ type: 'field', name, fieldKey, whenCase: [] });
105
+ children.push({ type: 'field', name, fieldKey, whenCase: [], alias });
104
106
  }
105
107
  }
106
108
  for (const [fieldKey, nestedFields] of Object.entries(relationalStructure)) {
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,10 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_users', (table) => {
3
+ table.string('text_direction').defaultTo('auto').notNullable();
4
+ });
5
+ }
6
+ export async function down(knex) {
7
+ await knex.schema.alterTable('directus_users', (table) => {
8
+ table.dropColumn('text_direction');
9
+ });
10
+ }
@@ -20,6 +20,7 @@ export function applyParentFilters(schema, nestedCollectionNodes, parentItem) {
20
20
  name: nestedNode.relation.field,
21
21
  fieldKey: nestedNode.relation.field,
22
22
  whenCase: [],
23
+ alias: false,
23
24
  });
24
25
  }
25
26
  if (nestedNode.relation.meta?.sort_field) {
@@ -28,6 +29,7 @@ export function applyParentFilters(schema, nestedCollectionNodes, parentItem) {
28
29
  name: nestedNode.relation.meta.sort_field,
29
30
  fieldKey: nestedNode.relation.meta.sort_field,
30
31
  whenCase: [],
32
+ alias: false,
31
33
  });
32
34
  }
33
35
  const foreignField = nestedNode.relation.field;
@@ -1,4 +1,6 @@
1
1
  import { applyFunctionToColumnName } from './apply-function-to-column-name.js';
2
2
  export function getNodeAlias(node) {
3
+ if ('alias' in node && node.alias === true)
4
+ return node.fieldKey;
3
5
  return applyFunctionToColumnName(node.fieldKey);
4
6
  }
@@ -38,9 +38,15 @@ export function removeTemporaryFields(schema, rawItem, ast, primaryKeyField, par
38
38
  }
39
39
  else {
40
40
  const fields = [];
41
+ const aliasFields = [];
41
42
  const nestedCollectionNodes = [];
42
43
  for (const child of ast.children) {
43
- fields.push(child.fieldKey);
44
+ if ('alias' in child && child.alias === true) {
45
+ aliasFields.push(child.fieldKey);
46
+ }
47
+ else {
48
+ fields.push(child.fieldKey);
49
+ }
44
50
  if (child.type !== 'field' && child.type !== 'functionField') {
45
51
  nestedCollectionNodes.push(child);
46
52
  }
@@ -65,7 +71,7 @@ export function removeTemporaryFields(schema, rawItem, ast, primaryKeyField, par
65
71
  : schema.collections[nestedNode.relation.collection].primary, item);
66
72
  }
67
73
  const fieldsWithFunctionsApplied = fields.map((field) => applyFunctionToColumnName(field));
68
- item = fields.length > 0 ? pick(rawItem, fieldsWithFunctionsApplied) : rawItem[primaryKeyField];
74
+ item = fields.length > 0 ? pick(rawItem, fieldsWithFunctionsApplied, aliasFields) : rawItem[primaryKeyField];
69
75
  items.push(item);
70
76
  }
71
77
  }
@@ -1,6 +1,7 @@
1
1
  import type { Extension } from '@directus/extensions';
2
2
  import { Router } from 'express';
3
3
  import type { ExtensionManagerOptions } from './types.js';
4
+ import type { ReadStream } from 'node:fs';
4
5
  export declare class ExtensionManager {
5
6
  private options;
6
7
  /**
@@ -14,11 +15,6 @@ export declare class ExtensionManager {
14
15
  * Settings for the extensions that are loaded within the current process
15
16
  */
16
17
  private extensionsSettings;
17
- /**
18
- * App extensions rolled up into a single bundle. Any chunks from the bundle will be available
19
- * under appExtensionChunks
20
- */
21
- private appExtensionsBundle;
22
18
  /**
23
19
  * Individual filename chunks from the rollup bundle. Used to improve the performance by allowing
24
20
  * extensions to split up their bundle into multiple smaller chunks
@@ -97,13 +93,10 @@ export declare class ExtensionManager {
97
93
  forceSync: boolean;
98
94
  }): Promise<unknown>;
99
95
  /**
100
- * Return the previously generated app extensions bundle
101
- */
102
- getAppExtensionsBundle(): string | null;
103
- /**
104
- * Return the previously generated app extension bundle chunk by name
96
+ * Return the previously generated app extension bundle chunk by name.
97
+ * Providing no name will return the entry bundle.
105
98
  */
106
- getAppExtensionChunk(name: string): string | null;
99
+ getAppExtensionChunk(name?: string): Promise<ReadStream | null>;
107
100
  /**
108
101
  * Return the scoped router for custom endpoints
109
102
  */
@@ -12,9 +12,10 @@ import ivm from 'isolated-vm';
12
12
  import { clone, debounce, isPlainObject } from 'lodash-es';
13
13
  import { readFile, readdir } from 'node:fs/promises';
14
14
  import os from 'node:os';
15
- import { dirname } from 'node:path';
15
+ import { dirname, join } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import path from 'path';
18
+ import { rolldown } from 'rolldown';
18
19
  import { rollup } from 'rollup';
19
20
  import { useBus } from '../bus/index.js';
20
21
  import getDatabase from '../database/index.js';
@@ -37,6 +38,7 @@ import { generateApiExtensionsSandboxEntrypoint } from './lib/sandbox/generate-a
37
38
  import { instantiateSandboxSdk } from './lib/sandbox/sdk/instantiate.js';
38
39
  import { syncExtensions } from './lib/sync-extensions.js';
39
40
  import { wrapEmbeds } from './lib/wrap-embeds.js';
41
+ import DriverLocal from '@directus/storage-driver-local';
40
42
  // Workaround for https://github.com/rollup/plugins/issues/1329
41
43
  const virtual = virtualDefault;
42
44
  const alias = aliasDefault;
@@ -63,16 +65,11 @@ export class ExtensionManager {
63
65
  * Settings for the extensions that are loaded within the current process
64
66
  */
65
67
  extensionsSettings = [];
66
- /**
67
- * App extensions rolled up into a single bundle. Any chunks from the bundle will be available
68
- * under appExtensionChunks
69
- */
70
- appExtensionsBundle = null;
71
68
  /**
72
69
  * Individual filename chunks from the rollup bundle. Used to improve the performance by allowing
73
70
  * extensions to split up their bundle into multiple smaller chunks
74
71
  */
75
- appExtensionChunks = new Map();
72
+ appExtensionChunks = [];
76
73
  /**
77
74
  * Callbacks to be able to unregister extensions
78
75
  */
@@ -220,7 +217,7 @@ export class ExtensionManager {
220
217
  }
221
218
  await Promise.all([this.registerInternalOperations(), this.registerApiExtensions()]);
222
219
  if (env['SERVE_APP']) {
223
- this.appExtensionsBundle = await this.generateExtensionBundle();
220
+ await this.generateExtensionBundle();
224
221
  }
225
222
  this.isLoaded = true;
226
223
  emitter.emitAction('extensions.load', {
@@ -234,7 +231,6 @@ export class ExtensionManager {
234
231
  async unload() {
235
232
  await this.unregisterApiExtensions();
236
233
  this.localEmitter.offAll();
237
- this.appExtensionsBundle = null;
238
234
  this.isLoaded = false;
239
235
  emitter.emitAction('extensions.unload', {
240
236
  extensions: this.extensions,
@@ -289,16 +285,24 @@ export class ExtensionManager {
289
285
  return promise;
290
286
  }
291
287
  /**
292
- * Return the previously generated app extensions bundle
293
- */
294
- getAppExtensionsBundle() {
295
- return this.appExtensionsBundle;
296
- }
297
- /**
298
- * Return the previously generated app extension bundle chunk by name
288
+ * Return the previously generated app extension bundle chunk by name.
289
+ * Providing no name will return the entry bundle.
299
290
  */
300
- getAppExtensionChunk(name) {
301
- return this.appExtensionChunks.get(name) ?? null;
291
+ async getAppExtensionChunk(name) {
292
+ let file;
293
+ if (!name) {
294
+ file = this.appExtensionChunks[0];
295
+ }
296
+ else if (this.appExtensionChunks.includes(name)) {
297
+ file = name;
298
+ }
299
+ if (!file)
300
+ return null;
301
+ const tempDir = join(env['TEMP_PATH'], 'app-extensions');
302
+ const tmpStorage = new DriverLocal({ root: tempDir });
303
+ if ((await tmpStorage.exists(file)) === false)
304
+ return null;
305
+ return await tmpStorage.read(file);
302
306
  }
303
307
  /**
304
308
  * Return the scoped router for custom endpoints
@@ -369,6 +373,7 @@ export class ExtensionManager {
369
373
  */
370
374
  async generateExtensionBundle() {
371
375
  const logger = useLogger();
376
+ const env = useEnv();
372
377
  const sharedDepsMapping = await getSharedDepsMapping(APP_SHARED_DEPS);
373
378
  const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
374
379
  find: name,
@@ -376,26 +381,30 @@ export class ExtensionManager {
376
381
  }));
377
382
  const entrypoint = generateExtensionsEntrypoint({ module: this.moduleExtensions, registry: this.registryExtensions, local: this.localExtensions }, this.extensionsSettings);
378
383
  try {
379
- const bundle = await rollup({
384
+ /** Opt In for now. Should be @deprecated later to always use rolldown! */
385
+ const rollDirection = env['EXTENSIONS_ROLLDOWN'] ?? false ? rolldown : rollup;
386
+ const bundle = await rollDirection({
380
387
  input: 'entry',
381
388
  external: Object.values(sharedDepsMapping),
382
389
  makeAbsoluteExternalsRelative: false,
383
390
  plugins: [virtual({ entry: entrypoint }), alias({ entries: internalImports }), nodeResolve({ browser: true })],
384
391
  });
385
- const { output } = await bundle.generate({ format: 'es', compact: true });
386
- for (const out of output) {
387
- if (out.type === 'chunk') {
388
- this.appExtensionChunks.set(out.fileName, out.code);
389
- }
390
- }
392
+ const tempDir = join(env['TEMP_PATH'], 'app-extensions');
393
+ const { output } = await bundle.write({
394
+ format: 'es',
395
+ dir: tempDir,
396
+ });
397
+ this.appExtensionChunks = output.reduce((acc, chunk) => {
398
+ if (chunk.type === 'chunk')
399
+ acc.push(chunk.fileName);
400
+ return acc;
401
+ }, []);
391
402
  await bundle.close();
392
- return output[0].code;
393
403
  }
394
404
  catch (error) {
395
405
  logger.warn(`Couldn't bundle App extensions`);
396
406
  logger.warn(error);
397
407
  }
398
- return null;
399
408
  }
400
409
  async registerSandboxedApiExtension(extension) {
401
410
  const logger = useLogger();
@@ -412,6 +421,7 @@ export class ExtensionManager {
412
421
  },
413
422
  });
414
423
  const context = await isolate.createContext();
424
+ context.global.setSync('process', { env: { NODE_ENV: process.env['NODE_ENV'] ?? 'production' } }, { copy: true });
415
425
  const module = await isolate.compileModule(extensionCode, { filename: `file://${entrypointPath}` });
416
426
  const sdkModule = await instantiateSandboxSdk(isolate, extension.sandbox?.requestedScopes ?? {});
417
427
  await module.instantiate(context, (specifier) => {
@@ -13,7 +13,8 @@ export async function validateItemAccess(options, context) {
13
13
  name: options.collection,
14
14
  query: { limit: options.primaryKeys.length },
15
15
  // Act as if every field was a "normal" field
16
- children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [] })) ?? [],
16
+ children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [], alias: false })) ??
17
+ [],
17
18
  cases: [],
18
19
  };
19
20
  await processAst({ ast, ...options }, context);
@@ -148,7 +148,7 @@ export class FieldsService {
148
148
  return data;
149
149
  });
150
150
  const knownCollections = Object.keys(this.schema.collections);
151
- const result = [...columnsWithSystem, ...aliasFieldsAsField].filter((field) => knownCollections.includes(field.collection));
151
+ let result = [...columnsWithSystem, ...aliasFieldsAsField].filter((field) => knownCollections.includes(field.collection));
152
152
  // Filter the result so we only return the fields you have read access to
153
153
  if (this.accountability && this.accountability.admin !== true) {
154
154
  const policies = await fetchPolicies(this.accountability, { knex: this.knex, schema: this.schema });
@@ -176,7 +176,7 @@ export class FieldsService {
176
176
  if (collection && collection in allowedFieldsInCollection === false) {
177
177
  throw new ForbiddenError();
178
178
  }
179
- return result.filter((field) => {
179
+ result = result.filter((field) => {
180
180
  if (field.collection in allowedFieldsInCollection === false)
181
181
  return false;
182
182
  const allowedFields = allowedFieldsInCollection[field.collection];
@@ -187,12 +187,6 @@ export class FieldsService {
187
187
  }
188
188
  // Update specific database type overrides
189
189
  for (const field of result) {
190
- if (field.meta?.special?.includes('cast-timestamp')) {
191
- field.type = 'timestamp';
192
- }
193
- else if (field.meta?.special?.includes('cast-datetime')) {
194
- field.type = 'dateTime';
195
- }
196
190
  field.type = this.helpers.schema.processFieldType(field);
197
191
  }
198
192
  return result;
@@ -25,6 +25,7 @@ import { FilesService } from './files.js';
25
25
  import { NotificationsService } from './notifications.js';
26
26
  import { UsersService } from './users.js';
27
27
  import { parseFields } from '../database/get-ast-from-query/lib/parse-fields.js';
28
+ import { set } from 'lodash-es';
28
29
  const env = useEnv();
29
30
  const logger = useLogger();
30
31
  export class ImportService {
@@ -166,13 +167,14 @@ export class ImportService {
166
167
  fileReadStream
167
168
  .pipe(Papa.parse(Papa.NODE_STREAM_INPUT, PapaOptions))
168
169
  .on('data', (obj) => {
170
+ const result = {};
169
171
  // Filter out all undefined fields
170
172
  for (const field in obj) {
171
- if (obj[field] === undefined) {
172
- delete obj[field];
173
+ if (obj[field] !== undefined) {
174
+ set(result, field, obj[field]);
173
175
  }
174
176
  }
175
- saveQueue.push(obj);
177
+ saveQueue.push(result);
176
178
  })
177
179
  .on('error', (error) => {
178
180
  cleanup();
@@ -66,6 +66,8 @@ export type FieldNode = {
66
66
  type: 'field';
67
67
  name: string;
68
68
  fieldKey: string;
69
+ /** If the field was created through alias query parameters */
70
+ alias: boolean;
69
71
  /**
70
72
  * Which permission cases have to be met on the current item for this field to return a value
71
73
  */
@@ -18,9 +18,9 @@ export async function getSnapshot(options) {
18
18
  fieldsService.readAll(),
19
19
  relationsService.readAll(),
20
20
  ]);
21
- const collectionsFiltered = collectionsRaw.filter((item) => excludeSystem(item));
22
- const fieldsFiltered = fieldsRaw.filter((item) => excludeSystem(item));
23
- const relationsFiltered = relationsRaw.filter((item) => excludeSystem(item));
21
+ const collectionsFiltered = collectionsRaw.filter((item) => excludeSystem(item) && excludeUntracked(item));
22
+ const fieldsFiltered = fieldsRaw.filter((item) => excludeSystem(item) && excludeUntracked(item));
23
+ const relationsFiltered = relationsRaw.filter((item) => excludeSystem(item) && excludeUntracked(item));
24
24
  const collectionsSorted = sortBy(mapValues(collectionsFiltered, sortDeep), ['collection']);
25
25
  const fieldsSorted = sortBy(mapValues(fieldsFiltered, sortDeep), ['collection', 'meta.id']).map(omitID);
26
26
  const relationsSorted = sortBy(mapValues(relationsFiltered, sortDeep), ['collection', 'meta.id']).map(omitID);
@@ -38,6 +38,11 @@ function excludeSystem(item) {
38
38
  return false;
39
39
  return true;
40
40
  }
41
+ function excludeUntracked(item) {
42
+ if (item?.meta === null)
43
+ return false;
44
+ return true;
45
+ }
41
46
  function omitID(item) {
42
47
  return omit(item, 'meta.id');
43
48
  }
@@ -1,4 +1,5 @@
1
+ import type { Accountability } from '@directus/types';
1
2
  import type { BasicAuthMessage } from './messages.js';
2
3
  import type { AuthenticationState } from './types.js';
3
- export declare function authenticateConnection(message: BasicAuthMessage & Record<string, any>): Promise<AuthenticationState>;
4
+ export declare function authenticateConnection(message: BasicAuthMessage & Record<string, any>, accountabilityOverrides?: Partial<Accountability>): Promise<AuthenticationState>;
4
5
  export declare function authenticationSuccess(uid?: string | number, refresh_token?: string): string;
@@ -1,10 +1,14 @@
1
+ import { isEqual } from 'lodash-es';
1
2
  import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
3
+ import getDatabase from '../database/index.js';
4
+ import emitter from '../emitter.js';
5
+ import { createDefaultAccountability } from '../permissions/utils/create-default-accountability.js';
2
6
  import { AuthenticationService } from '../services/index.js';
3
7
  import { getAccountabilityForToken } from '../utils/get-accountability-for-token.js';
4
8
  import { getSchema } from '../utils/get-schema.js';
5
9
  import { WebSocketError } from './errors.js';
6
10
  import { getExpiresAtForToken } from './utils/get-expires-at-for-token.js';
7
- export async function authenticateConnection(message) {
11
+ export async function authenticateConnection(message, accountabilityOverrides) {
8
12
  let access_token, refresh_token;
9
13
  try {
10
14
  if ('email' in message && 'password' in message) {
@@ -24,9 +28,26 @@ export async function authenticateConnection(message) {
24
28
  }
25
29
  if (!access_token)
26
30
  throw new Error();
27
- const accountability = await getAccountabilityForToken(access_token);
28
- const expires_at = getExpiresAtForToken(access_token);
29
- return { accountability, expires_at, refresh_token };
31
+ const defaultAccountability = createDefaultAccountability(accountabilityOverrides);
32
+ const authenticationState = {
33
+ accountability: defaultAccountability,
34
+ expires_at: getExpiresAtForToken(access_token),
35
+ refresh_token,
36
+ };
37
+ const customAccountability = await emitter.emitFilter('websocket.authenticate', defaultAccountability, {
38
+ message,
39
+ }, {
40
+ database: getDatabase(),
41
+ schema: null,
42
+ accountability: null,
43
+ });
44
+ if (customAccountability && isEqual(customAccountability, defaultAccountability) === false) {
45
+ authenticationState.accountability = customAccountability;
46
+ }
47
+ else {
48
+ authenticationState.accountability = await getAccountabilityForToken(access_token, defaultAccountability);
49
+ }
50
+ return authenticationState;
30
51
  }
31
52
  catch {
32
53
  throw new WebSocketError('auth', 'AUTH_FAILED', 'Authentication failed.', message['uid']);
@@ -10,12 +10,10 @@ import emitter from '../../emitter.js';
10
10
  import { useLogger } from '../../logger/index.js';
11
11
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
12
12
  import { createRateLimiter } from '../../rate-limiter.js';
13
- import { getAccountabilityForToken } from '../../utils/get-accountability-for-token.js';
14
13
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
15
14
  import { authenticateConnection, authenticationSuccess } from '../authenticate.js';
16
15
  import { WebSocketError, handleWebSocketError } from '../errors.js';
17
16
  import { AuthMode, WebSocketAuthMessage, WebSocketMessage } from '../messages.js';
18
- import { getExpiresAtForToken } from '../utils/get-expires-at-for-token.js';
19
17
  import { getMessageType } from '../utils/message.js';
20
18
  import { waitForAnyMessage, waitForMessageType } from '../utils/wait-for-message.js';
21
19
  const TOKEN_CHECK_INTERVAL = 15 * 60 * 1000; // 15 minutes
@@ -139,8 +137,9 @@ export default class SocketController {
139
137
  let expires_at = null;
140
138
  if (token) {
141
139
  try {
142
- accountability = await getAccountabilityForToken(token);
143
- expires_at = getExpiresAtForToken(token);
140
+ const state = await authenticateConnection({ access_token: token }, accountabilityOverrides);
141
+ accountability = state.accountability;
142
+ expires_at = state.expires_at;
144
143
  }
145
144
  catch {
146
145
  accountability = null;
@@ -162,7 +161,6 @@ export default class SocketController {
162
161
  socket.destroy();
163
162
  return;
164
163
  }
165
- Object.assign(accountability, accountabilityOverrides);
166
164
  this.server.handleUpgrade(request, socket, head, async (ws) => {
167
165
  this.catchInvalidMessages(ws);
168
166
  const state = { accountability, expires_at };
@@ -176,10 +174,7 @@ export default class SocketController {
176
174
  const payload = await waitForAnyMessage(ws, this.authentication.timeout);
177
175
  if (getMessageType(payload) !== 'auth')
178
176
  throw new Error();
179
- const state = await authenticateConnection(WebSocketAuthMessage.parse(payload));
180
- if (state.accountability) {
181
- Object.assign(state.accountability, accountabilityOverrides);
182
- }
177
+ const state = await authenticateConnection(WebSocketAuthMessage.parse(payload), accountabilityOverrides);
183
178
  this.checkUserRequirements(state.accountability);
184
179
  ws.send(authenticationSuccess(payload['uid'], state.refresh_token));
185
180
  this.server.emit('connection', ws, state);
@@ -268,19 +263,20 @@ export default class SocketController {
268
263
  }
269
264
  async handleAuthRequest(client, message) {
270
265
  try {
271
- const { accountability, expires_at, refresh_token } = await authenticateConnection(message);
272
- this.checkUserRequirements(accountability);
266
+ let accountabilityOverrides = {};
273
267
  /**
274
268
  * Re-use the existing ip, userAgent and origin accountability properties.
275
269
  * They are only sent in the original connection request
276
270
  */
277
- if (accountability && client.accountability) {
278
- Object.assign(accountability, {
271
+ if (client.accountability) {
272
+ accountabilityOverrides = {
279
273
  ip: client.accountability.ip,
280
274
  userAgent: client.accountability.userAgent,
281
275
  origin: client.accountability.origin,
282
- });
276
+ };
283
277
  }
278
+ const { accountability, expires_at, refresh_token } = await authenticateConnection(message, accountabilityOverrides);
279
+ this.checkUserRequirements(accountability);
284
280
  client.accountability = accountability;
285
281
  client.expires_at = expires_at;
286
282
  this.setTokenExpireTimer(client);
@@ -1,16 +1,16 @@
1
1
  import { CloseCode, MessageType, makeServer } from 'graphql-ws';
2
2
  import { useLogger } from '../../logger/index.js';
3
+ import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
3
4
  import { bindPubSub } from '../../services/graphql/subscription.js';
4
5
  import { GraphQLService } from '../../services/index.js';
5
- import { getSchema } from '../../utils/get-schema.js';
6
6
  import { getAddress } from '../../utils/get-address.js';
7
+ import { getSchema } from '../../utils/get-schema.js';
7
8
  import { authenticateConnection } from '../authenticate.js';
8
9
  import { handleWebSocketError } from '../errors.js';
9
10
  import { ConnectionParams, WebSocketMessage } from '../messages.js';
10
11
  import { getMessageType } from '../utils/message.js';
11
12
  import SocketController from './base.js';
12
13
  import { registerWebSocketEvents } from './hooks.js';
13
- import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
14
14
  const logger = useLogger();
15
15
  export class GraphQLSubscriptionController extends SocketController {
16
16
  gql;
@@ -51,6 +51,8 @@ export class GraphQLSubscriptionController extends SocketController {
51
51
  if (typeof params.access_token === 'string') {
52
52
  const { accountability, expires_at } = await authenticateConnection({
53
53
  access_token: params.access_token,
54
+ }, {
55
+ ip: client.accountability?.ip ?? null,
54
56
  });
55
57
  client.accountability = accountability;
56
58
  client.expires_at = expires_at;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "28.0.3",
3
+ "version": "29.0.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -140,6 +140,7 @@
140
140
  "qs": "6.14.0",
141
141
  "rate-limiter-flexible": "5.0.5",
142
142
  "rollup": "4.34.9",
143
+ "rolldown": "1.0.0-beta.28",
143
144
  "samlify": "2.10.0",
144
145
  "sanitize-html": "2.14.0",
145
146
  "sharp": "0.33.5",
@@ -152,30 +153,30 @@
152
153
  "ws": "8.18.1",
153
154
  "zod": "3.24.2",
154
155
  "zod-validation-error": "3.4.0",
155
- "@directus/app": "13.11.3",
156
- "@directus/env": "5.1.0",
157
- "@directus/constants": "13.0.1",
156
+ "@directus/app": "13.12.0",
157
+ "@directus/env": "5.1.1",
158
+ "@directus/extensions-registry": "3.0.8",
159
+ "@directus/extensions": "3.0.8",
158
160
  "@directus/errors": "2.0.2",
159
- "@directus/extensions": "3.0.7",
160
- "@directus/extensions-registry": "3.0.7",
161
+ "@directus/extensions-sdk": "15.0.0",
161
162
  "@directus/format-title": "12.0.1",
162
- "@directus/extensions-sdk": "14.0.0",
163
- "@directus/memory": "3.0.6",
164
- "@directus/pressure": "3.0.6",
163
+ "@directus/memory": "3.0.7",
165
164
  "@directus/schema": "13.0.1",
165
+ "@directus/pressure": "3.0.7",
166
166
  "@directus/schema-builder": "0.0.3",
167
- "@directus/specs": "11.1.0",
168
167
  "@directus/storage": "12.0.0",
169
- "@directus/storage-driver-azure": "12.0.6",
170
- "@directus/storage-driver-cloudinary": "12.0.6",
171
- "@directus/storage-driver-gcs": "12.0.6",
168
+ "@directus/specs": "11.1.0",
169
+ "@directus/storage-driver-azure": "12.0.7",
170
+ "@directus/storage-driver-gcs": "12.0.7",
171
+ "@directus/storage-driver-cloudinary": "12.0.7",
172
+ "@directus/storage-driver-s3": "12.0.7",
172
173
  "@directus/storage-driver-local": "12.0.0",
173
- "@directus/storage-driver-supabase": "3.0.6",
174
- "@directus/system-data": "3.1.1",
175
- "@directus/utils": "13.0.7",
176
- "@directus/storage-driver-s3": "12.0.6",
177
- "directus": "11.9.3",
178
- "@directus/validation": "2.0.6"
174
+ "@directus/storage-driver-supabase": "3.0.7",
175
+ "@directus/constants": "13.0.1",
176
+ "@directus/system-data": "3.2.0",
177
+ "@directus/utils": "13.0.8",
178
+ "@directus/validation": "2.0.7",
179
+ "directus": "11.10.0"
179
180
  },
180
181
  "devDependencies": {
181
182
  "@directus/tsconfig": "3.0.0",
@@ -201,7 +202,7 @@
201
202
  "@types/lodash-es": "4.17.12",
202
203
  "@types/mime-types": "2.1.4",
203
204
  "@types/ms": "2.1.0",
204
- "@types/node": "22.13.8",
205
+ "@types/node": "22.13.14",
205
206
  "@types/node-schedule": "2.1.7",
206
207
  "@types/nodemailer": "6.4.17",
207
208
  "@types/object-hash": "3.0.6",
@@ -212,16 +213,16 @@
212
213
  "@types/stream-json": "1.7.8",
213
214
  "@types/wellknown": "0.5.8",
214
215
  "@types/ws": "8.5.14",
215
- "@vitest/coverage-v8": "2.1.9",
216
+ "@vitest/coverage-v8": "3.2.4",
216
217
  "copyfiles": "2.4.1",
217
218
  "form-data": "4.0.2",
218
219
  "get-port": "7.1.0",
219
220
  "knex-mock-client": "3.0.2",
220
- "typescript": "5.8.2",
221
- "vitest": "2.1.9",
222
- "@directus/random": "2.0.1",
221
+ "typescript": "5.8.3",
222
+ "vitest": "3.2.4",
223
223
  "@directus/schema-builder": "0.0.3",
224
- "@directus/types": "13.2.0"
224
+ "@directus/types": "13.2.0",
225
+ "@directus/random": "2.0.1"
225
226
  },
226
227
  "optionalDependencies": {
227
228
  "@keyv/redis": "3.0.1",
@@ -240,6 +241,7 @@
240
241
  "cli": "NODE_ENV=development SERVE_APP=false tsx src/cli/run.ts",
241
242
  "dev": "NODE_ENV=development SERVE_APP=true tsx watch --ignore extensions --clear-screen=false src/start.ts",
242
243
  "test": "vitest run",
243
- "test:watch": "vitest"
244
+ "test:watch": "vitest",
245
+ "test:coverage": "vitest run --coverage"
244
246
  }
245
247
  }