@wbce-d9/api 10.3.0 → 10.4.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.
package/dist/app.js CHANGED
@@ -108,10 +108,12 @@ export default async function createApp() {
108
108
  await emitter.emitInit('app.before', { app });
109
109
  await emitter.emitInit('middlewares.before', { app });
110
110
  app.use(expressLogger);
111
- app.use((_req, res, next) => {
112
- res.setHeader('X-Powered-By', 'Directus');
113
- next();
114
- });
111
+ if (env['X_POWERED_BY_ENABLED'] === true) {
112
+ app.use((_req, res, next) => {
113
+ res.setHeader('X-Powered-By', 'Directus');
114
+ next();
115
+ });
116
+ }
115
117
  if (env['CORS_ENABLED'] === true) {
116
118
  app.use(cors);
117
119
  }
@@ -1,9 +1,10 @@
1
1
  import { Router } from 'express';
2
2
  import Joi from 'joi';
3
3
  import ldap from 'ldapjs';
4
- import { GET_SET_HEADER } from '../../constants.js';
4
+ import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
5
5
  import getDatabase from '../../database/index.js';
6
6
  import emitter from '../../emitter.js';
7
+ import env from '../../env.js';
7
8
  import { RecordNotUniqueException } from '../../exceptions/database/record-not-unique.js';
8
9
  import { InvalidConfigException, InvalidCredentialsException, InvalidPayloadException, InvalidProviderException, ServiceUnavailableException, UnexpectedResponseException, } from '../../exceptions/index.js';
9
10
  import logger from '../../logger.js';
@@ -314,7 +315,8 @@ export function createLDAPAuthRouter(provider) {
314
315
  payload['data']['refresh_token'] = refreshToken;
315
316
  }
316
317
  if (mode === 'cookie') {
317
- res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
318
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
319
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
318
320
  }
319
321
  res.locals['payload'] = payload;
320
322
  return next();
@@ -2,7 +2,7 @@ import argon2 from 'argon2';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import { performance } from 'perf_hooks';
5
- import { GET_SET_HEADER } from '../../constants.js';
5
+ import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
6
6
  import env from '../../env.js';
7
7
  import { InvalidCredentialsException, InvalidPayloadException } from '../../exceptions/index.js';
8
8
  import { respond } from '../../middleware/respond.js';
@@ -74,7 +74,8 @@ export function createLocalAuthRouter(provider) {
74
74
  payload['data']['refresh_token'] = refreshToken;
75
75
  }
76
76
  if (mode === 'cookie') {
77
- res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
77
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
78
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
78
79
  }
79
80
  res.locals['payload'] = payload;
80
81
  return next();
@@ -2,11 +2,11 @@ import { BaseException } from '@wbce-d9/exceptions';
2
2
  import express, { Router } from 'express';
3
3
  import flatten from 'flat';
4
4
  import jwt from 'jsonwebtoken';
5
- import { generators, Issuer } from 'openid-client';
5
+ import { Issuer, generators } from 'openid-client';
6
6
  import { getAuthProvider } from '../../auth.js';
7
- import { GET_SET_HEADER } from '../../constants.js';
7
+ import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
8
8
  import env from '../../env.js';
9
- import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException } from '../../exceptions/index.js';
9
+ import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException, InvalidPayloadException, } from '../../exceptions/index.js';
10
10
  import logger from '../../logger.js';
11
11
  import { respond } from '../../middleware/respond.js';
12
12
  import { AuthenticationService } from '../../services/authentication.js';
@@ -16,6 +16,7 @@ import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
16
16
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
17
17
  import { Url } from '../../utils/url.js';
18
18
  import { BaseOAuthDriver } from './baseoauth.js';
