@solidxai/core 0.1.13-beta.20 → 0.1.13-beta.21

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +590 -0
  2. package/CLAUDE.md +5 -6
  3. package/dist/constants/media-file-types.d.ts.map +1 -1
  4. package/dist/constants/media-file-types.js +1 -0
  5. package/dist/constants/media-file-types.js.map +1 -1
  6. package/dist/dtos/create-field-metadata.dto.d.ts +1 -0
  7. package/dist/dtos/create-field-metadata.dto.d.ts.map +1 -1
  8. package/dist/dtos/create-field-metadata.dto.js +12 -1
  9. package/dist/dtos/create-field-metadata.dto.js.map +1 -1
  10. package/dist/entities/field-metadata.entity.d.ts +1 -0
  11. package/dist/entities/field-metadata.entity.d.ts.map +1 -1
  12. package/dist/entities/field-metadata.entity.js +5 -1
  13. package/dist/entities/field-metadata.entity.js.map +1 -1
  14. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts +6 -0
  15. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
  16. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +54 -27
  17. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
  18. package/dist/services/chatter-message.service.d.ts +1 -0
  19. package/dist/services/chatter-message.service.d.ts.map +1 -1
  20. package/dist/services/chatter-message.service.js +44 -12
  21. package/dist/services/chatter-message.service.js.map +1 -1
  22. package/dist/services/crud.service.d.ts.map +1 -1
  23. package/dist/services/crud.service.js +1 -0
  24. package/dist/services/crud.service.js.map +1 -1
  25. package/dist/services/field-metadata.service.d.ts.map +1 -1
  26. package/dist/services/field-metadata.service.js +2 -0
  27. package/dist/services/field-metadata.service.js.map +1 -1
  28. package/dist/services/media.service.d.ts.map +1 -1
  29. package/dist/services/media.service.js +1 -0
  30. package/dist/services/media.service.js.map +1 -1
  31. package/dist/services/module-metadata.service.d.ts.map +1 -1
  32. package/dist/services/module-metadata.service.js +7 -0
  33. package/dist/services/module-metadata.service.js.map +1 -1
  34. package/dist/services/setting.service.d.ts.map +1 -1
  35. package/dist/services/setting.service.js +4 -0
  36. package/dist/services/setting.service.js.map +1 -1
  37. package/dist/services/settings/default-settings-provider.service.d.ts +20 -0
  38. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  39. package/dist/services/settings/default-settings-provider.service.js +14 -0
  40. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  41. package/dist/solid-core.module.d.ts.map +1 -1
  42. package/dist/solid-core.module.js +6 -0
  43. package/dist/solid-core.module.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/constants/media-file-types.ts +1 -0
  46. package/src/dtos/create-field-metadata.dto.ts +14 -1
  47. package/src/entities/field-metadata.entity.ts +3 -0
  48. package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +73 -34
  49. package/src/services/chatter-message.service.ts +56 -14
  50. package/src/services/crud.service.ts +1 -0
  51. package/src/services/field-metadata.service.ts +2 -0
  52. package/src/services/media.service.ts +1 -0
  53. package/src/services/module-metadata.service.ts +7 -0
  54. package/src/services/setting.service.ts +5 -0
  55. package/src/services/settings/default-settings-provider.service.ts +14 -0
  56. package/src/solid-core.module.ts +13 -0
  57. package/postman/mpin.postman_collection.json +0 -1045
@@ -12,6 +12,7 @@ export interface MediaFieldOptions {
12
12
  fieldName: string | undefined | null;
13
13
  mediaMaxSizeKb: number | undefined | null;
14
14
  mediaTypes: string[];
15
+ mediaAllowedExtensions?: string[] | null;
15
16
  isUpdate: boolean | undefined | null;
16
17
  }
17
18
 
@@ -86,24 +87,28 @@ export class MediaFieldCrudManager implements FieldCrudManager {
86
87
  return null;
87
88
  }
88
89
 
