@solidxai/core 0.1.16 → 0.1.17-beta.1
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/CHANGELOG.md +1986 -1
- package/CLAUDE.md +26 -0
- package/dist/constants/media-file-types.d.ts +2 -2
- package/dist/constants/media-file-types.d.ts.map +1 -1
- package/dist/constants/media-file-types.js +24 -10
- package/dist/constants/media-file-types.js.map +1 -1
- package/dist/controllers/chatter-message-details.controller.d.ts.map +1 -1
- package/dist/controllers/chatter-message-details.controller.js +2 -6
- package/dist/controllers/chatter-message-details.controller.js.map +1 -1
- package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
- package/dist/controllers/chatter-message.controller.js +3 -7
- package/dist/controllers/chatter-message.controller.js.map +1 -1
- package/dist/controllers/dashboard-user-layout.controller.d.ts.map +1 -1
- package/dist/controllers/dashboard-user-layout.controller.js +2 -6
- package/dist/controllers/dashboard-user-layout.controller.js.map +1 -1
- package/dist/controllers/import-transaction-error-log.controller.d.ts.map +1 -1
- package/dist/controllers/import-transaction-error-log.controller.js +2 -6
- package/dist/controllers/import-transaction-error-log.controller.js.map +1 -1
- package/dist/controllers/import-transaction.controller.d.ts.map +1 -1
- package/dist/controllers/import-transaction.controller.js +2 -6
- package/dist/controllers/import-transaction.controller.js.map +1 -1
- package/dist/controllers/list-of-values.controller.d.ts.map +1 -1
- package/dist/controllers/list-of-values.controller.js +2 -6
- package/dist/controllers/list-of-values.controller.js.map +1 -1
- package/dist/controllers/locale.controller.d.ts.map +1 -1
- package/dist/controllers/locale.controller.js +2 -6
- package/dist/controllers/locale.controller.js.map +1 -1
- package/dist/controllers/media.controller.d.ts.map +1 -1
- package/dist/controllers/media.controller.js +9 -8
- package/dist/controllers/media.controller.js.map +1 -1
- package/dist/controllers/model-sequence.controller.d.ts.map +1 -1
- package/dist/controllers/model-sequence.controller.js +2 -6
- package/dist/controllers/model-sequence.controller.js.map +1 -1
- package/dist/controllers/saved-filters.controller.d.ts.map +1 -1
- package/dist/controllers/saved-filters.controller.js +2 -6
- package/dist/controllers/saved-filters.controller.js.map +1 -1
- package/dist/controllers/scheduled-job.controller.d.ts.map +1 -1
- package/dist/controllers/scheduled-job.controller.js +2 -6
- package/dist/controllers/scheduled-job.controller.js.map +1 -1
- package/dist/controllers/security-rule.controller.d.ts.map +1 -1
- package/dist/controllers/security-rule.controller.js +2 -6
- package/dist/controllers/security-rule.controller.js.map +1 -1
- package/dist/controllers/user-activity-history.controller.d.ts.map +1 -1
- package/dist/controllers/user-activity-history.controller.js +2 -6
- package/dist/controllers/user-activity-history.controller.js.map +1 -1
- package/dist/controllers/user-view-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/user-view-metadata.controller.js +2 -6
- package/dist/controllers/user-view-metadata.controller.js.map +1 -1
- package/dist/controllers/workflow-definition.controller.d.ts.map +1 -1
- package/dist/controllers/workflow-definition.controller.js +2 -6
- package/dist/controllers/workflow-definition.controller.js.map +1 -1
- package/dist/controllers/workflow-execution-artifact.controller.d.ts.map +1 -1
- package/dist/controllers/workflow-execution-artifact.controller.js +2 -6
- package/dist/controllers/workflow-execution-artifact.controller.js.map +1 -1
- package/dist/controllers/workflow-execution-log.controller.d.ts.map +1 -1
- package/dist/controllers/workflow-execution-log.controller.js +2 -6
- package/dist/controllers/workflow-execution-log.controller.js.map +1 -1
- package/dist/controllers/workflow-execution.controller.d.ts.map +1 -1
- package/dist/controllers/workflow-execution.controller.js +2 -6
- package/dist/controllers/workflow-execution.controller.js.map +1 -1
- package/dist/controllers/workflow-secret.controller.d.ts.map +1 -1
- package/dist/controllers/workflow-secret.controller.js +2 -6
- package/dist/controllers/workflow-secret.controller.js.map +1 -1
- package/dist/controllers/workflow-step-execution.controller.d.ts.map +1 -1
- package/dist/controllers/workflow-step-execution.controller.js +2 -6
- package/dist/controllers/workflow-step-execution.controller.js.map +1 -1
- package/dist/controllers/workflow-trigger-execution.controller.d.ts.map +1 -1
- package/dist/controllers/workflow-trigger-execution.controller.js +2 -6
- package/dist/controllers/workflow-trigger-execution.controller.js.map +1 -1
- package/dist/dtos/basic-filters.dto.d.ts +3 -5
- package/dist/dtos/basic-filters.dto.d.ts.map +1 -1
- package/dist/dtos/basic-filters.dto.js +6 -8
- package/dist/dtos/basic-filters.dto.js.map +1 -1
- package/dist/dtos/otp-sign-up.dto.d.ts +1 -0
- package/dist/dtos/otp-sign-up.dto.d.ts.map +1 -1
- package/dist/dtos/otp-sign-up.dto.js +12 -1
- package/dist/dtos/otp-sign-up.dto.js.map +1 -1
- package/dist/enums/show-soft-deleted.enum.d.ts +5 -0
- package/dist/enums/show-soft-deleted.enum.d.ts.map +1 -0
- package/dist/enums/show-soft-deleted.enum.js +9 -0
- package/dist/enums/show-soft-deleted.enum.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +15 -6
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/chatter-message.service.d.ts +2 -0
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +48 -21
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +20 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +12 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +5 -1
- package/dist/solid-core.module.js.map +1 -1
- package/package.json +1 -1
- package/postman/signup-intent-verification.postman_collection.json +573 -0
- package/src/constants/media-file-types.ts +76 -24
- package/src/controllers/chatter-message-details.controller.ts +1 -4
- package/src/controllers/chatter-message.controller.ts +1 -4
- package/src/controllers/dashboard-user-layout.controller.ts +1 -4
- package/src/controllers/import-transaction-error-log.controller.ts +1 -4
- package/src/controllers/import-transaction.controller.ts +1 -4
- package/src/controllers/list-of-values.controller.ts +1 -4
- package/src/controllers/locale.controller.ts +1 -4
- package/src/controllers/media.controller.ts +19 -7
- package/src/controllers/model-sequence.controller.ts +1 -4
- package/src/controllers/saved-filters.controller.ts +1 -4
- package/src/controllers/scheduled-job.controller.ts +1 -4
- package/src/controllers/security-rule.controller.ts +1 -4
- package/src/controllers/user-activity-history.controller.ts +1 -4
- package/src/controllers/user-view-metadata.controller.ts +1 -4
- package/src/controllers/workflow-definition.controller.ts +1 -4
- package/src/controllers/workflow-execution-artifact.controller.ts +1 -4
- package/src/controllers/workflow-execution-log.controller.ts +1 -4
- package/src/controllers/workflow-execution.controller.ts +1 -4
- package/src/controllers/workflow-secret.controller.ts +1 -4
- package/src/controllers/workflow-step-execution.controller.ts +1 -4
- package/src/controllers/workflow-trigger-execution.controller.ts +1 -4
- package/src/dtos/basic-filters.dto.ts +6 -7
- package/src/dtos/otp-sign-up.dto.ts +11 -1
- package/src/enums/show-soft-deleted.enum.ts +4 -0
- package/src/index.ts +1 -0
- package/src/services/authentication.service.ts +61 -26
- package/src/services/chatter-message.service.ts +54 -23
- package/src/services/settings/default-settings-provider.service.ts +12 -0
- package/src/solid-core.module.ts +12 -2
|
@@ -70,9 +70,18 @@ interface otp {
|
|
|
70
70
|
|
|
71
71
|
/** Where a signup's role names come from. */
|
|
72
72
|
enum RolesSource {
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Ask nobody; returning [] lets performSignUp apply the configured `defaultRole`.
|
|
75
|
+
*
|
|
76
|
+
* No intent maps here today - it is kept for paths that deliberately bypass the
|
|
77
|
+
* provider. OAuth is the live example: its DTO can never carry a discriminator, so
|
|
78
|
+
* it takes `defaultRole` outright (currently inline in UserService).
|
|
79
|
+
*/
|
|
74
80
|
Default,
|
|
75
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* `provider.roles(dto)` - and nothing else. Yields [] when no provider is registered
|
|
83
|
+
* or when the provider declines to name any, which falls through to `defaultRole`.
|
|
84
|
+
*/
|
|
76
85
|
Provider,
|
|
77
86
|
/** The caller's own `dto.roles`. */
|
|
78
87
|
Caller,
|
|
@@ -82,10 +91,18 @@ enum RolesSource {
|
|
|
82
91
|
* The single place that decides, per intent, which entity a signup builds and where
|
|
83
92
|
* its roles come from.
|
|
84
93
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
94
|
+
* Where an extension user provider is registered it is the authority on that app's user
|
|
95
|
+
* roles, so every provider-backed intent asks it. `defaultRole` is the *fallback* for
|
|
96
|
+
* when it names none - or when there is no provider at all - not the rule.
|
|
97
|
+
*
|
|
98
|
+
* SelfRegistration and ExtensionModel resolve identically today. They stay separate
|
|
99
|
+
* intents because only SelfRegistration is gated on `allowPublicRegistration`, and
|
|
100
|
+
* because "an anonymous visitor signing themselves up" and "an admin creating a user
|
|
101
|
+
* from the model's own form" are different things that may yet need to diverge.
|
|
102
|
+
*
|
|
103
|
+
* `useProvider` is computable from `rolesSource` today (`Caller` implies false), but the
|
|
104
|
+
* two answer different questions - "which entity?" and "which roles?" - that merely
|
|
105
|
+
* coincide across these intents. Keep both, and change behaviour here, not at a call site.
|
|
89
106
|
*/
|
|
90
107
|
const SIGNUP_POLICY: Record<
|
|
91
108
|
SignupIntent,
|
|
@@ -93,7 +110,7 @@ const SIGNUP_POLICY: Record<
|
|
|
93
110
|
> = {
|
|
94
111
|
[SignupIntent.SelfRegistration]: {
|
|
95
112
|
useProvider: true,
|
|
96
|
-
rolesSource: RolesSource.
|
|
113
|
+
rolesSource: RolesSource.Provider,
|
|
97
114
|
},
|
|
98
115
|
[SignupIntent.ExtensionModel]: {
|
|
99
116
|
useProvider: true,
|
|
@@ -246,12 +263,24 @@ export class AuthenticationService {
|
|
|
246
263
|
dto: Record<string, any>,
|
|
247
264
|
source: RolesSource,
|
|
248
265
|
): string[] {
|
|
266
|
+
// Only `Caller` reads dto.roles. Anywhere else, roles in the body are ignored -
|
|
267
|
+
// which is what keeps an anonymous caller from naming their own on a @Public()
|
|
268
|
+
// endpoint - so say so rather than dropping them silently.
|
|
269
|
+
if (source !== RolesSource.Caller && dto.roles?.length) {
|
|
270
|
+
this.logger.warn(
|
|
271
|
+
`Ignoring caller-supplied roles for "${dto.username}": roles on this path come from ` +
|
|
272
|
+
(source === RolesSource.Provider
|
|
273
|
+
? "the extension user provider"
|
|
274
|
+
: "the configured defaultRole"),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
249
278
|
switch (source) {
|
|
250
279
|
case RolesSource.Provider:
|
|
251
|
-
// Sole authority.
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
280
|
+
// Sole authority. Reading dto.roles as a preference or fallback would skip
|
|
281
|
+
// roles(), which is where a provider validates its discriminator, and
|
|
282
|
+
// CreateUserDto types roles as UpdateRoleMetadataDto[] where performSignUp
|
|
283
|
+
// expects role-name strings. [] here falls through to `defaultRole`.
|
|
255
284
|
return (
|
|
256
285
|
this.solidRegistry
|
|
257
286
|
.getExtensionUserCreationProvider()
|
|
@@ -262,14 +291,6 @@ export class AuthenticationService {
|
|
|
262
291
|
return dto.roles ?? [];
|
|
263
292
|
|
|
264
293
|
case RolesSource.Default:
|
|
265
|
-
// Public signup: the form has no business naming roles. Returning empty lets
|
|
266
|
-
// performSignUp apply the configured `defaultRole`, rather than adding a
|
|
267
|
-
// second mechanism for the same thing.
|
|
268
|
-
if (dto.roles?.length) {
|
|
269
|
-
this.logger.warn(
|
|
270
|
-
`Ignoring caller-supplied roles on public registration for "${dto.username}"`,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
294
|
return [];
|
|
274
295
|
}
|
|
275
296
|
}
|
|
@@ -772,16 +793,31 @@ export class AuthenticationService {
|
|
|
772
793
|
validationSource: string,
|
|
773
794
|
): Promise<User> {
|
|
774
795
|
if (isEmpty(existingUser)) {
|
|
796
|
+
// Resolved before anything is written: where a provider requires its discriminator
|
|
797
|
+
// roles() throws, and that should surface as a rejected request rather than as a
|
|
798
|
+
// half-registered user with no roles.
|
|
799
|
+
const roles = this.resolveSignupRoles(
|
|
800
|
+
signUpDto,
|
|
801
|
+
SIGNUP_POLICY[SignupIntent.SelfRegistration].rolesSource,
|
|
802
|
+
);
|
|
803
|
+
|
|
775
804
|
// A new registration is saved through whichever repository createUser resolved,
|
|
776
805
|
// which is the provider's when the app registers one.
|
|
777
806
|
const { entity: user, repo } = await this.createUser(signUpDto);
|
|
778
807
|
user.active = false; // User will be activated only after OTP verification, hence setting active to false for new user.
|
|
779
808
|
await this.assignRegistrationOtp(validationSource, user);
|
|
780
809
|
await repo.save(user);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
this.
|
|
784
|
-
|
|
810
|
+
|
|
811
|
+
if (roles.length) {
|
|
812
|
+
await this.userService.addRolesToUser(user.username, roles);
|
|
813
|
+
} else {
|
|
814
|
+
// The provider named none, or there is no provider: fall back to the configured
|
|
815
|
+
// default, matching what performSignUp does on the password paths.
|
|
816
|
+
await this.userService.addRoleToUser(
|
|
817
|
+
user.username,
|
|
818
|
+
this.settingService.getConfigValue<SolidCoreSetting>("defaultRole"),
|
|
819
|
+
);
|
|
820
|
+
}
|
|
785
821
|
return user;
|
|
786
822
|
}
|
|
787
823
|
|
|
@@ -812,9 +848,8 @@ export class AuthenticationService {
|
|
|
812
848
|
entity.username = signUpDto.username;
|
|
813
849
|
entity.email = signUpDto.email;
|
|
814
850
|
entity.mobile = signUpDto.mobile;
|
|
815
|
-
// OTPSignUpDto declares no `fullName`, so username is the only source here. Mirrors
|
|
816
|
-
|
|
817
|
-
entity.fullName = signUpDto.username;
|
|
851
|
+
// OTPSignUpDto declares no `fullName`, so username is the only source here. Mirrors the fallback in populateForSignup, which this path does not go through.
|
|
852
|
+
entity.fullName = signUpDto.fullName?.trim() || signUpDto.username;
|
|
818
853
|
entity.customPayload = signUpDto.customPayload;
|
|
819
854
|
entity.lastLoginProvider = LoginProvider.OTP;
|
|
820
855
|
|
|
@@ -771,38 +771,69 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
771
771
|
|
|
772
772
|
if (field.type === 'relation') {
|
|
773
773
|
if (field.relationType === "many-to-one") {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
try {
|
|
779
|
-
const relatedModel = await this.modelMetadataRepo.findOne({
|
|
780
|
-
where: { singularName: field.relationCoModelSingularName || field.relation },
|
|
781
|
-
relations: { userKeyField: true }
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
if (relatedModel && relatedModel.userKeyField) {
|
|
785
|
-
const userKeyFieldName = relatedModel.userKeyField.name;
|
|
786
|
-
return value[userKeyFieldName] ? value[userKeyFieldName].toString() : '';
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
if (value.id) {
|
|
790
|
-
return value.id.toString();
|
|
791
|
-
}
|
|
792
|
-
} catch (error: any) {
|
|
793
|
-
console.error('Error fetching related model metadata:', error);
|
|
794
|
-
return value.id ? value.id.toString() : '';
|
|
795
|
-
}
|
|
774
|
+
const relatedModel = await this.loadRelatedModelForDisplay(field);
|
|
775
|
+
return this.resolveRelationDisplayValue(value, relatedModel?.userKeyField?.name);
|
|
796
776
|
}
|
|
797
777
|
|
|
798
778
|
if (field.relationType === 'many-to-many') {
|
|
799
|
-
|
|
779
|
+
if (!Array.isArray(value)) {
|
|
780
|
+
const relatedModel = await this.loadRelatedModelForDisplay(field);
|
|
781
|
+
return this.resolveRelationDisplayValue(value, relatedModel?.userKeyField?.name);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const relatedModel = await this.loadRelatedModelForDisplay(field);
|
|
785
|
+
const userKeyFieldName = relatedModel?.userKeyField?.name;
|
|
786
|
+
return value
|
|
787
|
+
.map(item => this.resolveRelationDisplayValue(item, userKeyFieldName))
|
|
788
|
+
.filter(Boolean)
|
|
789
|
+
.join(', ');
|
|
800
790
|
}
|
|
801
791
|
}
|
|
802
792
|
|
|
803
793
|
return value.toString();
|
|
804
794
|
}
|
|
805
795
|
|
|
796
|
+
private async loadRelatedModelForDisplay(field: any): Promise<any | null> {
|
|
797
|
+
try {
|
|
798
|
+
return await this.modelMetadataRepo.findOne({
|
|
799
|
+
where: { singularName: field.relationCoModelSingularName || field.relation },
|
|
800
|
+
relations: { userKeyField: true }
|
|
801
|
+
});
|
|
802
|
+
} catch (error: any) {
|
|
803
|
+
this._logger.warn(`Unable to load related model metadata for audit display: ${error?.message ?? error}`);
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private resolveRelationDisplayValue(value: any, userKeyFieldName?: string): string {
|
|
809
|
+
if (value === null || value === undefined) {
|
|
810
|
+
return '';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (typeof value !== 'object') {
|
|
814
|
+
return String(value);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const candidateKeys = [
|
|
818
|
+
userKeyFieldName,
|
|
819
|
+
'fullName',
|
|
820
|
+
'displayName',
|
|
821
|
+
'username',
|
|
822
|
+
'name',
|
|
823
|
+
'title',
|
|
824
|
+
'id',
|
|
825
|
+
].filter(Boolean) as string[];
|
|
826
|
+
|
|
827
|
+
for (const key of candidateKeys) {
|
|
828
|
+
const candidate = value[key];
|
|
829
|
+
if (candidate !== null && candidate !== undefined && String(candidate).trim() !== '') {
|
|
830
|
+
return String(candidate);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return '';
|
|
835
|
+
}
|
|
836
|
+
|
|
806
837
|
private hasValueChanged(newValue: any, oldValue: any): boolean {
|
|
807
838
|
if (newValue === oldValue) {
|
|
808
839
|
return false;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SettingLevel,
|
|
8
8
|
} from "src/interfaces";
|
|
9
9
|
import { getDefaultThemeKey, getThemesByMode } from "src/theme/theme-registry";
|
|
10
|
+
import { DANGEROUS_EXTENSIONS } from "src/constants/media-file-types";
|
|
10
11
|
|
|
11
12
|
export const DEFAULT_MEDIA_UPLOAD_DIR = "media-uploads";
|
|
12
13
|
export const DEFAULT_MEDIA_FILE_STORAGE_DIR = "media-files-storage";
|
|
@@ -546,6 +547,17 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
546
547
|
controlType: "numeric",
|
|
547
548
|
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
549
|
},
|
|
550
|
+
{
|
|
551
|
+
moduleName: "solid-core",
|
|
552
|
+
key: "dangerousExtensions",
|
|
553
|
+
value: [...DANGEROUS_EXTENSIONS].join(','),
|
|
554
|
+
level: SettingLevel.SystemAdminReadonly,
|
|
555
|
+
label: "Blocked Upload Extensions",
|
|
556
|
+
group: "storage-settings",
|
|
557
|
+
sortOrder: 46,
|
|
558
|
+
controlType: "shortText",
|
|
559
|
+
helpText: "File extensions rejected on every upload path, regardless of field configuration. Read-only here: it's applied once at process start. To change it, copy this value into AB_DANGEROUS_EXTENSIONS, edit it, and restart - note that variable REPLACES this list rather than adding to it, so any extension you leave out becomes an accepted upload type.",
|
|
560
|
+
},
|
|
549
561
|
|
|
550
562
|
// aws-s3-settings-provider.service.ts
|
|
551
563
|
{
|
package/src/solid-core.module.ts
CHANGED
|
@@ -541,17 +541,27 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
|
|
|
541
541
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
542
542
|
|
|
543
543
|
// Only a small allowlist of media types is ever safe to render inline. Everything
|
|
544
|
-
// else (including
|
|
544
|
+
// else (including html and any other uploaded file) is forced to download
|
|
545
545
|
// rather than be displayed/executed by the browser, regardless of what mimetype or
|
|
546
546
|
// extension it was uploaded with.
|
|
547
547
|
// basename first: getLowercaseFileExtension takes a file name, not a path, so a
|
|
548
548
|
// directory containing a dot must not be mistaken for the extension. An
|
|
549
549
|
// extensionless file yields undefined, which is not inline-safe - so it correctly
|
|
550
550
|
// falls through to the forced-download branch.
|
|
551
|
-
|
|
551
|
+
const ext = getLowercaseFileExtension(basename(path)) ?? '';
|
|
552
|
+
if (!INLINE_SAFE_EXTENSIONS.has(ext)) {
|
|
552
553
|
res.setHeader("Content-Type", "application/octet-stream");
|
|
553
554
|
res.setHeader("Content-Disposition", "attachment");
|
|
554
555
|
}
|
|
556
|
+
|
|
557
|
+
// svg is only ever inline-safe (see INLINE_SAFE_EXTENSIONS) once it can also render
|
|
558
|
+
// as an active document (<script>, event handlers). <img>/CSS never execute it, but
|
|
559
|
+
// a direct navigation to this URL would - so pin it to a locked-down document: no
|
|
560
|
+
// script, no network, and `sandbox` gives it an opaque origin so even a parser bypass
|
|
561
|
+
// can't reach app cookies or storage.
|
|
562
|
+
if (ext === 'svg') {
|
|
563
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox");
|
|
564
|
+
}
|
|
555
565
|
},
|
|
556
566
|
},
|
|
557
567
|
}),
|