19
+ import { isRedirectAllowedOnLogin } from '../../utils/is-redirect-allowed-on-login.js';
19
20
  export class OAuth2AuthDriver extends BaseOAuthDriver {
20
21
  client;
21
22
  redirectUrl;
@@ -118,7 +119,11 @@ export function createOAuth2AuthRouter(providerName) {
118
119
  const provider = getAuthProvider(providerName);
119
120
  const codeVerifier = provider.generateCodeVerifier();
120
121
  const prompt = !!req.query['prompt'];
121
- const token = jwt.sign({ verifier: codeVerifier, redirect: req.query['redirect'], prompt }, env['SECRET'], {
122
+ const redirect = req.query['redirect'];
123
+ if (isRedirectAllowedOnLogin(redirect, providerName) === false) {
124
+ throw new InvalidPayloadException(`URL "${redirect}" can't be used to redirect after login`);
125
+ }
126
+ const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, env['SECRET'], {
122
127
  expiresIn: '5m',
123
128
  issuer: 'directus',
124
129
  });
@@ -186,7 +191,8 @@ export function createOAuth2AuthRouter(providerName) {
186
191
  }
187
192
  const { accessToken, refreshToken, expires } = authResponse;
188
193
  if (redirect) {
189
- res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
194
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
195
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
190
196
  return res.redirect(redirect);
191
197
  }
192
198
  res.locals['payload'] = {
@@ -2,11 +2,11 @@ import { BaseException } from '@wbce-d9/exceptions';
2
2
  import express, { Router } from 'express';
3
3
  import flatten from 'flat';
4
4
  import jwt from 'jsonwebtoken';
5
- import { generators, Issuer } from 'openid-client';
5
+ import { Issuer, generators } from 'openid-client';
6
6
  import { getAuthProvider } from '../../auth.js';
7
- import { GET_SET_HEADER } from '../../constants.js';
7
+ import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
8
8
  import env from '../../env.js';
9
- import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException } from '../../exceptions/index.js';
9
+ import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException, InvalidPayloadException, } from '../../exceptions/index.js';
10
10
  import logger from '../../logger.js';
11
11
  import { respond } from '../../middleware/respond.js';
12
12
  import { AuthenticationService } from '../../services/authentication.js';
@@ -15,6 +15,7 @@ import asyncHandler from '../../utils/async-handler.js';
15
15
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
16
16
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
17
17
  import { Url } from '../../utils/url.js';
18
+ import { isRedirectAllowedOnLogin } from '../../utils/is-redirect-allowed-on-login.js';
18
19
  import { BaseOAuthDriver } from './baseoauth.js';
19
20
  export class OpenIDAuthDriver extends BaseOAuthDriver {
20
21
  client;
@@ -138,7 +139,11 @@ export function createOpenIDAuthRouter(providerName) {
138
139
  const provider = getAuthProvider(providerName);
139
140
  const codeVerifier = provider.generateCodeVerifier();
140
141
  const prompt = !!req.query['prompt'];
141
- const token = jwt.sign({ verifier: codeVerifier, redirect: req.query['redirect'], prompt }, env['SECRET'], {
142
+ const redirect = req.query['redirect'];
143
+ if (isRedirectAllowedOnLogin(redirect, providerName) === false) {
144
+ throw new InvalidPayloadException(`URL "${redirect}" can't be used to redirect after login`);
145
+ }
146
+ const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, env['SECRET'], {
142
147
  expiresIn: '5m',
143
148
  issuer: 'directus',
144
149
  });
@@ -211,7 +216,8 @@ export function createOpenIDAuthRouter(providerName) {
211
216
  }
212
217
  const { accessToken, refreshToken, expires } = authResponse;
213
218
  if (redirect) {
214
- res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
219
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
220
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
215
221
  return res.redirect(redirect);
216
222
  }
217
223
  res.locals['payload'] = {
@@ -3,18 +3,19 @@ import { BaseException } from '@wbce-d9/exceptions';
3
3
  import express, { Router } from 'express';
4
4
  import * as samlify from 'samlify';
5
5
  import { getAuthProvider } from '../../auth.js';
6
- import { COOKIE_OPTIONS, GET_SET_HEADER } from '../../constants.js';
6
+ import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../../constants.js';
7
7
  import getDatabase from '../../database/index.js';
8
8
  import emitter from '../../emitter.js';
9
9
  import env from '../../env.js';
10
10
  import { RecordNotUniqueException } from '../../exceptions/database/record-not-unique.js';
11
- import { InvalidCredentialsException, InvalidProviderException } from '../../exceptions/index.js';
11
+ import { InvalidCredentialsException, InvalidProviderException, InvalidPayloadException, } from '../../exceptions/index.js';
12
12
  import logger from '../../logger.js';
13
13
  import { respond } from '../../middleware/respond.js';
14
14
  import { AuthenticationService } from '../../services/authentication.js';
15
15
  import { UsersService } from '../../services/users.js';
16
16
  import asyncHandler from '../../utils/async-handler.js';
17
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
18
+ import { isRedirectAllowedOnLogin } from '../../utils/is-redirect-allowed-on-login.js';
18
19
  import { LocalAuthDriver } from './local.js';
19
20
  // Register the samlify schema validator
20
21
  samlify.setSchemaValidator(validator);
@@ -89,7 +90,11 @@ export function createSAMLAuthRouter(providerName) {
89
90
  const { context: url } = sp.createLoginRequest(idp, 'redirect');
90
91
  const parsedUrl = new URL(url);
91
92
  if (req.query['redirect']) {
92
- parsedUrl.searchParams.append('RelayState', req.query['redirect']);
93
+ const redirect = req.query['redirect'];
94
+ if (isRedirectAllowedOnLogin(redirect, providerName) === false) {
95
+ throw new InvalidPayloadException(`URL "${redirect}" can't be used to redirect after login`);
96
+ }
97
+ parsedUrl.searchParams.append('RelayState', redirect);
93
98
  }
94
99
  return res.redirect(parsedUrl.toString());
95
100
  }));
@@ -97,11 +102,14 @@ export function createSAMLAuthRouter(providerName) {
97
102
  const { sp, idp } = getAuthProvider(providerName);
98
103
  const { context } = sp.createLogoutRequest(idp, 'redirect', req.body);
99
104
  const authService = new AuthenticationService({ accountability: req.accountability, schema: req.schema });
105
+ if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
106
+ res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
107
+ }
100
108
  if (req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
101
109
  const currentRefreshToken = req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
102
110
  if (currentRefreshToken) {
103
111
  await authService.logout(currentRefreshToken);
104
- res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], COOKIE_OPTIONS);
112
+ res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
105
113
  }
106
114
  }
107
115
  return res.redirect(context);
@@ -121,7 +129,8 @@ export function createSAMLAuthRouter(providerName) {
121
129
  },
122
130
  };
123
131
  if (relayState) {
124
- res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
132
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
133
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
125
134
  return res.redirect(relayState);
126
135
  }
127
136
  return next();
@@ -199,6 +199,18 @@ STORAGE_LOCAL_ROOT="./uploads"
199
199
  # The duration that the access token is valid ["15m"]
200
200
  ACCESS_TOKEN_TTL="15m"
201
201
 
202
+ # Whether or not to set the secure attribute for the access token cookie [false]
203
+ ACCESS_TOKEN_COOKIE_SECURE=false
204
+
205
+ # Value of sameSite for the access token cookie ["lax"]
206
+ ACCESS_TOKEN_COOKIE_SAME_SITE="lax"
207
+
208
+ # Name of the access token cookie ["directus_refresh_token"]
209
+ ACCESS_TOKEN_COOKIE_NAME="directus_session_token"
210
+
211
+ # Which domain to use for the access token cookie. Useful for development mode.
212
+ # ACCESS_TOKEN_COOKIE_DOMAIN
213
+
202
214
  # The duration that the refresh token is valid, and also how long users stay logged-in to the App ["7d"]
203
215
  REFRESH_TOKEN_TTL="7d"
204
216
 
@@ -218,6 +230,9 @@ REFRESH_TOKEN_COOKIE_NAME="directus_refresh_token"
218
230
  # and it should be greater than the time taken for a login request with an invalid password [500]
219
231
  # LOGIN_STALL_TIME=500
220
232
 
233
+ # Whether or not to enable the X-Powered-By Header [true]
234
+ X_POWERED_BY_ENABLED=false
235
+
221
236
  # Whether or not to enable the CORS headers [false]
222
237
  CORS_ENABLED=true
223
238
 
@@ -8,8 +8,11 @@ export declare const DEFAULT_AUTH_PROVIDER = "default";
8
8
  export declare const COLUMN_TRANSFORMS: string[];
9
9
  export declare const GENERATE_SPECIAL: string[];
10
10
  export declare const UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
11
- export declare const COOKIE_OPTIONS: CookieOptions;
11
+ export declare const REFRESH_COOKIE_OPTIONS: CookieOptions;
12
+ export declare const REFRESH_COOKIE_CLEAR_OPTIONS: CookieOptions;
12
13
  export declare const GET_SET_HEADER: (cookieValue: string) => string;
14
+ export declare const ACCESS_COOKIE_OPTIONS: CookieOptions;
15
+ export declare const ACCESS_COOKIE_CLEAR_OPTIONS: CookieOptions;
13
16
  export declare const OAS_REQUIRED_SCHEMAS: string[];
14
17
  /** Formats from which transformation is supported */
15
18
  export declare const SUPPORTED_IMAGE_TRANSFORM_FORMATS: string[];
package/dist/constants.js CHANGED
@@ -48,19 +48,32 @@ export const DEFAULT_AUTH_PROVIDER = 'default';
48
48
  export const COLUMN_TRANSFORMS = ['year', 'month', 'day', 'weekday', 'hour', 'minute', 'second'];
49
49
  export const GENERATE_SPECIAL = ['uuid', 'date-created', 'role-created', 'user-created'];
50
50
  export const UUID_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
51
- export const COOKIE_OPTIONS = {
51
+ export const REFRESH_COOKIE_OPTIONS = {
52
52
  httpOnly: true,
53
53
  domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
54
54
  maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
55
55
  secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
56
56
  sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
57
+ path: '/auth',
57
58
  };
59
+ const { maxAge: refreshCookieMaxAge, expires: refreshCookieExpires, ...refresh_cookie_clear_options } = REFRESH_COOKIE_OPTIONS;
60
+ export const REFRESH_COOKIE_CLEAR_OPTIONS = refresh_cookie_clear_options;
58
61
  export const GET_SET_HEADER = (cookieValue) => {
59
62
  const domainHeader = env['REFRESH_TOKEN_COOKIE_DOMAIN'] ? ` Domain=${env['REFRESH_TOKEN_COOKIE_DOMAIN']};` : '';
60
63
  const partitionedHeader = env['REFRESH_TOKEN_COOKIE_PARTITIONED'] == true ? ' Partitioned;' : '';
61
- const cookieHeaderValue = `${env['REFRESH_TOKEN_COOKIE_NAME']}=${cookieValue}; HttpOnly; Max-Age=${getMilliseconds(env['REFRESH_TOKEN_TTL'])}; Secure=${env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false}; SameSite=${env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict'}; Path=/;${domainHeader}${partitionedHeader}`;
64
+ const cookieHeaderValue = `${env['REFRESH_TOKEN_COOKIE_NAME']}=${cookieValue}; HttpOnly; Max-Age=${getMilliseconds(env['REFRESH_TOKEN_TTL'])}; Secure=${env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false}; SameSite=${env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict'}; Path=/auth;${domainHeader}${partitionedHeader}`;
62
65
  return cookieHeaderValue;
63
66
  };
67
+ export const ACCESS_COOKIE_OPTIONS = {
68
+ httpOnly: true,
69
+ domain: env['ACCESS_TOKEN_COOKIE_DOMAIN'],
70
+ maxAge: getMilliseconds(env['ACCESS_TOKEN_TTL']),
71
+ secure: env['ACCESS_TOKEN_COOKIE_SECURE'] ?? false,
72
+ sameSite: env['ACCESS_TOKEN_COOKIE_SAME_SITE'] || 'strict',
73
+ path: '/',
74
+ };
75
+ const { maxAge: accessCookieMaxAge, expires: accessCookieExpires, ...access_cookie_clear_options } = ACCESS_COOKIE_OPTIONS;
76
+ export const ACCESS_COOKIE_CLEAR_OPTIONS = access_cookie_clear_options;
64
77
  export const OAS_REQUIRED_SCHEMAS = ['Diff', 'Schema', 'Query', 'x-metadata'];
65
78
  /** Formats from which transformation is supported */
66
79
  export const SUPPORTED_IMAGE_TRANSFORM_FORMATS = ['image/jpeg', 'image/png', 'image/webp', 'image/tiff', 'image/avif'];
@@ -1,6 +1,6 @@
1
1
  import { Router } from 'express';
2
2
  import { createLDAPAuthRouter, createLocalAuthRouter, createOAuth2AuthRouter, createOpenIDAuthRouter, createSAMLAuthRouter, } from '../auth/drivers/index.js';
3
- import { DEFAULT_AUTH_PROVIDER, GET_SET_HEADER } from '../constants.js';
3
+ import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../constants.js';
4
4
  import env from '../env.js';
5
5
  import { InvalidPayloadException } from '../exceptions/index.js';
6
6
  import logger from '../logger.js';
@@ -55,6 +55,9 @@ router.post('/refresh', asyncHandler(async (req, res, next) => {
55
55
  accountability: accountability,
56
56
  schema: req.schema,
57
57
  });
58
+ if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
59
+ res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
60
+ }
58
61
  const currentRefreshToken = req.body.refresh_token || req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
59
62
  if (!currentRefreshToken) {
60
63
  throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
@@ -68,7 +71,8 @@ router.post('/refresh', asyncHandler(async (req, res, next) => {
68
71
  payload['data']['refresh_token'] = refreshToken;
69
72
  }
70
73
  if (mode === 'cookie') {
71
- res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
74
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
75
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
72
76
  }
73
77
  res.locals['payload'] = payload;
74
78
  return next();
@@ -93,13 +97,11 @@ router.post('/logout', asyncHandler(async (req, res, next) => {
93
97
  throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
94
98
  }
95
99
  await authenticationService.logout(currentRefreshToken);
100
+ if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
101
+ res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
102
+ }
96
103
  if (req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
97
- res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], {
98
- httpOnly: true,
99
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
100
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
101
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
102
- });
104
+ res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
103
105
  }
104
106
  return next();
105
107
  }), respond);
@@ -1,6 +1,7 @@
1
1
  import express from 'express';
2
2
  import Joi from 'joi';
3
- import { GET_SET_HEADER, UUID_REGEX } from '../constants.js';
3
+ import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS, UUID_REGEX } from '../constants.js';
4
+ import env from '../env.js';
4
5
  import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