90
+ // Extension is authoritative: a declared mimetype is fully attacker-controlled and
91
+ // proved insufficient on its own (a .msi uploaded as image/png previously resolved to
92
+ // 'image', since the mimetype match short-circuited before the extension was ever
93
+ // checked). The extension must independently resolve to a known-safe category before
94
+ // anything is accepted - not merely "not on the denylist".
89
95
  const ext = getLowercaseFileExtension(getUploadedFileName(file));
90
- const mt = (file.mimetype || '').toLowerCase().trim();
91
- if (mt && MIME_TO_MEDIA_TYPE[mt]) {
92
- return MIME_TO_MEDIA_TYPE[mt];
96
+ const extType = ext ? EXT_TO_MEDIA_TYPE[ext] : undefined;
97
+ if (!extType) {
98
+ return null;
93
99
  }
94
100
 
95
- // Some libs may send "image/*" etc. Treat broad families safely.
96
- if (mt.startsWith('image/')) return 'image';
97
- if (mt.startsWith('audio/')) return 'audio';
98
- if (mt.startsWith('video/')) return 'video';
99
-
100
- // Fallback to extension if provided (also covers generic/unrecognized
101
- // mimetypes such as application/octet-stream).
102
- if (ext && EXT_TO_MEDIA_TYPE[ext]) {
103
- return EXT_TO_MEDIA_TYPE[ext];
101
+ // The mimetype may only confirm or veto what the extension already established, never
102
+ // grant a category on its own. A mimetype that doesn't resolve to anything (e.g.
103
+ // application/octet-stream) has no opinion and is treated as agreeing.
104
+ const mt = (file.mimetype || '').toLowerCase().trim();
105
+ const mimeType = MIME_TO_MEDIA_TYPE[mt]
106
+ ?? (mt.startsWith('image/') ? 'image' : mt.startsWith('audio/') ? 'audio' : mt.startsWith('video/') ? 'video' : undefined);
107
+ if (mimeType && mimeType !== extType) {
108
+ return null;
104
109
  }
105
110
 
106
- return null;
111
+ return extType;
107
112
  }
108
113
 
109
114
  validate(dto: any, files: Array<Express.Multer.File>): ValidationError[] {
@@ -112,18 +117,17 @@ export class MediaFieldCrudManager implements FieldCrudManager {
112
117
  }
113
118
 
114
119
  private applyValidations(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
115
- // Runs for every media field type and regardless of the admin-configured mediaTypes
116
- // allowlist - a dangerous upload is never acceptable, so this must not sit behind
117
- // the optional type checks below.
118
- const errors = this.validateDangerousFiles(fieldFiles);
120
+ // A dangerous upload is never acceptable, regardless of admin config, so this is the
121
+ // only check that sits outside validateCommon() and can't be skipped either way below.
122
+ const dangerousFileErrors = this.validateDangerousFiles(fieldFiles);
119
123
 
120
124
  switch (this.options.type) {
121
125
  case SolidMediaType.mediaSingle:
122
- return [...errors, ...this.validateMediaSingle(fieldFiles)];
126
+ return [...dangerousFileErrors, ...this.validateMediaSingle(fieldFiles)];
123
127
  case SolidMediaType.mediaMultiple:
124
- return [...errors, ...this.validateMediaMultiple(fieldFiles)];
128
+ return [...dangerousFileErrors, ...this.validateMediaMultiple(fieldFiles)];
125
129
  default:
126
- return errors;
130
+ return dangerousFileErrors;
127
131
  }
128
132
  }
129
133
 
@@ -137,7 +141,18 @@ export class MediaFieldCrudManager implements FieldCrudManager {
137
141
  }));
138
142
  }
139
143
 
