@directus/api 27.0.1 → 27.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.
@@ -311,6 +311,7 @@ export function createOpenIDAuthRouter(providerName) {
311
311
  res.redirect(303, `./callback?${new URLSearchParams(req.body)}`);
312
312
  }, respond);
313
313
  router.get('/callback', asyncHandler(async (req, res, next) => {
314
+ const env = useEnv();
314
315
  const logger = useLogger();
315
316
  let tokenData;
316
317
  try {
@@ -318,7 +319,8 @@ export function createOpenIDAuthRouter(providerName) {
318
319
  }
319
320
  catch (e) {
320
321
  logger.warn(e, `[OpenID] Couldn't verify OpenID cookie`);
321
- throw new InvalidCredentialsError();
322
+ const url = new Url(env['PUBLIC_URL']).addPath('admin', 'login');
323
+ return res.redirect(`${url.toString()}?reason=${ErrorCode.InvalidCredentials}`);
322
324
  }
323
325
  const { verifier, redirect, prompt } = tokenData;
324
326
  const accountability = createDefaultAccountability({ ip: getIPFromReq(req) });
@@ -1,9 +1,9 @@
1
1
  import type { Accountability, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  export interface ConvertWildcardsOptions {
4
- parentCollection: string;
4
+ collection: string;
5
5
  fields: string[];
6
- query: Query;
6
+ alias: Query['alias'];
7
7
  accountability: Accountability | null;
8
8
  }
9
9
  export interface ConvertWildCardsContext {
@@ -1,13 +1,14 @@
1
1
  import { getRelation } from '@directus/utils';
2
2
  import { cloneDeep } from 'lodash-es';
3
3
  import { fetchAllowedFields } from '../../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
4
+ import { parseFilterKey } from '../../../utils/parse-filter-key.js';
4
5
  export async function convertWildcards(options, context) {
5
6
  const fields = cloneDeep(options.fields);
6
- const fieldsInCollection = Object.entries(context.schema.collections[options.parentCollection].fields).map(([name]) => name);
7
+ const fieldsInCollection = Object.entries(context.schema.collections[options.collection].fields).map(([name]) => name);
7
8
  let allowedFields = fieldsInCollection;
8
9
  if (options.accountability && options.accountability.admin === false) {
9
10
  allowedFields = await fetchAllowedFields({
10
- collection: options.parentCollection,
11
+ collection: options.collection,
11
12
  action: 'read',
12
13
  accountability: options.accountability,
13
14
  }, context);
@@ -22,7 +23,7 @@ export async function convertWildcards(options, context) {
22
23
  if (fieldKey.includes('*') === false)
23
24
  continue;
24
25
  if (fieldKey === '*') {
25
- const aliases = Object.keys(options.query.alias ?? {});
26
+ const aliases = Object.keys(options.alias ?? {});
26
27
  // Set to all fields in collection
27
28
  if (allowedFields.includes('*')) {
28
29
  fields.splice(index, 1, ...fieldsInCollection, ...aliases);
@@ -30,8 +31,8 @@ export async function convertWildcards(options, context) {
30
31
  else {
31
32
  // Set to all allowed fields
32
33
  const allowedAliases = aliases.filter((fieldKey) => {
33
- const name = options.query.alias[fieldKey];
34
- return allowedFields.includes(name);
34
+ const { fieldName } = parseFilterKey(options.alias[fieldKey]);
35
+ return allowedFields.includes(fieldName);
35
36
  });
36
37
  fields.splice(index, 1, ...allowedFields, ...allowedAliases);
37
38
  }
@@ -41,16 +42,15 @@ export async function convertWildcards(options, context) {
41
42
  const parts = fieldKey.split('.');
42
43
  const relationalFields = allowedFields.includes('*')
43
44
  ? context.schema.relations
44
- .filter((relation) => relation.collection === options.parentCollection ||
45
- relation.related_collection === options.parentCollection)
45
+ .filter((relation) => relation.collection === options.collection || relation.related_collection === options.collection)
46
46
  .map((relation) => {
47
- const isMany = relation.collection === options.parentCollection;
47
+ const isMany = relation.collection === options.collection;
48
48
  return isMany ? relation.field : relation.meta?.one_field;
49
49
  })
50
- : allowedFields.filter((fieldKey) => !!getRelation(context.schema.relations, options.parentCollection, fieldKey));
50
+ : allowedFields.filter((fieldKey) => !!getRelation(context.schema.relations, options.collection, fieldKey));
51
51
  const nonRelationalFields = allowedFields.filter((fieldKey) => relationalFields.includes(fieldKey) === false);
52
- const aliasFields = Object.keys(options.query.alias ?? {}).map((fieldKey) => {
53
- const name = options.query.alias[fieldKey];
52
+ const aliasFields = Object.keys(options.alias ?? {}).map((fieldKey) => {
53
+ const name = options.alias[fieldKey];
54
54
  if (relationalFields.includes(name)) {
55
55
  return `${fieldKey}.${parts.slice(1).join('.')}`;
56
56
  }
@@ -14,8 +14,8 @@ export async function parseFields(options, context) {
14
14
  return [];
15
15
  fields = await convertWildcards({
16
16
  fields,
17
- parentCollection: options.parentCollection,
18
- query: options.query,
17
+ collection: options.parentCollection,
18
+ alias: options.query.alias,
19
19
  accountability: options.accountability,
20
20
  }, context);
21
21
  if (!fields || !Array.isArray(fields))
@@ -169,13 +169,25 @@ export class ExtensionManager {
169
169
  * Installs an external extension from registry
170
170
  */
171
171
  async install(versionId) {
172
+ const logger = useLogger();
172
173
  await this.installationManager.install(versionId);
173
174
  await this.reload({ forceSync: true });
175
+ emitter.emitAction('extensions.installed', {
176
+ extensions: this.extensions,
177
+ versionId,
178
+ });
179
+ logger.info(`Installed extension: ${versionId}`);
174
180
  await this.broadcastReloadNotification();
175
181
  }
176
182
  async uninstall(folder) {
183
+ const logger = useLogger();
177
184
  await this.installationManager.uninstall(folder);
178
185
  await this.reload({ forceSync: true });
186
+ emitter.emitAction('extensions.uninstalled', {
187
+ extensions: this.extensions,
188
+ folder,
189
+ });
190
+ logger.info(`Uninstalled extension: ${folder}`);
179
191
  await this.broadcastReloadNotification();
180
192
  }
181
193
  async broadcastReloadNotification() {
@@ -211,6 +223,10 @@ export class ExtensionManager {
211
223
  this.appExtensionsBundle = await this.generateExtensionBundle();
212
224
  }
213
225
  this.isLoaded = true;
226
+ emitter.emitAction('extensions.load', {
227
+ extensions: this.extensions,
228
+ });
229
+ logger.info('Extensions loaded');
214
230
  }
215
231
  /**
216
232
  * Unregister all extensions from the current process
@@ -220,6 +236,11 @@ export class ExtensionManager {
220
236
  this.localEmitter.offAll();
221
237
  this.appExtensionsBundle = null;
222
238
  this.isLoaded = false;
239
+ emitter.emitAction('extensions.unload', {
240
+ extensions: this.extensions,
241
+ });
242
+ const logger = useLogger();
243
+ logger.info('Extensions unloaded');
223
244
  }
224
245
  /**
225
246
  * Reload all the extensions. Will unload if extensions have already been loaded
@@ -247,6 +268,11 @@ export class ExtensionManager {
247
268
  this.updateWatchedExtensions(added, removed);
248
269
  const addedExtensions = added.map((extension) => extension.name);
249
270
  const removedExtensions = removed.map((extension) => extension.name);
271
+ emitter.emitAction('extensions.reload', {
272
+ extensions: this.extensions,
273
+ added: addedExtensions,
274
+ removed: removedExtensions,
275
+ });
250
276
  if (addedExtensions.length > 0) {
251
277
  logger.info(`Added extensions: ${addedExtensions.join(', ')}`);
252
278
  }
@@ -4,7 +4,7 @@ import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '
4
4
  export default defineOperationApi({
5
5
  id: 'condition',
6
6
  handler: ({ filter }, { data, accountability }) => {
7
- const parsedFilter = parseFilter(filter, accountability);
7
+ const parsedFilter = parseFilter(filter, accountability, undefined, true);
8
8
  if (!parsedFilter) {
9
9
  return null;
10
10
  }
@@ -285,7 +285,7 @@ export class PayloadService {
285
285
  payload[name] = newValue;
286
286
  }
287
287
  if (dateColumn.type === 'dateTime') {
288
- const year = String(value.getFullYear());
288
+ const year = String(value.getFullYear()).padStart(4, '0');
289
289
  const month = String(value.getMonth() + 1).padStart(2, '0');
290
290
  const day = String(value.getDate()).padStart(2, '0');
291
291
  const hours = String(value.getHours()).padStart(2, '0');
@@ -295,7 +295,7 @@ export class PayloadService {
295
295
  payload[name] = newValue;
296
296
  }
297
297
  if (dateColumn.type === 'date') {
298
- const year = String(value.getFullYear());
298
+ const year = String(value.getFullYear()).padStart(4, '0');
299
299
  const month = String(value.getMonth() + 1).padStart(2, '0');
300
300
  const day = String(value.getDate()).padStart(2, '0');
301
301
  // Strip off the time / timezone information from a date-only value
@@ -16,7 +16,7 @@ export declare class RelationsService {
16
16
  helpers: Helpers;
17
17
  constructor(options: AbstractServiceOptions);
18
18
  foreignKeys(collection?: string): Promise<ForeignKey[]>;
19
- readAll(collection?: string, opts?: QueryOptions): Promise<Relation[]>;
19
+ readAll(collection?: string, opts?: QueryOptions, bypassCache?: boolean): Promise<Relation[]>;
20
20
  readOne(collection: string, field: string): Promise<Relation>;
21
21
  /**
22
22
  * Create a new relationship / foreign key constraint
@@ -58,7 +58,7 @@ export class RelationsService {
58
58
  }
59
59
  return foreignKeys;
60
60
  }
61
- async readAll(collection, opts) {
61
+ async readAll(collection, opts, bypassCache) {
62
62
  if (this.accountability) {
63
63
  await validateAccess({
64
64
  accountability: this.accountability,
@@ -87,7 +87,10 @@ export class RelationsService {
87
87
  return true;
88
88
  return metaRow.many_collection === collection;
89
89
  });
90
- const schemaRows = await this.foreignKeys(collection);
90
+ let schemaRows = bypassCache ? await this.schemaInspector.foreignKeys() : await this.foreignKeys(collection);
91
+ if (collection && bypassCache) {
92
+ schemaRows = schemaRows.filter((row) => row.table === collection);
93
+ }
91
94
  const results = this.stitchRelations(metaRows, schemaRows);
92
95
  return await this.filterForbidden(results);
93
96
  }
@@ -1,2 +1,3 @@
1
1
  import type { Request } from 'express';
2
- export declare function getIPFromReq(req: Request): string | null;
2
+ import type { IncomingMessage } from 'http';
3
+ export declare function getIPFromReq(req: IncomingMessage | Request): string | null;
@@ -1,12 +1,39 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { isIP } from 'net';
3
+ import proxyAddr from 'proxy-addr';
3
4
  import { useLogger } from '../logger/index.js';
5
+ /**
6
+ * Generate the trusted ip list
7
+ *
8
+ * Adapted to have feature parity with the express equivalent https://github.com/expressjs/express/blob/9f4dbe3a1332cd883069ba9b73a9eed99234cfc7/lib/utils.js#L192
9
+ */
10
+ function getTrustValue(trust) {
11
+ if (typeof trust === 'boolean') {
12
+ // Support plain true/false
13
+ return (_addr, _i) => trust;
14
+ }
15
+ else if (typeof trust === 'number') {
16
+ // Support trusting hop count
17
+ return (_addr, i) => i < trust;
18
+ }
19
+ else if (typeof trust === 'string') {
20
+ // Support comma-separated values
21
+ trust = trust.split(',').map((v) => v.trim());
22
+ }
23
+ return proxyAddr.compile(trust || []);
24
+ }
4
25
  export function getIPFromReq(req) {
5
26
  const env = useEnv();
6
27
  const logger = useLogger();
7
- let ip = req.ip;
28
+ let ip = 'ip' in req ? req.ip : proxyAddr(req, getTrustValue(env['IP_TRUST_PROXY']));
8
29
  if (env['IP_CUSTOM_HEADER']) {
9
- const customIPHeaderValue = req.get(env['IP_CUSTOM_HEADER']);
30
+ const customIPHeaderName = env['IP_CUSTOM_HEADER'].toLowerCase();
31
+ // All req.headers are auto lower-cased
32
+ let customIPHeaderValue = req.headers[customIPHeaderName];
33
+ // // Done to have feature parity with https://github.com/expressjs/express/blob/9f4dbe3a1332cd883069ba9b73a9eed99234cfc7/lib/request.js#L63
34
+ if (customIPHeaderName === 'referer' || customIPHeaderName === 'referrer') {
35
+ customIPHeaderValue = req.headers['referrer'] || req.headers['referer'];
36
+ }
10
37
  if (typeof customIPHeaderValue === 'string' && isIP(customIPHeaderValue) !== 0) {
11
38
  ip = customIPHeaderValue;
12
39
  }
@@ -162,6 +162,6 @@ async function getDatabaseSchema(database, schemaInspector) {
162
162
  };
163
163
  }
164
164
  const relationsService = new RelationsService({ knex: database, schema: result });
165
- result.relations = await relationsService.readAll();
165
+ result.relations = await relationsService.readAll(undefined, undefined, true);
166
166
  return result;
167
167
  }
@@ -22,8 +22,8 @@ export default abstract class SocketController {
22
22
  protected getRateLimiter(): RateLimiterAbstract | null;
23
23
  private catchInvalidMessages;
24
24
  protected handleUpgrade(request: IncomingMessage, socket: internal.Duplex, head: Buffer): Promise<void>;
25
- protected handleTokenUpgrade({ request, socket, head }: UpgradeContext, token: string | null): Promise<void>;
26
- protected handleHandshakeUpgrade({ request, socket, head }: UpgradeContext): Promise<void>;
25
+ protected handleTokenUpgrade({ request, socket, head, accountabilityOverrides }: UpgradeContext, token: string | null): Promise<void>;
26
+ protected handleHandshakeUpgrade({ request, socket, head, accountabilityOverrides }: UpgradeContext): Promise<void>;
27
27
  createClient(ws: WebSocket, { accountability, expires_at }: AuthenticationState): WebSocketClient;
28
28
  protected parseMessage(data: string): WebSocketMessage;
29
29
  protected handleAuthRequest(client: WebSocketClient, message: WebSocketAuthMessage): Promise<void>;
@@ -8,15 +8,16 @@ import WebSocket, { WebSocketServer } from 'ws';
8
8
  import { fromZodError } from 'zod-validation-error';
9
9
  import emitter from '../../emitter.js';
10
10
  import { useLogger } from '../../logger/index.js';
11
+ import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
11
12
  import { createRateLimiter } from '../../rate-limiter.js';
12
13
  import { getAccountabilityForToken } from '../../utils/get-accountability-for-token.js';
14
+ import { getIPFromReq } from '../../utils/get-ip-from-req.js';
13
15
  import { authenticateConnection, authenticationSuccess } from '../authenticate.js';
14
16
  import { WebSocketError, handleWebSocketError } from '../errors.js';
15
17
  import { AuthMode, WebSocketAuthMessage, WebSocketMessage } from '../messages.js';
16
18
  import { getExpiresAtForToken } from '../utils/get-expires-at-for-token.js';
17
19
  import { getMessageType } from '../utils/message.js';
18
20
  import { waitForAnyMessage, waitForMessageType } from '../utils/wait-for-message.js';
19
- import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
20
21
  const TOKEN_CHECK_INTERVAL = 15 * 60 * 1000; // 15 minutes
21
22
  const logger = useLogger();
22
23
  export default class SocketController {
@@ -98,8 +99,17 @@ export default class SocketController {
98
99
  }
99
100
  const env = useEnv();
100
101
  const cookies = request.headers.cookie ? cookie.parse(request.headers.cookie) : {};
101
- const context = { request, socket, head };
102
102
  const sessionCookieName = env['SESSION_COOKIE_NAME'];
103
+ const accountabilityOverrides = {
104
+ ip: getIPFromReq(request) ?? null,
105
+ };
106
+ const userAgent = request.headers['user-agent']?.substring(0, 1024);
107
+ if (userAgent)
108
+ accountabilityOverrides.userAgent = userAgent;
109
+ const origin = request.headers['origin'];
110
+ if (origin)
111
+ accountabilityOverrides.origin = origin;
112
+ const context = { request, socket, head, accountabilityOverrides };
103
113
  if (this.authentication.mode === 'strict' || query['access_token'] || cookies[sessionCookieName]) {
104
114
  let token = null;
105
115
  if (typeof query['access_token'] === 'string') {
@@ -117,11 +127,14 @@ export default class SocketController {
117
127
  }
118
128
  this.server.handleUpgrade(request, socket, head, async (ws) => {
119
129
  this.catchInvalidMessages(ws);
120
- const state = { accountability: createDefaultAccountability(), expires_at: null };
130
+ const state = {
131
+ accountability: createDefaultAccountability(accountabilityOverrides),
132
+ expires_at: null,
133
+ };
121
134
  this.server.emit('connection', ws, state);
122
135
  });
123
136
  }
124
- async handleTokenUpgrade({ request, socket, head }, token) {
137
+ async handleTokenUpgrade({ request, socket, head, accountabilityOverrides }, token) {
125
138
  let accountability = null;
126
139
  let expires_at = null;
127
140
  if (token) {
@@ -149,13 +162,14 @@ export default class SocketController {
149
162
  socket.destroy();
150
163
  return;
151
164
  }
165
+ Object.assign(accountability, accountabilityOverrides);
152
166
  this.server.handleUpgrade(request, socket, head, async (ws) => {
153
167
  this.catchInvalidMessages(ws);
154
168
  const state = { accountability, expires_at };
155
169
  this.server.emit('connection', ws, state);
156
170
  });
157
171
  }
158
- async handleHandshakeUpgrade({ request, socket, head }) {
172
+ async handleHandshakeUpgrade({ request, socket, head, accountabilityOverrides }) {
159
173
  this.server.handleUpgrade(request, socket, head, async (ws) => {
160
174
  this.catchInvalidMessages(ws);
161
175
  try {
@@ -163,6 +177,9 @@ export default class SocketController {
163
177
  if (getMessageType(payload) !== 'auth')
164
178
  throw new Error();
165
179
  const state = await authenticateConnection(WebSocketAuthMessage.parse(payload));
180
+ if (state.accountability) {
181
+ Object.assign(state.accountability, accountabilityOverrides);
182
+ }
166
183
  this.checkUserRequirements(state.accountability);
167
184
  ws.send(authenticationSuccess(payload['uid'], state.refresh_token));
168
185
  this.server.emit('connection', ws, state);
@@ -253,6 +270,17 @@ export default class SocketController {
253
270
  try {
254
271
  const { accountability, expires_at, refresh_token } = await authenticateConnection(message);
255
272
  this.checkUserRequirements(accountability);
273
+ /**
274
+ * Re-use the existing ip, userAgent and origin accountability properties.
275
+ * They are only sent in the original connection request
276
+ */
277
+ if (accountability && client.accountability) {
278
+ Object.assign(accountability, {
279
+ ip: client.accountability.ip,
280
+ userAgent: client.accountability.userAgent,
281
+ origin: client.accountability.origin,
282
+ });
283
+ }
256
284
  client.accountability = accountability;
257
285
  client.expires_at = expires_at;
258
286
  this.setTokenExpireTimer(client);
@@ -30,6 +30,7 @@ export type UpgradeContext = {
30
30
  request: IncomingMessage;
31
31
  socket: internal.Duplex;
32
32
  head: Buffer;
33
+ accountabilityOverrides: Pick<Accountability, 'ip' | 'userAgent' | 'origin'>;
33
34
  };
34
35
  export type GraphQLSocket = {
35
36
  client: WebSocketClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "27.0.1",
3
+ "version": "27.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",
@@ -136,10 +136,11 @@
136
136
  "pino-pretty": "13.0.0",
137
137
  "pm2": "5.4.3",
138
138
  "prom-client": "15.1.3",
139
+ "proxy-addr": "2.0.7",
139
140
  "qs": "6.14.0",
140
141
  "rate-limiter-flexible": "5.0.5",
141
142
  "rollup": "4.34.9",
142
- "samlify": "2.9.1",
143
+ "samlify": "2.10.0",
143
144
  "sanitize-html": "2.14.0",
144
145
  "sharp": "0.33.5",
145
146
  "snappy": "7.2.2",
@@ -150,30 +151,30 @@
150
151
  "ws": "8.18.1",
151
152
  "zod": "3.24.2",
152
153
  "zod-validation-error": "3.4.0",
153
- "@directus/app": "13.9.1",
154
- "@directus/constants": "13.0.1",
155
- "@directus/env": "5.0.4",
154
+ "@directus/app": "13.10.0",
155
+ "@directus/env": "5.0.5",
156
156
  "@directus/errors": "2.0.1",
157
- "@directus/extensions-registry": "3.0.5",
158
- "@directus/extensions-sdk": "13.1.0",
159
- "@directus/extensions": "3.0.5",
157
+ "@directus/extensions": "3.0.6",
158
+ "@directus/constants": "13.0.1",
159
+ "@directus/extensions-registry": "3.0.6",
160
+ "@directus/pressure": "3.0.5",
160
161
  "@directus/format-title": "12.0.1",
161
- "@directus/memory": "3.0.4",
162
+ "@directus/memory": "3.0.5",
163
+ "@directus/extensions-sdk": "13.1.1",
162
164
  "@directus/schema": "13.0.1",
163
- "@directus/schema-builder": "0.0.0",
164
- "@directus/pressure": "3.0.4",
165
- "@directus/storage": "12.0.0",
166
- "@directus/storage-driver-azure": "12.0.4",
167
- "@directus/storage-driver-cloudinary": "12.0.4",
165
+ "@directus/schema-builder": "0.0.2",
168
166
  "@directus/specs": "11.1.0",
169
- "@directus/storage-driver-gcs": "12.0.4",
167
+ "@directus/storage": "12.0.0",
168
+ "@directus/storage-driver-cloudinary": "12.0.5",
169
+ "@directus/storage-driver-gcs": "12.0.5",
170
170
  "@directus/storage-driver-local": "12.0.0",
171
- "@directus/storage-driver-s3": "12.0.4",
172
- "@directus/storage-driver-supabase": "3.0.4",
173
- "@directus/utils": "13.0.4",
171
+ "@directus/storage-driver-azure": "12.0.5",
172
+ "@directus/storage-driver-s3": "12.0.5",
173
+ "@directus/storage-driver-supabase": "3.0.5",
174
174
  "@directus/system-data": "3.1.0",
175
- "directus": "11.7.1",
176
- "@directus/validation": "2.0.4"
175
+ "@directus/utils": "13.0.6",
176
+ "directus": "11.8.0",
177
+ "@directus/validation": "2.0.5"
177
178
  },
178
179
  "devDependencies": {
179
180
  "@directus/tsconfig": "3.0.0",
@@ -204,6 +205,7 @@
204
205
  "@types/nodemailer": "6.4.17",
205
206
  "@types/object-hash": "3.0.6",
206
207
  "@types/papaparse": "5.3.15",
208
+ "@types/proxy-addr": "2.0.3",
207
209
  "@types/qs": "6.9.18",
208
210
  "@types/sanitize-html": "2.13.0",
209
211
  "@types/stream-json": "1.7.8",
@@ -217,8 +219,8 @@
217
219
  "typescript": "5.8.2",
218
220
  "vitest": "2.1.9",
219
221
  "@directus/random": "2.0.1",
220
- "@directus/types": "13.1.1",
221
- "@directus/schema-builder": "0.0.0"
222
+ "@directus/schema-builder": "0.0.2",
223
+ "@directus/types": "13.1.2"
222
224
  },
223
225
  "optionalDependencies": {
224
226
  "@keyv/redis": "3.0.1",
@@ -233,7 +235,7 @@
233
235
  "node": ">=22"
234
236
  },
235
237
  "scripts": {
236
- "build": "tsc --project tsconfig.prod.json && copyfiles \"src/**/*.{yaml,liquid}\" -u 1 dist",
238
+ "build": "rimraf ./dist && tsc --project tsconfig.prod.json && copyfiles \"src/**/*.{yaml,liquid}\" -u 1 dist",
237
239
  "cli": "NODE_ENV=development SERVE_APP=false tsx src/cli/run.ts",
238
240
  "dev": "NODE_ENV=development SERVE_APP=true tsx watch --ignore extensions --clear-screen=false src/start.ts",
239
241
  "test": "vitest run",