5
6
  import { respond } from '../middleware/respond.js';
6
7
  import useCollection from '../middleware/use-collection.js';
@@ -24,7 +25,8 @@ router.post('/auth', asyncHandler(async (req, res, next) => {
24
25
  throw new InvalidPayloadException(error.message);
25
26
  }
26
27
  const { accessToken, refreshToken, expires } = await service.login(req.body);
27
- res.setHeader('Set-Cookie', GET_SET_HEADER(refreshToken));
28
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
29
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
28
30
  res.locals['payload'] = { data: { access_token: accessToken, expires } };
29
31
  return next();
30
32
  }), respond);
@@ -35,7 +35,7 @@ export default async function runAST(originalAST, schema, options) {
35
35
  return null;
36
36
  // Run the items through the special transforms
37
37
  const payloadService = new PayloadService(collection, { knex, schema });
38
- let items = await payloadService.processValues('read', rawItems);
38
+ let items = await payloadService.processValues('read', rawItems, query.alias ?? {});
39
39
  if (!items || (Array.isArray(items) && items.length === 0))
40
40
  return items;
41
41
  // Apply the `_in` filters to the nested collection batches
package/dist/env.js CHANGED
@@ -34,6 +34,10 @@ const allowedEnvironmentVars = [
34
34
  'KEY',
35
35
  'SECRET',
36
36
  'ACCESS_TOKEN_TTL',
37
+ 'ACCESS_TOKEN_COOKIE_DOMAIN',
38
+ 'ACCESS_TOKEN_COOKIE_SECURE',
39
+ 'ACCESS_TOKEN_COOKIE_SAME_SITE',
40
+ 'ACCESS_TOKEN_COOKIE_NAME',
37
41
  'REFRESH_TOKEN_TTL',
38
42
  'REFRESH_TOKEN_COOKIE_DOMAIN',
39
43
  'REFRESH_TOKEN_COOKIE_SECURE',
@@ -50,6 +54,7 @@ const allowedEnvironmentVars = [
50
54
  'HSTS_.+',
51
55
  // hashing
52
56
  'HASH_.+',
57
+ 'X_POWERED_BY_ENABLED',
53
58
  // cors
54
59
  'CORS_ENABLED',
55
60
  'CORS_ORIGIN',
@@ -143,6 +148,7 @@ const allowedEnvironmentVars = [
143
148
  'AUTH_.+_GROUP_SCOPE',
144
149
  'AUTH_.+_IDP.+',
145
150
  'AUTH_.+_SP.+',
151
+ 'AUTH_.+_REDIRECT_ALLOW_LIST',
146
152
  // extensions
147
153
  'PACKAGE_FILE_LOCATION',
148
154
  'EXTENSIONS_PATH',
@@ -211,6 +217,9 @@ const defaults = {
211
217
  RATE_LIMITER_GLOBAL_DURATION: 1,
212
218
  RATE_LIMITER_GLOBAL_STORE: 'memory',
213
219
  ACCESS_TOKEN_TTL: '15m',
220
+ ACCESS_TOKEN_COOKIE_SECURE: false,
221
+ ACCESS_TOKEN_COOKIE_SAME_SITE: 'lax',
222
+ ACCESS_TOKEN_COOKIE_NAME: 'directus_access_token',
214
223
  REFRESH_TOKEN_TTL: '7d',
215
224
  REFRESH_TOKEN_COOKIE_SECURE: false,
216
225
  REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
@@ -218,6 +227,7 @@ const defaults = {
218
227
  LOGIN_STALL_TIME: 500,
219
228
  SERVER_SHUTDOWN_TIMEOUT: 1000,
220
229
  ROOT_REDIRECT: './admin',
230
+ X_POWERED_BY_ENABLED: true,
221
231
  CORS_ENABLED: false,
222
232
  CORS_ORIGIN: false,
223
233
  CORS_METHODS: 'GET,POST,PATCH,DELETE',
@@ -2,7 +2,7 @@ import { isEqual } from 'lodash-es';
2
2
  import getDatabase from '../database/index.js';
3
3
  import emitter from '../emitter.js';
4
4
  import env from '../env.js';
5
- import { InvalidCredentialsException } from '../exceptions/index.js';
5
+ import { InvalidCredentialsException, TokenExpiredException } from '../exceptions/index.js';
6
6
  import asyncHandler from '../utils/async-handler.js';
7
7
  import { getIPFromReq } from '../utils/get-ip-from-req.js';
8
8
  import isDirectusJWT from '../utils/is-directus-jwt.js';
@@ -40,6 +40,20 @@ export const handler = async (req, _res, next) => {
40
40
  if (req.token) {
41
41
  if (isDirectusJWT(req.token)) {
42
42
  const payload = verifyAccessJWT(req.token, env['SECRET']);
43
+ // Try finding the user with a valid refresh token stored in the access token
44
+ const user = await database
45
+ .select({
46
+ session_expires: 's.expires',
47
+ user_id: 'u.id',
48
+ })
49
+ .from('directus_sessions AS s')
50
+ .leftJoin('directus_users AS u', 's.user', 'u.id')
51
+ .where('s.token', payload.refresh_token)
52
+ .andWhere('s.expires', '>=', new Date())
53
+ .first();
54
+ if (!user) {
55
+ throw new TokenExpiredException();
56
+ }
43
57
  req.accountability.role = payload.role;
44
58
  req.accountability.admin = payload.admin_access === true || payload.admin_access == 1;
45
59
  req.accountability.app = payload.app_access === true || payload.app_access == 1;
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Authorization: Bearer
5
5
  * access_token query parameter
6
+ * Access token cookie
6
7
  *
7
8
  * and store in req.token
8
9
  */
@@ -3,9 +3,11 @@
3
3
  *
4
4
  * Authorization: Bearer
5
5
  * access_token query parameter
6
+ * Access token cookie
6
7
  *
7
8
  * and store in req.token
8
9
  */
10
+ import env from '../env.js';
9
11
  const extractToken = (req, _res, next) => {
10
12
  let token = null;
11
13
  if (req.query && req.query['access_token']) {
@@ -17,6 +19,12 @@ const extractToken = (req, _res, next) => {
17
19
  token = parts[1];
18
20
  }
19
21
  }
22
+ /**
23
+ * Check if there is an access token stored in the cookies
24
+ */
25
+ if (req?.cookies?.[env['ACCESS_TOKEN_COOKIE_NAME']]) {
26
+ token = req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']];
27
+ }
20
28
  /**
21
29
  * @TODO
22
30
  * Look into RFC6750 compliance:
@@ -128,11 +128,22 @@ export class AuthenticationService {
128
128
  throw new InvalidOTPException(`"otp" is invalid`);
129
129
  }
130
130
  }
131
+ const refreshToken = nanoid(64);
132
+ const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
133
+ await this.knex('directus_sessions').insert({
134
+ token: refreshToken,
135
+ user: user.id,
136
+ expires: refreshTokenExpiration,
137
+ ip: this.accountability?.ip,
138
+ user_agent: this.accountability?.userAgent,
139
+ origin: this.accountability?.origin,
140
+ });
131
141
  const tokenPayload = {
132
142
  id: user.id,
133
143
  role: user.role,
134
144
  app_access: user.app_access,
135
145
  admin_access: user.admin_access,
146
+ refresh_token: refreshToken,
136
147
  };
137
148
  const customClaims = await emitter.emitFilter('auth.jwt', tokenPayload, {
138
149
  status: 'pending',
@@ -148,16 +159,6 @@ export class AuthenticationService {
148
159
  expiresIn: env['ACCESS_TOKEN_TTL'],
149
160
  issuer: 'directus',
150
161
  });
151
- const refreshToken = nanoid(64);
152
- const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
153
- await this.knex('directus_sessions').insert({
154
- token: refreshToken,
155
- user: user.id,
156
- expires: refreshTokenExpiration,
157
- ip: this.accountability?.ip,
158
- user_agent: this.accountability?.userAgent,
159
- origin: this.accountability?.origin,
160
- });
161
162
  await this.knex('directus_sessions').delete().where('expires', '<', new Date());
162
163
  if (this.accountability) {
163
164
  await this.activityService.createOne({
@@ -260,11 +261,20 @@ export class AuthenticationService {
260
261
  admin_access: record.role_admin_access,
261
262
  });
262
263
  }
264
+ const newRefreshToken = nanoid(64);
265
+ const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
266
+ await this.knex('directus_sessions')
267
+ .update({
268
+ token: newRefreshToken,
269
+ expires: refreshTokenExpiration,
270
+ })
271
+ .where({ token: refreshToken });
263
272
  const tokenPayload = {
264
273
  id: record.user_id,
265
274
  role: record.role_id,
266
275
  app_access: record.role_app_access,
267
276
  admin_access: record.role_admin_access,
277
+ refresh_token: newRefreshToken,
268
278
  };
269
279
  if (record.share_id) {
270
280
  tokenPayload.share = record.share_id;
@@ -291,14 +301,6 @@ export class AuthenticationService {
291
301
  expiresIn: env['ACCESS_TOKEN_TTL'],
292
302
  issuer: 'directus',
293
303
  });
294
- const newRefreshToken = nanoid(64);
295
- const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
296
- await this.knex('directus_sessions')
297
- .update({
298
- token: newRefreshToken,
299
- expires: refreshTokenExpiration,
300
- })
301
- .where({ token: refreshToken });
302
304
  if (record.user_id) {
303
305
  await this.knex('directus_users').update({ last_access: new Date() }).where({ id: record.user_id });
304
306
  }
@@ -5,14 +5,13 @@ import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID,
5
5
  import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
6
6
  import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
7
7
  import { clearSystemCache, getCache } from '../../cache.js';
8
- import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL } from '../../constants.js';
8
+ import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../../constants.js';
9
9
  import getDatabase from '../../database/index.js';
10
10
  import env from '../../env.js';
11
11
  import { ForbiddenException, GraphQLValidationException, InvalidPayloadException } from '../../exceptions/index.js';
12
12
  import { getExtensionManager } from '../../extensions.js';
13
13
  import { generateHash } from '../../utils/generate-hash.js';
14
14
  import { getGraphQLType } from '../../utils/get-graphql-type.js';
15
- import { getMilliseconds } from '../../utils/get-milliseconds.js';
16
15
  import { reduceSchema } from '../../utils/reduce-schema.js';
17
16
  import { sanitizeQuery } from '../../utils/sanitize-query.js';
18
17
  import { validateQuery } from '../../utils/validate-query.js';
@@ -1696,13 +1695,8 @@ export class GraphQLService {
1696
1695
  });
1697
1696
  const result = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, args?.otp);
1698
1697
  if (args['mode'] === 'cookie') {
1699
- res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], {
1700
- httpOnly: true,
1701
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
1702
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
1703
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
1704
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
1705
- });
1698
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], result['accessToken'], ACCESS_COOKIE_OPTIONS);
1699
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], REFRESH_COOKIE_OPTIONS);
1706
1700
  }
1707
1701
  return {
1708
1702
  access_token: result['accessToken'],
@@ -1737,13 +1731,8 @@ export class GraphQLService {
1737
1731
  }
1738
1732
  const result = await authenticationService.refresh(currentRefreshToken);
1739
1733
  if (args['mode'] === 'cookie') {
1740
- res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], {
1741
- httpOnly: true,
1742
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
1743
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
1744
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
1745
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
1746
- });
1734
+ res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], result['accessToken'], ACCESS_COOKIE_OPTIONS);
1735
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], REFRESH_COOKIE_OPTIONS);
1747
1736
  }
1748
1737
  return {
1749
1738
  access_token: result['accessToken'],
@@ -1757,7 +1746,7 @@ export class GraphQLService {
1757
1746
  args: {
1758
1747
  refresh_token: GraphQLString,
1759
1748
  },
1760
- resolve: async (_, args, { req }) => {
1749
+ resolve: async (_, args, { req, res }) => {
1761
1750
  const accountability = { role: null };
1762
1751
  if (req?.ip)
1763
1752
  accountability.ip = req.ip;
@@ -1776,6 +1765,12 @@ export class GraphQLService {
1776
1765
  throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
1777
1766
  }
1778
1767
  await authenticationService.logout(currentRefreshToken);
1768
+ if (req?.cookies?.[env['ACCESS_TOKEN_COOKIE_NAME']]) {
1769
+ res?.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
1770
+ }
1771
+ if (req?.cookies?.[env['REFRESH_TOKEN_COOKIE_NAME']]) {
1772
+ res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
1773
+ }
1779
1774
  return true;
1780
1775
  },
1781
1776
  },
@@ -27,6 +27,8 @@ export declare class PayloadService {
27
27
  transformers: Transformers;
28
28
  processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
29
29
  processValues(action: Action, payload: Partial<Item>): Promise<Partial<Item>>;
30
+ processValues(action: Action, payloads: Partial<Item>[], aliasMap: Record<string, string>): Promise<Partial<Item>[]>;
31
+ processValues(action: Action, payload: Partial<Item>, aliasMap: Record<string, string>): Promise<Partial<Item>>;
30
32
  processAggregates(payload: Partial<Item>[]): void;
31
33
  processField(field: SchemaOverview['collections'][string]['fields'][string], payload: Partial<Item>, action: Action, accountability: Accountability | null): Promise<any>;
32
34
  /**
@@ -121,12 +121,20 @@ export class PayloadService {
121
121
  return value;
122
122
  },
123
123
  };
124
- async processValues(action, payload) {
124
+ async processValues(action, payload, aliasMap = {}) {
125
125
  const processedPayload = toArray(payload);
126
126
  if (processedPayload.length === 0)
127
127
  return [];
128
128
  const fieldsInPayload = Object.keys(processedPayload[0]);
129
129
  let specialFieldsInCollection = Object.entries(this.schema.collections[this.collection].fields).filter(([_name, field]) => field.special && field.special.length > 0);
130
+ const aliasEntries = Object.entries(aliasMap);
131
+ for (const [name, field] of specialFieldsInCollection) {
132
+ for (const [aliasName, fieldName] of aliasEntries) {
133
+ if (fieldName === name) {
134
+ specialFieldsInCollection.push([aliasName, { ...field, field: aliasName }]);
135
+ }
136
+ }
137
+ }
130
138
  if (action === 'read') {
131
139
  specialFieldsInCollection = specialFieldsInCollection.filter(([name]) => {
132
140
  return fieldsInPayload.includes(name);
@@ -59,6 +59,17 @@ export class SharesService extends ItemsService {
59
59
  await this.knex('directus_shares')
60
60
  .update({ times_used: record.share_times_used + 1 })
61
61
  .where('id', record.share_id);
62
+ const refreshToken = nanoid(64);
63
+ const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
64
+ await this.knex('directus_sessions').insert({
65
+ token: refreshToken,
66
+ expires: refreshTokenExpiration,
67
+ ip: this.accountability?.ip,
68
+ user_agent: this.accountability?.userAgent,
69
+ origin: this.accountability?.origin,
70
+ share: record.share_id,
71
+ });
72
+ await this.knex('directus_sessions').delete().where('expires', '<', new Date());
62
73
  const tokenPayload = {
63
74
  app_access: false,
64
75
  admin_access: false,
@@ -68,22 +79,12 @@ export class SharesService extends ItemsService {
68
79
  item: record.share_item,
69
80
  collection: record.share_collection,
70
81
  },
82
+ refresh_token: refreshToken,
71
83
  };
72
84
  const accessToken = jwt.sign(tokenPayload, env['SECRET'], {
73
85
  expiresIn: env['ACCESS_TOKEN_TTL'],
74
86
  issuer: 'directus',
75
87
  });
76
- const refreshToken = nanoid(64);
77
- const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
78
- await this.knex('directus_sessions').insert({
79
- token: refreshToken,
80
- expires: refreshTokenExpiration,
81
- ip: this.accountability?.ip,
82
- user_agent: this.accountability?.userAgent,
83
- origin: this.accountability?.origin,
84
- share: record.share_id,
85
- });
86
- await this.knex('directus_sessions').delete().where('expires', '<', new Date());
87
88
  return {
88
89
  accessToken,
89
90
  refreshToken,
@@ -34,6 +34,7 @@ export type DirectusTokenPayload = {
34
34
  collection: string;
35
35
  item: string;
36
36
  };
37
+ refresh_token: string;
37
38
  };
38
39
  export type ShareData = {
39
40
  share_id: string;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Checks if the defined redirect after successful SSO login is in the allow list
3
+ */
4
+ export declare function isRedirectAllowedOnLogin(redirect: unknown, provider: string): boolean;
@@ -0,0 +1,42 @@
1
+ import { toArray } from '@wbce-d9/utils';
2
+ import env from '../env.js';
3
+ import isUrlAllowed from './is-url-allowed.js';
4
+ function isUrlValid(string) {
5
+ try {
6
+ new URL(string);
7
+ return true;
8
+ }
9
+ catch (err) {
10
+ return false;
11
+ }
12
+ }
13
+ /**
14
+ * Checks if the defined redirect after successful SSO login is in the allow list
15
+ */
16
+ export function isRedirectAllowedOnLogin(redirect, provider) {
17
+ if (!redirect)
18
+ return true; // empty redirect
19
+ if (typeof redirect !== 'string')
20
+ return false; // invalid type
21
+ const publicUrl = env['PUBLIC_URL'];
22
+ if (!isUrlValid(redirect)) {
23
+ if (!redirect.startsWith('//')) {
24
+ // should be a relative path like `/admin/test`
25
+ return true;
26
+ }
27
+ // domain without protocol `//example.com/test`
28
+ return false;
29
+ }
30
+ const { protocol: redirectProtocol, hostname: redirectDomain } = new URL(redirect);
31
+ const envKey = `AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`;
32
+ if (envKey in env) {
33
+ if (isUrlAllowed(redirect, [...toArray(env[envKey]), publicUrl]))
34
+ return true;
35
+ }
36
+ if (!isUrlValid(publicUrl)) {
37
+ return false;
38
+ }
39
+ // allow redirects to the defined PUBLIC_URL
40
+ const { protocol: publicProtocol, hostname: publicDomain } = new URL(publicUrl);
41
+ return `${redirectProtocol}//${redirectDomain}` === `${publicProtocol}//${publicDomain}`;
42
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
1
  /**
2
- * Check if url matches allow list either exactly or by domain+path
2
+ * Check if url matches allow list either exactly or by origin+pathname
3
3
  */
4
4
  export default function isUrlAllowed(url: string, allowList: string | string[]): boolean;
@@ -2,7 +2,7 @@ import { toArray } from '@wbce-d9/utils';
2
2
  import logger from '../logger.js';
3
3
  import { URL } from 'url';
4
4
  /**
5
- * Check if url matches allow list either exactly or by domain+path
5
+ * Check if url matches allow list either exactly or by origin+pathname
6
6
  */
7
7
  export default function isUrlAllowed(url, allowList) {
8
8
  const urlAllowList = toArray(allowList);
@@ -11,8 +11,8 @@ export default function isUrlAllowed(url, allowList) {
11
11
  const parsedWhitelist = urlAllowList
12
12
  .map((allowedURL) => {
13
13
  try {
14
- const { hostname, pathname } = new URL(allowedURL);
15
- return hostname + pathname;
14
+ const { origin, pathname } = new URL(allowedURL);
15
+ return origin + pathname;
16
16
  }
17
17
  catch {
18
18
  logger.warn(`Invalid URL used "${url}"`);
@@ -21,8 +21,8 @@ export default function isUrlAllowed(url, allowList) {
21
21
  })
22
22
  .filter((f) => f);
23
23
  try {
24
- const { hostname, pathname } = new URL(url);
25
- return parsedWhitelist.includes(hostname + pathname);
24
+ const { origin, pathname } = new URL(url);
25
+ return parsedWhitelist.includes(origin + pathname);
26
26
  }
27
27
  catch {
28
28
  return false;
@@ -0,0 +1 @@
1
+ export {};
package/dist/utils/jwt.js CHANGED
@@ -21,9 +21,9 @@ export function verifyJWT(token, secret) {
21
21
  return payload;
22
22
  }
23
23
  export function verifyAccessJWT(token, secret) {
24
- const { id, role, app_access, admin_access, share, share_scope } = verifyJWT(token, secret);
25
- if (role === undefined || app_access === undefined || admin_access === undefined) {
24
+ const { id, role, app_access, admin_access, share, share_scope, refresh_token } = verifyJWT(token, secret);
25
+ if (role === undefined || app_access === undefined || admin_access === undefined || refresh_token === undefined) {
26
26
  throw new InvalidTokenException('Invalid token payload.');
27
27
  }
28
- return { id, role, app_access, admin_access, share, share_scope };
28
+ return { id, role, app_access, admin_access, share, share_scope, refresh_token };
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbce-d9/api",
3
- "version": "10.3.0",
3
+ "version": "10.4.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -72,7 +72,7 @@
72
72
  "@wbce-d9/format-title": "10.0.0",
73
73
  "argon2": "0.30.3",
74
74
  "async": "3.2.4",
75
- "axios": "1.3.6",
75
+ "axios": "^1.7.2",
76
76
  "busboy": "1.6.0",
77
77
  "bytes": "3.1.2",
78
78
  "camelcase": "7.0.1",
@@ -143,20 +143,20 @@
143
143
  "uuid-validate": "0.0.3",
144
144
  "vm2": "^3.9.19",
145
145
  "wellknown": "0.5.0",
146
- "@wbce-d9/app": "9.29.0",
147
- "@wbce-d9/constants": "9.26.3",
148
- "@wbce-d9/exceptions": "9.26.3",
149
- "@wbce-d9/extensions-sdk": "9.26.5",
150
- "@wbce-d9/schema": "9.26.3",
151
- "@wbce-d9/storage-driver-azure": "9.26.5",
152
- "@wbce-d9/specs": "9.26.3",
153
- "@wbce-d9/storage": "9.26.3",
154
- "@wbce-d9/storage-driver-cloudinary": "9.26.5",
155
- "@wbce-d9/storage-driver-gcs": "9.26.5",
156
- "@wbce-d9/storage-driver-s3": "9.26.7",
157
- "@wbce-d9/update-check": "9.26.3",
158
- "@wbce-d9/utils": "9.28.0",
159
- "@wbce-d9/storage-driver-local": "9.26.5"
146
+ "@wbce-d9/app": "9.30.0",
147
+ "@wbce-d9/exceptions": "9.26.4",
148
+ "@wbce-d9/constants": "9.27.0",
149
+ "@wbce-d9/extensions-sdk": "9.26.7",
150
+ "@wbce-d9/schema": "9.26.4",
151
+ "@wbce-d9/storage": "9.26.4",
152
+ "@wbce-d9/specs": "9.26.4",
153
+ "@wbce-d9/storage-driver-azure": "9.26.7",
154
+ "@wbce-d9/storage-driver-cloudinary": "9.26.7",
155
+ "@wbce-d9/storage-driver-gcs": "9.26.7",
156
+ "@wbce-d9/update-check": "9.26.4",
157
+ "@wbce-d9/storage-driver-local": "9.26.7",
158
+ "@wbce-d9/storage-driver-s3": "9.26.9",
159
+ "@wbce-d9/utils": "9.29.0"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@ngneat/falso": "6.4.0",
@@ -204,7 +204,7 @@
204
204
  "supertest": "6.3.3",
205
205
  "typescript": "5.0.4",
206
206
  "vitest": "0.30.1",
207
- "@wbce-d9/types": "9.26.3"
207
+ "@wbce-d9/types": "9.27.0"
208
208
  },
209
209
  "optionalDependencies": {
210
210
  "@keyv/redis": "2.5.7",