140
- private validateMediaSingle(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
144
+ // Every optional, admin-configured, per-field check - no-ops when its setting is unset -
145
+ // shared between mediaSingle and mediaMultiple so neither can drift out of sync with it.
146
+ private validateCommon(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
147
+ return [
148
+ ...this.validateRequired(fieldFiles),
149
+ ...this.validateSize(fieldFiles),
150
+ ...this.validateTypes(fieldFiles),
151
+ ...this.validateAllowedExtensions(fieldFiles),
152
+ ];
153
+ }
154
+
155
+ private validateRequired(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
141
156
  const errors: ValidationError[] = [];
142
157
  if (!this.options.isUpdate && this.options.required && fieldFiles.length === 0) {
143
158
  errors.push({
@@ -145,13 +160,11 @@ export class MediaFieldCrudManager implements FieldCrudManager {
145
160
  error: `${this.options.fieldName} is required`
146
161
  });
147
162
  }
148
- if (fieldFiles.length > 1) {
149
- errors.push({
150
- field: this.options.fieldName,
151
- error: `${this.options.fieldName} must be a single file`
152
- });
153
- }
154
- // validate size
163
+ return errors;
164
+ }
165
+
166
+ private validateSize(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
167
+ const errors: ValidationError[] = [];
155
168
  if (this.options.mediaMaxSizeKb) {
156
169
  for (let i = 0; i < fieldFiles.length; i++) {
157
170
  const fieldFile = fieldFiles[i];
@@ -164,7 +177,11 @@ export class MediaFieldCrudManager implements FieldCrudManager {
164
177
  }
165
178
  }
166
179
  }
167
- // validate type
180
+ return errors;
181
+ }
182
+
183
+ private validateTypes(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
184
+ const errors: ValidationError[] = [];
168
185
  if (this.options.mediaTypes && this.options.mediaTypes.length > 0) {
169
186
  const allowedFileTypes = this.options.mediaTypes as MediaType[];
170
187
 
@@ -182,21 +199,43 @@ export class MediaFieldCrudManager implements FieldCrudManager {
182
199
  }
183
200
  }
184
201
  }
185
-
186
202
  return errors;
187
203
  }
188
204
 
189
- private validateMediaMultiple(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
190
- const errors: ValidationError[] = [];
191
- if (!this.options.isUpdate && this.options.required && fieldFiles.length === 0) {
205
+ private validateAllowedExtensions(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
206
+ const allowed = this.options.mediaAllowedExtensions;
207
+ if (!allowed || allowed.length === 0) {
208
+ return [];
209
+ }
210
+
211
+ const normalizedAllowed = new Set(allowed.map(ext => ext.toLowerCase().replace(/^\./, '')));
212
+ return fieldFiles
213
+ .filter(fieldFile => {
214
+ const ext = getLowercaseFileExtension(getUploadedFileName(fieldFile));
215
+ return !ext || !normalizedAllowed.has(ext);
216
+ })
217
+ .map(fieldFile => ({
218
+ field: this.options.fieldName,
219
+ error: `${this.options.fieldName} file type not allowed. ` +
220
+ `Allowed extensions: ${[...normalizedAllowed].join(', ')}.`
221
+ }));
222
+ }
223
+
224
+ private validateMediaSingle(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
225
+ const errors = this.validateCommon(fieldFiles);
226
+ if (fieldFiles.length > 1) {
192
227
  errors.push({
193
228
  field: this.options.fieldName,
194
- error: `${this.options.fieldName} is required`
229
+ error: `${this.options.fieldName} must be a single file`
195
230
  });
196
231
  }
197
232
  return errors;
198
233
  }
199
234
 
235
+ private validateMediaMultiple(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
236
+ return this.validateCommon(fieldFiles);
237
+ }
238
+
200
239
  transformForCreate(dto: any): any {
201
240
  return dto;
202
241
  }
@@ -8,6 +8,7 @@ import { classify } from '@angular-devkit/core/src/utils/strings';
8
8
  import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE, CHATTER_MESSAGE_USER_FIELDS } from 'src/constants/chatter-message.constants';
9
9
  import { isDangerousMediaFile } from 'src/constants/media-file-types';
10
10
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
11
+ import { MediaFieldCrudManager, SolidMediaType } from 'src/helpers/field-crud-managers/MediaFieldCrudManager';
11
12
  import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto';
12
13
  import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto';
13
14
  import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
@@ -196,6 +197,34 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
196
197
  }
197
198
  }
198
199
 
200
+ /**
201
+ * Validates newly-uploaded attachments against each declared media field's real config
202
+ * (mediaTypes/mediaMaxSizeKb/mediaAllowedExtensions), grouped by fieldname - the same rules
203
+ * MediaFieldCrudManager already enforces everywhere else. Shared between postMessage and
204
+ * updateCustomNoteMessage so the two entry points can't drift apart; isUpdate is the only
205
+ * thing that differs between them.
206
+ */
207
+ private validateChatterAttachmentsForFields(files: Express.Multer.File[], mediaFields: any[], isUpdate: boolean): void {
208
+ for (const mediaField of mediaFields) {
209
+ const media = files.filter(multerFile => multerFile.fieldname === mediaField.name);
210
+ if (media.length === 0) continue;
211
+
212
+ const validator = new MediaFieldCrudManager({
213
+ type: mediaField.type as SolidMediaType,
214
+ required: mediaField.required,
215
+ fieldName: mediaField.name,
216
+ mediaMaxSizeKb: mediaField.mediaMaxSizeKb,
217
+ mediaTypes: mediaField.mediaTypes,
218
+ mediaAllowedExtensions: mediaField.mediaAllowedExtensions,
219
+ isUpdate,
220
+ });
221
+ const validationErrors = validator.validate({}, media);
222
+ if (validationErrors.length > 0) {
223
+ throw new BadRequestException(validationErrors.map(error => error.error).join(', '));
224
+ }
225
+ }
226
+ }
227
+
199
228
  private async publishChatterMentionNotifications(message: ChatterMessage, model: any) {
200
229
  if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM || message.messageSubType !== CHATTER_MESSAGE_SUBTYPE.NOTE) {
201
230
  return;
@@ -329,6 +358,24 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
329
358
 
330
359
  this.validateChatterMediaFiles(files);
331
360
 
361
+ let chatterModel: any = null;
362
+ let mediaFields: any[] = [];
363
+ if (removeAttachmentIds.length > 0 || hasNewFiles) {
364
+ chatterModel = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
365
+ fields: {
366
+ model: true,
367
+ mediaStorageProvider: true,
368
+ },
369
+ module: true,
370
+ });
371
+
372
+ mediaFields = chatterModel.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
373
+
374
+ if (hasNewFiles) {
375
+ this.validateChatterAttachmentsForFields(files, mediaFields, true);
376
+ }
377
+ }
378
+
332
379
  if (hasMessageBody) {
333
380
  message.messageBody = trimmedMessageBody;
334
381
  }
@@ -345,15 +392,6 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
345
392
  const savedMessage = await this.repo.save(message);
346
393
 
347
394
  if (removeAttachmentIds.length > 0 || hasNewFiles) {
348
- const model = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
349
- fields: {
350
- model: true,
351
- mediaStorageProvider: true,
352
- },
353
- module: true,
354
- });
355
-
356
- const mediaFields = model.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
357
395
  const attachmentFieldIds = mediaFields.map(field => field.id);
358
396
 
359
397
  if (removeAttachmentIds.length > 0 && attachmentFieldIds.length > 0) {
@@ -361,7 +399,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
361
399
  where: {
362
400
  id: In(removeAttachmentIds),
363
401
  entityId: savedMessage.id,
364
- modelMetadata: { id: model.id },
402
+ modelMetadata: { id: chatterModel.id },
365
403
  fieldMetadata: { id: In(attachmentFieldIds) },
366
404
  },
367
405
  relations: {
@@ -434,10 +472,9 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
434
472
 
435
473
  this.stampMessageAuditFields(chatterMessage);
436
474
 
437
- const savedMessage = await this.repo.save(chatterMessage);
438
-
475
+ let mediaFields: any[] = [];
439
476
  if (files && files.length > 0) {
440
- const model = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
477
+ const chatterModel = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
441
478
  fields: {
442
479
  model: true,
443
480
  mediaStorageProvider: true,
@@ -445,8 +482,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
445
482
  module: true,
446
483
  });
447
484
 
448
- const mediaFields = model.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
485
+ mediaFields = chatterModel.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
486
+ this.validateChatterAttachmentsForFields(files, mediaFields, false);
487
+ }
488
+
489
+ const savedMessage = await this.repo.save(chatterMessage);
449
490
 
491
+ if (files && files.length > 0) {
450
492
  for (const mediaField of mediaFields) {
451
493
  const media = files.filter(multerFile => multerFile.fieldname === mediaField.name);
452
494
  if (media.length > 0) {
@@ -446,6 +446,7 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
446
446
  ...commonOptions,
447
447
  mediaMaxSizeKb: fieldMetadata.mediaMaxSizeKb,
448
448
  mediaTypes: fieldMetadata.mediaTypes,
449
+ mediaAllowedExtensions: fieldMetadata.mediaAllowedExtensions,
449
450
  type: fieldMetadata.type as unknown as SolidMediaType
450
451
  };
451
452
  return new MediaFieldCrudManager(options);
@@ -945,6 +945,7 @@ export class FieldMetadataService implements OnApplicationBootstrap {
945
945
  "isSystem",
946
946
  "mediaTypes",
947
947
  "mediaMaxSizeKb",
948
+ "mediaAllowedExtensions",
948
949
  "mediaStorageProviderId",
949
950
  "required",
950
951
  "unique",
@@ -966,6 +967,7 @@ export class FieldMetadataService implements OnApplicationBootstrap {
966
967
  "isSystem",
967
968
  "mediaTypes",
968
969
  "mediaMaxSizeKb",
970
+ "mediaAllowedExtensions",
969
971
  "mediaStorageProviderId",
970
972
  "required",
971
973
  "unique",
@@ -113,6 +113,7 @@ export class MediaService extends CRUDService<Media> {
113
113
  fieldName: 'files',
114
114
  mediaMaxSizeKb: createDto['fieldMetadata']?.mediaMaxSizeKb,
115
115
  mediaTypes: createDto['fieldMetadata']?.mediaTypes || [],
116
+ mediaAllowedExtensions: createDto['fieldMetadata']?.mediaAllowedExtensions || [],
116
117
  isUpdate: false,
117
118
  });
118
119
  const validationErrors = validator.validate(createDto, files);
@@ -13,6 +13,7 @@ import { ConfigService } from '@nestjs/config';
13
13
  import * as fs from 'fs/promises'; // Use the Promise-based version of fs for async/await
14
14
  import * as path from 'path'; // To handle file paths
15
15
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
16
+ import { isDangerousMediaFile } from 'src/constants/media-file-types';
16
17
  import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator';
17
18
  import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service';
18
19
  import { ModuleMetadataRepository } from 'src/repository/module-metadata.repository';
@@ -137,6 +138,9 @@ export class ModuleMetadataService {
137
138
 
138
139
  async createInDB(manager: EntityManager, createDto: CreateModuleMetadataDto, files: Express.Multer.File[] = []) {
139
140
  if (files.length > 0) {
141
+ if (isDangerousMediaFile(files[0])) {
142
+ throw new BadRequestException('Dangerous file types are not allowed for the menu icon.');
143
+ }
140
144
  const fileStoragePath = this.getFullFilePathForDisk(this.getFileName(files[0]));
141
145
  this.fileService.copy(files[0].path, fileStoragePath);
142
146
  this.fileService.delete(files[0].path);
@@ -241,6 +245,9 @@ export class ModuleMetadataService {
241
245
  throw new NotFoundException(ERROR_MESSAGES.MODULE_ID_NOT_FOUND(id));
242
246
  }
243
247
  if (files.length > 0) {
248
+ if (isDangerousMediaFile(files[0])) {
249
+ throw new BadRequestException('Dangerous file types are not allowed for the menu icon.');
250
+ }
244
251
 
245
252
  const fileStoragePath = this.getFullFilePathForDisk(this.getFileName(files[0]));
246
253
  this.fileService.copy(files[0].path, fileStoragePath);
@@ -17,6 +17,7 @@ import type { SolidCoreSetting } from './settings/default-settings-provider.serv
17
17
  import { Logger } from '@nestjs/common';
18
18
  import { EncryptionService } from './encryption.service';
19
19
  import { sanitizeStoredMediaFileName } from './media-storage.utils';
20
+ import { isDangerousMediaFile } from 'src/constants/media-file-types';
20
21
 
21
22
 
22
23
  @Injectable()
@@ -358,6 +359,10 @@ export class SettingService {
358
359
  // Handle uploaded files
359
360
  if (uploadedFiles?.length) {
360
361
  for (const file of uploadedFiles) {
362
+ if (isDangerousMediaFile(file)) {
363
+ throw new BadRequestException('Dangerous file types are not allowed for settings.');
364
+ }
365
+
361
366
  const settingKey = file.fieldname;
362
367
  // originalname is attacker-controlled and feeds straight into a storage path.
363
368
  const relativeFileName = `${file.filename}-${sanitizeStoredMediaFileName(file.originalname)}`;
@@ -532,6 +532,20 @@ const getSolidCoreSettings = (isProd: boolean) =>
532
532
  ],
533
533
  helpText: "Default storage implementation used for file handling, such as local disk or S3.",
534
534
  },
535
+ {
536
+ moduleName: "solid-core",
537
+ key: "maxMediaFileSizeMb",
538
+ value: (() => {
539
+ const parsedMb = Number(process.env.AB_MEDIA_MAX_FILE_SIZE_MB);
540
+ return Number.isFinite(parsedMb) && parsedMb > 0 ? parsedMb : 100;
541
+ })(),
542
+ level: SettingLevel.SystemAdminReadonly,
543
+ label: "Max Media File Size (MB)",
544
+ group: "storage-settings",
545
+ sortOrder: 45,
546
+ controlType: "numeric",
547
+ helpText: "Global upload size limit enforced by Multer under every per-field mediaMaxSizeKb restriction. Read-only here: it's applied once at process start, so changing AB_MEDIA_MAX_FILE_SIZE_MB requires an app restart to take effect - editing this in the admin UI would not do that.",
548
+ },
535
549
 
536
550
  // aws-s3-settings-provider.service.ts
537
551
  {
@@ -557,6 +557,19 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
557
557
  imports: [ConfigModule],
558
558
  useFactory: async (configService: ConfigService) => ({
559
559
  dest: process.env.AB_MEDIA_UPLOAD_DIR ?? "media-uploads",
560
+ // A global floor under whatever per-field mediaMaxSizeKb restrictions apply, not a
561
+ // replacement for them - some upload paths (e.g. chatter attachments) had no size cap
562
+ // enforced anywhere before this. Must stay >= the largest legitimate per-field limit
563
+ // already configured (chatter's messageAttachments is 102400 KB / 100MB), since Multer
564
+ // enforces this before any of our own field-level checks ever run. Mirrored (read-only,
565
+ // for visibility only - see comment there for why) as the "maxMediaFileSizeMb" setting
566
+ // in default-settings-provider.service.ts; keep both parsing expressions in sync.
567
+ limits: {
568
+ fileSize: (() => {
569
+ const parsedMb = Number(process.env.AB_MEDIA_MAX_FILE_SIZE_MB);
570
+ return (Number.isFinite(parsedMb) && parsedMb > 0 ? parsedMb : 100) * 1024 * 1024;
571
+ })(),
572
+ },
560
573
  }),
561
574
  inject: [ConfigService],
562
575
  }),