@solidxai/core 0.1.13-beta.9 → 0.1.13

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 (255) hide show
  1. package/CHANGELOG.md +7465 -0
  2. package/README.md +19 -0
  3. package/dist/commands/run-tests.command.d.ts +2 -0
  4. package/dist/commands/run-tests.command.d.ts.map +1 -1
  5. package/dist/commands/run-tests.command.js +20 -3
  6. package/dist/commands/run-tests.command.js.map +1 -1
  7. package/dist/config/cache.options.d.ts.map +1 -1
  8. package/dist/config/cache.options.js +2 -2
  9. package/dist/config/cache.options.js.map +1 -1
  10. package/dist/constants/error-messages.d.ts +7 -0
  11. package/dist/constants/error-messages.d.ts.map +1 -1
  12. package/dist/constants/error-messages.js +7 -0
  13. package/dist/constants/error-messages.js.map +1 -1
  14. package/dist/constants/media-file-types.d.ts +3 -0
  15. package/dist/constants/media-file-types.d.ts.map +1 -1
  16. package/dist/constants/media-file-types.js +47 -8
  17. package/dist/constants/media-file-types.js.map +1 -1
  18. package/dist/constants/success-messages.d.ts +3 -0
  19. package/dist/constants/success-messages.d.ts.map +1 -1
  20. package/dist/constants/success-messages.js +3 -0
  21. package/dist/constants/success-messages.js.map +1 -1
  22. package/dist/controllers/authentication.controller.d.ts +1 -1
  23. package/dist/controllers/authentication.controller.d.ts.map +1 -1
  24. package/dist/controllers/authentication.controller.js +4 -3
  25. package/dist/controllers/authentication.controller.js.map +1 -1
  26. package/dist/controllers/field-metadata.controller.d.ts +2 -2
  27. package/dist/controllers/mpin-authentication.controller.d.ts +44 -0
  28. package/dist/controllers/mpin-authentication.controller.d.ts.map +1 -0
  29. package/dist/controllers/mpin-authentication.controller.js +102 -0
  30. package/dist/controllers/mpin-authentication.controller.js.map +1 -0
  31. package/dist/dtos/change-mpin.dto.d.ts +6 -0
  32. package/dist/dtos/change-mpin.dto.d.ts.map +1 -0
  33. package/dist/dtos/change-mpin.dto.js +39 -0
  34. package/dist/dtos/change-mpin.dto.js.map +1 -0
  35. package/dist/dtos/create-field-metadata.dto.d.ts +3 -6
  36. package/dist/dtos/create-field-metadata.dto.d.ts.map +1 -1
  37. package/dist/dtos/create-field-metadata.dto.js +15 -10
  38. package/dist/dtos/create-field-metadata.dto.js.map +1 -1
  39. package/dist/dtos/create-model-sequence.dto.d.ts +1 -1
  40. package/dist/dtos/create-model-sequence.dto.d.ts.map +1 -1
  41. package/dist/dtos/create-model-sequence.dto.js +1 -1
  42. package/dist/dtos/create-model-sequence.dto.js.map +1 -1
  43. package/dist/dtos/mpin-login.dto.d.ts +5 -0
  44. package/dist/dtos/mpin-login.dto.d.ts.map +1 -0
  45. package/dist/dtos/mpin-login.dto.js +33 -0
  46. package/dist/dtos/mpin-login.dto.js.map +1 -0
  47. package/dist/dtos/setup-mpin.dto.d.ts +7 -0
  48. package/dist/dtos/setup-mpin.dto.d.ts.map +1 -0
  49. package/dist/dtos/setup-mpin.dto.js +47 -0
  50. package/dist/dtos/setup-mpin.dto.js.map +1 -0
  51. package/dist/entities/dashboard-user-layout.entity.js +2 -2
  52. package/dist/entities/dashboard-user-layout.entity.js.map +1 -1
  53. package/dist/entities/field-metadata.entity.d.ts +1 -0
  54. package/dist/entities/field-metadata.entity.d.ts.map +1 -1
  55. package/dist/entities/field-metadata.entity.js +5 -1
  56. package/dist/entities/field-metadata.entity.js.map +1 -1
  57. package/dist/entities/user-device-credential.entity.d.ts +18 -0
  58. package/dist/entities/user-device-credential.entity.d.ts.map +1 -0
  59. package/dist/entities/user-device-credential.entity.js +91 -0
  60. package/dist/entities/user-device-credential.entity.js.map +1 -0
  61. package/dist/entities/user.entity.js +2 -2
  62. package/dist/entities/user.entity.js.map +1 -1
  63. package/dist/entities/view-metadata.entity.js.map +1 -1
  64. package/dist/entities/workflow-execution-artifact.entity.d.ts.map +1 -1
  65. package/dist/entities/workflow-execution-artifact.entity.js +4 -3
  66. package/dist/entities/workflow-execution-artifact.entity.js.map +1 -1
  67. package/dist/entities/workflow-execution-log.entity.d.ts.map +1 -1
  68. package/dist/entities/workflow-execution-log.entity.js +4 -3
  69. package/dist/entities/workflow-execution-log.entity.js.map +1 -1
  70. package/dist/entities/workflow-execution.entity.d.ts.map +1 -1
  71. package/dist/entities/workflow-execution.entity.js +3 -2
  72. package/dist/entities/workflow-execution.entity.js.map +1 -1
  73. package/dist/entities/workflow-secret.entity.d.ts.map +1 -1
  74. package/dist/entities/workflow-secret.entity.js +3 -2
  75. package/dist/entities/workflow-secret.entity.js.map +1 -1
  76. package/dist/entities/workflow-step-execution.entity.d.ts.map +1 -1
  77. package/dist/entities/workflow-step-execution.entity.js +3 -2
  78. package/dist/entities/workflow-step-execution.entity.js.map +1 -1
  79. package/dist/entities/workflow-trigger-execution.entity.d.ts.map +1 -1
  80. package/dist/entities/workflow-trigger-execution.entity.js +6 -5
  81. package/dist/entities/workflow-trigger-execution.entity.js.map +1 -1
  82. package/dist/filters/http-exception.filter.d.ts.map +1 -1
  83. package/dist/filters/http-exception.filter.js +5 -1
  84. package/dist/filters/http-exception.filter.js.map +1 -1
  85. package/dist/helpers/bootstrap.helper.d.ts +5 -2
  86. package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
  87. package/dist/helpers/bootstrap.helper.js +7 -3
  88. package/dist/helpers/bootstrap.helper.js.map +1 -1
  89. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts +6 -0
  90. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
  91. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +54 -27
  92. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
  93. package/dist/helpers/file-download.helper.d.ts.map +1 -1
  94. package/dist/helpers/file-download.helper.js +6 -1
  95. package/dist/helpers/file-download.helper.js.map +1 -1
  96. package/dist/helpers/security.helper.d.ts +5 -1
  97. package/dist/helpers/security.helper.d.ts.map +1 -1
  98. package/dist/helpers/security.helper.js +15 -4
  99. package/dist/helpers/security.helper.js.map +1 -1
  100. package/dist/helpers/solid-core-error-codes-provider.service.d.ts.map +1 -1
  101. package/dist/helpers/solid-core-error-codes-provider.service.js +45 -0
  102. package/dist/helpers/solid-core-error-codes-provider.service.js.map +1 -1
  103. package/dist/helpers/solid-microservice-adapter.service.d.ts.map +1 -1
  104. package/dist/helpers/solid-microservice-adapter.service.js +1 -1
  105. package/dist/helpers/solid-microservice-adapter.service.js.map +1 -1
  106. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  107. package/dist/helpers/typeorm-db-helper.js +9 -0
  108. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  109. package/dist/index.d.ts +6 -0
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +6 -0
  112. package/dist/index.js.map +1 -1
  113. package/dist/interfaces/active-user-data.interface.d.ts +1 -0
  114. package/dist/interfaces/active-user-data.interface.d.ts.map +1 -1
  115. package/dist/interfaces/active-user-data.interface.js.map +1 -1
  116. package/dist/repository/user-device-credential.repository.d.ts +12 -0
  117. package/dist/repository/user-device-credential.repository.d.ts.map +1 -0
  118. package/dist/repository/user-device-credential.repository.js +34 -0
  119. package/dist/repository/user-device-credential.repository.js.map +1 -0
  120. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  121. package/dist/seeders/module-metadata-seeder.service.js +7 -3
  122. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  123. package/dist/seeders/seed-data/solid-core-metadata.json +168 -2
  124. package/dist/services/api-key.service.d.ts.map +1 -1
  125. package/dist/services/api-key.service.js +1 -1
  126. package/dist/services/api-key.service.js.map +1 -1
  127. package/dist/services/authentication.service.d.ts +5 -4
  128. package/dist/services/authentication.service.d.ts.map +1 -1
  129. package/dist/services/authentication.service.js +50 -19
  130. package/dist/services/authentication.service.js.map +1 -1
  131. package/dist/services/chatter-message.service.d.ts +5 -1
  132. package/dist/services/chatter-message.service.d.ts.map +1 -1
  133. package/dist/services/chatter-message.service.js +48 -13
  134. package/dist/services/chatter-message.service.js.map +1 -1
  135. package/dist/services/crud-helper.service.d.ts +1 -2
  136. package/dist/services/crud-helper.service.d.ts.map +1 -1
  137. package/dist/services/crud-helper.service.js +2 -16
  138. package/dist/services/crud-helper.service.js.map +1 -1
  139. package/dist/services/crud.service.d.ts.map +1 -1
  140. package/dist/services/crud.service.js +2 -1
  141. package/dist/services/crud.service.js.map +1 -1
  142. package/dist/services/draft-publish-helper.service.d.ts +1 -0
  143. package/dist/services/draft-publish-helper.service.d.ts.map +1 -1
  144. package/dist/services/draft-publish-helper.service.js +38 -4
  145. package/dist/services/draft-publish-helper.service.js.map +1 -1
  146. package/dist/services/field-metadata.service.d.ts +2 -2
  147. package/dist/services/field-metadata.service.d.ts.map +1 -1
  148. package/dist/services/field-metadata.service.js +4 -1
  149. package/dist/services/field-metadata.service.js.map +1 -1
  150. package/dist/services/media-storage.utils.d.ts +1 -0
  151. package/dist/services/media-storage.utils.d.ts.map +1 -1
  152. package/dist/services/media-storage.utils.js +12 -1
  153. package/dist/services/media-storage.utils.js.map +1 -1
  154. package/dist/services/media.service.d.ts.map +1 -1
  155. package/dist/services/media.service.js +1 -0
  156. package/dist/services/media.service.js.map +1 -1
  157. package/dist/services/module-metadata.service.d.ts.map +1 -1
  158. package/dist/services/module-metadata.service.js +9 -1
  159. package/dist/services/module-metadata.service.js.map +1 -1
  160. package/dist/services/mpin.service.d.ts +67 -0
  161. package/dist/services/mpin.service.d.ts.map +1 -0
  162. package/dist/services/mpin.service.js +282 -0
  163. package/dist/services/mpin.service.js.map +1 -0
  164. package/dist/services/refresh-token-ids-storage.service.d.ts +25 -6
  165. package/dist/services/refresh-token-ids-storage.service.d.ts.map +1 -1
  166. package/dist/services/refresh-token-ids-storage.service.js +80 -42
  167. package/dist/services/refresh-token-ids-storage.service.js.map +1 -1
  168. package/dist/services/setting.service.d.ts.map +1 -1
  169. package/dist/services/setting.service.js +6 -1
  170. package/dist/services/setting.service.js.map +1 -1
  171. package/dist/services/settings/default-settings-provider.service.d.ts +202 -6
  172. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  173. package/dist/services/settings/default-settings-provider.service.js +114 -3
  174. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  175. package/dist/solid-core.module.d.ts +1 -3
  176. package/dist/solid-core.module.d.ts.map +1 -1
  177. package/dist/solid-core.module.js +14 -39
  178. package/dist/solid-core.module.js.map +1 -1
  179. package/dist/testing/reporter/console-reporter.d.ts +3 -0
  180. package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
  181. package/dist/testing/reporter/console-reporter.js +2 -0
  182. package/dist/testing/reporter/console-reporter.js.map +1 -1
  183. package/dist/testing/reporter/progress-reporter.d.ts +64 -0
  184. package/dist/testing/reporter/progress-reporter.d.ts.map +1 -0
  185. package/dist/testing/reporter/progress-reporter.js +224 -0
  186. package/dist/testing/reporter/progress-reporter.js.map +1 -0
  187. package/dist/testing/reporter/reporter.types.d.ts +3 -0
  188. package/dist/testing/reporter/reporter.types.d.ts.map +1 -1
  189. package/dist/testing/reporter/reporter.types.js.map +1 -1
  190. package/dist/testing/runner/run-from-metadata.d.ts.map +1 -1
  191. package/dist/testing/runner/run-from-metadata.js +1 -0
  192. package/dist/testing/runner/run-from-metadata.js.map +1 -1
  193. package/dist/theme/theme-registry.d.ts +14 -0
  194. package/dist/theme/theme-registry.d.ts.map +1 -0
  195. package/dist/theme/theme-registry.js +147 -0
  196. package/dist/theme/theme-registry.js.map +1 -0
  197. package/package.json +2 -2
  198. package/src/commands/run-tests.command.ts +17 -3
  199. package/src/config/cache.options.ts +9 -1
  200. package/src/constants/error-messages.ts +14 -0
  201. package/src/constants/media-file-types.ts +87 -9
  202. package/src/constants/success-messages.ts +5 -0
  203. package/src/controllers/authentication.controller.ts +5 -2
  204. package/src/controllers/mpin-authentication.controller.ts +67 -0
  205. package/src/dtos/change-mpin.dto.ts +18 -0
  206. package/src/dtos/create-field-metadata.dto.ts +17 -9
  207. package/src/dtos/create-model-sequence.dto.ts +1 -1
  208. package/src/dtos/mpin-login.dto.ts +24 -0
  209. package/src/dtos/setup-mpin.dto.ts +51 -0
  210. package/src/entities/dashboard-user-layout.entity.ts +2 -2
  211. package/src/entities/field-metadata.entity.ts +3 -0
  212. package/src/entities/user-device-credential.entity.ts +130 -0
  213. package/src/entities/user.entity.ts +2 -2
  214. package/src/entities/view-metadata.entity.ts +2 -2
  215. package/src/entities/workflow-execution-artifact.entity.ts +4 -3
  216. package/src/entities/workflow-execution-log.entity.ts +4 -3
  217. package/src/entities/workflow-execution.entity.ts +3 -2
  218. package/src/entities/workflow-secret.entity.ts +3 -2
  219. package/src/entities/workflow-step-execution.entity.ts +3 -2
  220. package/src/entities/workflow-trigger-execution.entity.ts +6 -5
  221. package/src/filters/http-exception.filter.ts +11 -1
  222. package/src/helpers/bootstrap.helper.ts +43 -6
  223. package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +73 -34
  224. package/src/helpers/file-download.helper.ts +15 -1
  225. package/src/helpers/security.helper.ts +35 -8
  226. package/src/helpers/solid-core-error-codes-provider.service.ts +53 -0
  227. package/src/helpers/solid-microservice-adapter.service.ts +6 -1
  228. package/src/helpers/typeorm-db-helper.ts +12 -1
  229. package/src/index.ts +6 -0
  230. package/src/interfaces/active-user-data.interface.ts +9 -0
  231. package/src/repository/user-device-credential.repository.ts +17 -0
  232. package/src/seeders/module-metadata-seeder.service.ts +11 -3
  233. package/src/seeders/seed-data/solid-core-metadata.json +168 -2
  234. package/src/services/api-key.service.ts +13 -1
  235. package/src/services/authentication.service.ts +127 -36
  236. package/src/services/chatter-message.service.ts +78 -15
  237. package/src/services/crud-helper.service.ts +19 -16
  238. package/src/services/crud.service.ts +3 -1
  239. package/src/services/draft-publish-helper.service.ts +56 -4
  240. package/src/services/field-metadata.service.ts +5 -2
  241. package/src/services/media-storage.utils.ts +26 -1
  242. package/src/services/media.service.ts +1 -0
  243. package/src/services/module-metadata.service.ts +10 -1
  244. package/src/services/mpin.service.ts +447 -0
  245. package/src/services/refresh-token-ids-storage.service.ts +229 -90
  246. package/src/services/setting.service.ts +8 -1
  247. package/src/services/settings/default-settings-provider.service.ts +119 -3
  248. package/src/solid-core.module.ts +26 -12
  249. package/src/testing/README.md +2 -1
  250. package/src/testing/reporter/console-reporter.ts +4 -0
  251. package/src/testing/reporter/progress-reporter.ts +251 -0
  252. package/src/testing/reporter/reporter.types.ts +3 -0
  253. package/src/testing/runner/run-from-metadata.ts +1 -0
  254. package/src/theme/theme-registry.ts +181 -0
  255. package/CLAUDE.md +0 -26
@@ -0,0 +1,51 @@
1
+ import { IsNotEmpty, IsOptional, IsString, IsUUID, Matches, MaxLength } from 'class-validator';
2
+
3
+ export class SetupMpinDto {
4
+ /**
5
+ * Stable identifier for the physical device, generated once by the client.
6
+ * Validated as a UUID before it ever reaches a cache key or a bucket name,
7
+ * so it cannot smuggle delimiters into the keyspace.
8
+ */
9
+ @IsUUID()
10
+ deviceId: string;
11
+
12
+ /**
13
+ * A string, not a number, even though it looks like digits.
14
+ *
15
+ * A numeric MPIN cannot carry a leading zero: JSON forbids leading zeros in
16
+ * number literals, so a client serialising `012345` numerically emits
17
+ * `12345` and the server stores a five-digit MPIN the user never chose -
18
+ * which still satisfies the default `^\d{4,6}$` and so raises no error.
19
+ * `mpinRegex` is also tenant-configurable and may permit non-digits.
20
+ *
21
+ * Note this cannot be enforced here: the global ValidationPipe runs with
22
+ * `enableImplicitConversion`, so a numeric payload is coerced to a string
23
+ * before @IsString() sees it. Clients must send a quoted JSON string; that
24
+ * requirement is documented in the mobile integration guide.
25
+ *
26
+ * Length and shape are checked server-side against `mpinRegex`, since that
27
+ * setting is the single authority. This only bounds the input.
28
+ */
29
+ @IsNotEmpty()
30
+ @IsString()
31
+ @MaxLength(32)
32
+ mpin: string;
33
+
34
+ /**
35
+ * Rendered in a device-management list, so it is bounded here rather than
36
+ * trusting every consuming UI to escape it.
37
+ */
38
+ @IsOptional()
39
+ @IsString()
40
+ @MaxLength(64)
41
+ @Matches(/^[\w\s.'()-]*$/, {
42
+ message: 'deviceName may contain only letters, numbers, spaces and . \' ( ) - _',
43
+ })
44
+ deviceName?: string;
45
+
46
+ @IsOptional()
47
+ @IsString()
48
+ @MaxLength(32)
49
+ @Matches(/^[\w.-]*$/, { message: 'platform may contain only letters, numbers and . - _' })
50
+ platform?: string;
51
+ }
@@ -6,12 +6,12 @@ import { ModuleMetadata } from 'src/entities/module-metadata.entity'
6
6
  @Entity('ss_dashboard_user_layout')
7
7
  export class DashboardUserLayout extends CommonEntity {
8
8
  @Index()
9
- @ManyToOne(() => User, { onDelete: "CASCADE", nullable: false })
9
+ @ManyToOne(() => User, { nullable: false })
10
10
  @JoinColumn()
11
11
  user: User;
12
12
 
13
13
  @Index()
14
- @ManyToOne(() => ModuleMetadata, { onDelete: "CASCADE", nullable: false })
14
+ @ManyToOne(() => ModuleMetadata, { nullable: false })
15
15
  @JoinColumn()
16
16
  module: ModuleMetadata;
17
17
 
@@ -77,6 +77,9 @@ export class FieldMetadata extends CommonEntity {
77
77
  @Column({ name: 'media_max_size_kb', nullable: true })
78
78
  mediaMaxSizeKb: number;
79
79
 
80
+ @Column({ name: 'media_allowed_extensions', nullable: true, type: "simple-json" })
81
+ mediaAllowedExtensions: string[];
82
+
80
83
  @Index()
81
84
  @ManyToOne(() => MediaStorageProviderMetadata, { onDelete: 'SET NULL' })
82
85
  @JoinColumn({ name: 'media_storage_provider_id' })
@@ -0,0 +1,130 @@
1
+ import { Exclude, Expose } from "class-transformer";
2
+ import { CommonEntity } from "src/entities/common.entity";
3
+ import { Column, Entity, Index, ManyToOne } from "typeorm";
4
+ import { User } from "./user.entity";
5
+
6
+ /**
7
+ * A per-device MPIN credential.
8
+ *
9
+ * Login presents `credentialRef` (a server-issued opaque handle) plus the MPIN.
10
+ * The handle - not a mobile number or email - is what identifies the account,
11
+ * which is why the login route needs no identifier and offers no user
12
+ * enumeration surface.
13
+ */
14
+ @Entity("ss_user_device_credential")
15
+ // Non-unique: one physical device may legitimately hold credentials for
16
+ // several accounts, and each account setup mints its own row.
17
+ @Index(["user", "deviceId"])
18
+ @Exclude()
19
+ export class UserDeviceCredential extends CommonEntity {
20
+
21
+ // SHA-256 of the opaque handle, and the login lookup key. Never exposed:
22
+ // the raw handle is returned to the client once at setup and never stored.
23
+ // SHA-256 rather than bcrypt because the handle is 256 bits of randomness -
24
+ // there is nothing to brute-force, so a slow hash would only add latency.
25
+ // Same treatment as UserApiKey.hashedKey.
26
+ @Index({ unique: true })
27
+ @Column()
28
+ hashedCredentialRef: string;
29
+
30
+ // bcrypt hash of the MPIN, via HashingService. Never exposed - same
31
+ // treatment as User.password. Slow hashing is warranted here: a 4-6 digit
32
+ // secret is exactly the low-entropy case bcrypt exists for.
33
+ @Column()
34
+ hashedMpin: string;
35
+
36
+ // Mirrors User.passwordScheme / passwordSchemeVersion so
37
+ // HashingService.needsRehash can upgrade the hash on a successful login.
38
+ @Column()
39
+ mpinScheme: string;
40
+
41
+ /**
42
+ * The hashing *policy* version this row was hashed under - not a version of
43
+ * MPIN itself. It comes from `HashingService.currentVersion()`, which
44
+ * `BcryptService` currently returns as **2**, so freshly created rows read
45
+ * 2, exactly as `ss_user.password_scheme_version` does.
46
+ *
47
+ * It matters because `BcryptService.normalize` applies the configured
48
+ * pepper only when `version >= 2`. Storing the version per row means an
49
+ * older hash still verifies after the policy changes: `compare()` uses the
50
+ * version the row was written with, and `needsRehash()` upgrades it on the
51
+ * next successful login.
52
+ *
53
+ * The `default: 1` is inherited from the `User` precedent and is never
54
+ * reached, since every write path sets this explicitly. Note it would be
55
+ * *wrong* if it ever were: a row hashed at 2 but recorded as 1 would be
56
+ * compared without the pepper and could never authenticate.
57
+ */
58
+ @Column({ default: 1 })
59
+ mpinSchemeVersion: number;
60
+
61
+ // Client-supplied, stable for the physical device. Used for grouping and
62
+ // the device-management UI only - never a lookup key. Deliberately NOT
63
+ // unique: several accounts on one device share a deviceId.
64
+ @Expose()
65
+ @Column()
66
+ deviceId: string;
67
+
68
+ /**
69
+ * Human-readable natural key - `<username>-<deviceId>` - unique per row.
70
+ *
71
+ * Neither column alone can serve as the model's user key: `deviceId` is
72
+ * shared by every account on a device, and `deviceName` is nullable.
73
+ * Combining them is unique in both directions - one user across many
74
+ * devices, and many users on one device.
75
+ *
76
+ * Populated by MpinService rather than declared as a `computed` field in
77
+ * the metadata manifest. Computed fields are evaluated by
78
+ * ComputedFieldCrudManager on the generic CRUD path (`crud.service.ts`),
79
+ * which this service bypasses by writing through the repository directly,
80
+ * so a computed declaration would silently leave this null. It is also
81
+ * beyond ConcatComputedFieldProvider, which resolves `dto[field]` flatly
82
+ * within one model and cannot reach `username` across the user relation.
83
+ */
84
+ @Expose()
85
+ @Index({ unique: true })
86
+ @Column()
87
+ credentialKey: string;
88
+
89
+ @Expose()
90
+ @Column({ nullable: true })
91
+ deviceName: string;
92
+
93
+ @Expose()
94
+ @Column({ nullable: true })
95
+ platform: string;
96
+
97
+ // Revocation is a soft-delete: the row survives so that a correct MPIN can
98
+ // be answered with "this device was removed" rather than a baffling
99
+ // "incorrect MPIN". Only active rows count towards mpinMaxDevicesPerUser.
100
+ @Expose()
101
+ @Column({ default: true })
102
+ isActive: boolean;
103
+
104
+ // Cumulative, and reset only on a successful login - never when a lockout
105
+ // expires. That is what makes the second threshold detectable without a
106
+ // separate lockout counter.
107
+ @Expose()
108
+ @Column({ default: 0 })
109
+ failedAttempts: number;
110
+
111
+ @Expose()
112
+ @Column({ nullable: true })
113
+ lockedUntil: Date;
114
+
115
+ @Expose()
116
+ @Column({ nullable: true })
117
+ lastUsedAt: Date;
118
+
119
+ // Deliberately no inverse property on User. Nothing needs
120
+ // `user.deviceCredentials`, and leaving the most widely referenced entity
121
+ // in the system untouched keeps this feature purely additive.
122
+ //
123
+ // `nullable: false` because TypeORM makes relations nullable by default,
124
+ // which would let a credential exist with no owner. Such a row could never
125
+ // authenticate - loginWithMpin rejects it - so it would simply be dead
126
+ // data, but the database should refuse it outright. This also keeps the
127
+ // column honest against the metadata manifest, which declares it required.
128
+ @ManyToOne(() => User, { nullable: false })
129
+ user: User;
130
+ }
@@ -36,12 +36,12 @@ export class User extends CommonEntity {
36
36
  @Expose()
37
37
  username: string;
38
38
 
39
- @Index({ unique: true })
39
+ @Index()
40
40
  @Column({ type: "varchar", nullable: true })
41
41
  @Expose()
42
42
  email: string;
43
43
 
44
- @Index({ unique: true })
44
+ @Index()
45
45
  @Column({ type: "varchar", nullable: true })
46
46
  @Expose()
47
47
  mobile: string;
@@ -19,11 +19,11 @@ export class ViewMetadata extends CommonEntity {
19
19
  @Column({ name: "type", type: "varchar" })
20
20
  type: string;
21
21
 
22
- //TODO: To make this truly cross db compatible, we should avoid setting a db type
22
+ // TODO: To make this truly cross db compatible, we should avoid setting a db type
23
23
  @Column({ name: "context", type: "text" })
24
24
  context: any = "{}";
25
25
 
26
- //TODO: To make this truly cross db compatible, we should avoid setting a db type
26
+ // TODO: To make this truly cross db compatible, we should avoid setting a db type
27
27
  @Column({ name: "layout", type: "text" })
28
28
  layout: any;
29
29
 
@@ -2,6 +2,7 @@ import { CommonEntity } from 'src/entities/common.entity';
2
2
  import { Entity, Column, Index, JoinColumn, ManyToOne } from 'typeorm';
3
3
  import { WorkflowExecution } from './workflow-execution.entity';
4
4
  import { WorkflowStepExecution } from './workflow-step-execution.entity'
5
+ import { getColumnType } from 'src/helpers/typeorm-db-helper';
5
6
 
6
7
  @Entity('ss_workflow_execution_artifact')
7
8
  export class WorkflowExecutionArtifact extends CommonEntity {
@@ -10,12 +11,12 @@ export class WorkflowExecutionArtifact extends CommonEntity {
10
11
  artifactKey: string;
11
12
 
12
13
  @Index()
13
- @ManyToOne(() => WorkflowExecution, { onDelete: "CASCADE", nullable: false })
14
+ @ManyToOne(() => WorkflowExecution, { nullable: false })
14
15
  @JoinColumn()
15
16
  workflowExecution: WorkflowExecution;
16
17
 
17
18
  @Index()
18
- @ManyToOne(() => WorkflowStepExecution, { onDelete: "CASCADE", nullable: true })
19
+ @ManyToOne(() => WorkflowStepExecution, { nullable: true })
19
20
  @JoinColumn()
20
21
  workflowStepExecution: WorkflowStepExecution;
21
22
 
@@ -61,7 +62,7 @@ export class WorkflowExecutionArtifact extends CommonEntity {
61
62
  checksum: string;
62
63
 
63
64
  @Index()
64
- @Column({ type: "timestamp", nullable: true })
65
+ @Column({ nullable: true, ...getColumnType('datetime') })
65
66
  producedAt: Date;
66
67
 
67
68
  @Column({ type: "simple-json", nullable: true })
@@ -2,6 +2,7 @@ import { CommonEntity } from 'src/entities/common.entity';
2
2
  import { Entity, Column, Index, JoinColumn, ManyToOne } from 'typeorm';
3
3
  import { WorkflowExecution } from './workflow-execution.entity';
4
4
  import { WorkflowStepExecution } from './workflow-step-execution.entity'
5
+ import { getColumnType } from 'src/helpers/typeorm-db-helper';
5
6
 
6
7
  @Entity('ss_workflow_execution_log')
7
8
  export class WorkflowExecutionLog extends CommonEntity {
@@ -10,12 +11,12 @@ export class WorkflowExecutionLog extends CommonEntity {
10
11
  logKey: string;
11
12
 
12
13
  @Index()
13
- @ManyToOne(() => WorkflowExecution, { onDelete: "CASCADE", nullable: false })
14
+ @ManyToOne(() => WorkflowExecution, { nullable: false })
14
15
  @JoinColumn()
15
16
  workflowExecution: WorkflowExecution;
16
17
 
17
18
  @Index()
18
- @ManyToOne(() => WorkflowStepExecution, { onDelete: "CASCADE", nullable: true })
19
+ @ManyToOne(() => WorkflowStepExecution, { nullable: true })
19
20
  @JoinColumn()
20
21
  workflowStepExecution: WorkflowStepExecution;
21
22
 
@@ -47,7 +48,7 @@ export class WorkflowExecutionLog extends CommonEntity {
47
48
  sequenceNumber: number;
48
49
 
49
50
  @Index()
50
- @Column({ type: "timestamp" })
51
+ @Column({ ...getColumnType('datetime') })
51
52
  occurredAt: Date;
52
53
 
53
54
  @Column({ type: "simple-json", nullable: true })
@@ -4,6 +4,7 @@ import { WorkflowDefinition } from './workflow-definition.entity'
4
4
  import { WorkflowStepExecution } from './workflow-step-execution.entity';
5
5
  import { WorkflowExecutionLog } from './workflow-execution-log.entity';
6
6
  import { WorkflowExecutionArtifact } from './workflow-execution-artifact.entity';
7
+ import { getColumnType } from 'src/helpers/typeorm-db-helper';
7
8
 
8
9
  @Entity('ss_workflow_execution')
9
10
  export class WorkflowExecution extends CommonEntity {
@@ -32,11 +33,11 @@ export class WorkflowExecution extends CommonEntity {
32
33
  triggerType: string = "manual";
33
34
 
34
35
  @Index()
35
- @Column({ type: "timestamp", nullable: true })
36
+ @Column({ nullable: true, ...getColumnType('datetime') })
36
37
  startedAt: Date;
37
38
 
38
39
  @Index()
39
- @Column({ type: "timestamp", nullable: true })
40
+ @Column({ nullable: true, ...getColumnType('datetime') })
40
41
  finishedAt: Date;
41
42
 
42
43
  @Column({ type: "bigint", nullable: true })
@@ -1,5 +1,6 @@
1
1
  import { CommonEntity } from 'src/entities/common.entity';
2
2
  import { Entity, Column, Index } from 'typeorm';
3
+ import { getColumnType } from 'src/helpers/typeorm-db-helper';
3
4
 
4
5
  @Entity('ss_workflow_secret')
5
6
  export class WorkflowSecret extends CommonEntity {
@@ -25,9 +26,9 @@ export class WorkflowSecret extends CommonEntity {
25
26
  @Column({ type: "varchar", default: "active" })
26
27
  status: string = "active";
27
28
 
28
- @Column({ type: "timestamp", nullable: true })
29
+ @Column({ nullable: true, ...getColumnType('datetime') })
29
30
  lastRotatedAt: Date;
30
31
 
31
- @Column({ type: "timestamp", nullable: true })
32
+ @Column({ nullable: true, ...getColumnType('datetime') })
32
33
  lastAccessedAt: Date;
33
34
  }
@@ -1,6 +1,7 @@
1
1
  import { CommonEntity } from 'src/entities/common.entity';
2
2
  import { Entity, Column, Index, JoinColumn, ManyToOne } from 'typeorm';
3
3
  import { WorkflowExecution } from './workflow-execution.entity'
4
+ import { getColumnType } from 'src/helpers/typeorm-db-helper';
4
5
 
5
6
  @Entity('ss_workflow_step_execution')
6
7
  export class WorkflowStepExecution extends CommonEntity {
@@ -55,11 +56,11 @@ export class WorkflowStepExecution extends CommonEntity {
55
56
  sequenceNumber: number;
56
57
 
57
58
  @Index()
58
- @Column({ type: "timestamp", nullable: true })
59
+ @Column({ nullable: true, ...getColumnType('datetime') })
59
60
  startedAt: Date;
60
61
 
61
62
  @Index()
62
- @Column({ type: "timestamp", nullable: true })
63
+ @Column({ nullable: true, ...getColumnType('datetime') })
63
64
  finishedAt: Date;
64
65
 
65
66
  @Column({ type: "bigint", nullable: true })
@@ -2,6 +2,7 @@ import { CommonEntity } from 'src/entities/common.entity';
2
2
  import { Entity, Column, Index, JoinColumn, ManyToOne } from 'typeorm';
3
3
  import { WorkflowDefinition } from './workflow-definition.entity';
4
4
  import { WorkflowExecution } from './workflow-execution.entity'
5
+ import { getColumnType } from 'src/helpers/typeorm-db-helper';
5
6
 
6
7
  @Entity('ss_workflow_trigger_execution')
7
8
  export class WorkflowTriggerExecution extends CommonEntity {
@@ -10,12 +11,12 @@ export class WorkflowTriggerExecution extends CommonEntity {
10
11
  triggerExecutionKey: string;
11
12
 
12
13
  @Index()
13
- @ManyToOne(() => WorkflowDefinition, { onDelete: "CASCADE", nullable: false })
14
+ @ManyToOne(() => WorkflowDefinition, { nullable: false })
14
15
  @JoinColumn()
15
16
  workflowDefinition: WorkflowDefinition;
16
17
 
17
18
  @Index()
18
- @ManyToOne(() => WorkflowExecution, { onDelete: "CASCADE", nullable: true })
19
+ @ManyToOne(() => WorkflowExecution, { nullable: true })
19
20
  @JoinColumn()
20
21
  workflowExecution: WorkflowExecution;
21
22
 
@@ -39,15 +40,15 @@ export class WorkflowTriggerExecution extends CommonEntity {
39
40
  matched: boolean = false;
40
41
 
41
42
  @Index()
42
- @Column({ type: "timestamp" })
43
+ @Column({ ...getColumnType('datetime') })
43
44
  firedAt: Date;
44
45
 
45
46
  @Index()
46
- @Column({ type: "timestamp", nullable: true })
47
+ @Column({ nullable: true, ...getColumnType('datetime') })
47
48
  startedAt: Date;
48
49
 
49
50
  @Index()
50
- @Column({ type: "timestamp", nullable: true })
51
+ @Column({ nullable: true, ...getColumnType('datetime') })
51
52
  finishedAt: Date;
52
53
 
53
54
  @Column({ type: "bigint", nullable: true })
@@ -30,12 +30,22 @@ export class HttpExceptionFilter implements ExceptionFilter {
30
30
  const isHttp = exception instanceof HttpException;
31
31
  const explicitStatus = isHttp ? exception.getStatus() : undefined;
32
32
 
33
+ // Errors from http-errors-style libraries (e.g. body-parser's PayloadTooLargeError)
34
+ // are plain Errors carrying their own status. Honour it rather than collapsing to 500.
35
+ const rawStatus = exception?.status ?? exception?.statusCode;
36
+ const derivedStatus =
37
+ typeof rawStatus === 'number' && rawStatus >= 400 && rawStatus < 600
38
+ ? rawStatus
39
+ : undefined;
40
+
33
41
  // Canonical code + static message
34
42
  const code: ErrorCode = this.errorMapper.mapException(exception);
35
43
  const defaultStatus = this.errorMapper.getHttpStatus(code);
36
44
  const message = code === 'solidx-unknown-error' ? `${exception?.message}` : this.errorMapper.getMessage(code);
37
45
 
38
- const status = explicitStatus ?? defaultStatus ?? 500;
46
+ // derivedStatus must precede defaultStatus: getHttpStatus() returns a hard 500
47
+ // for unmatched codes, so it is never undefined and would always win otherwise.
48
+ const status = explicitStatus ?? derivedStatus ?? defaultStatus ?? 500;
39
49
 
40
50
  // Logging
41
51
  this.logger.error(
@@ -1,5 +1,6 @@
1
1
  import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
2
2
  import { NestFactory, Reflector } from '@nestjs/core';
3
+ import { NestExpressApplication } from '@nestjs/platform-express';
3
4
  import { WsAdapter } from '@nestjs/platform-ws';
4
5
  import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
5
6
  import { NextFunction, Request, Response } from 'express';
@@ -12,7 +13,12 @@ import { CommandFactory } from 'nest-commander';
12
13
  import { createWinstonLoggerConfig } from '../winston.logger';
13
14
  import { WrapResponseInterceptor } from '../interceptors/wrap-response.interceptor';
14
15
  import { buildDefaultCorsOptions } from './cors.helper';
15
- import { buildDefaultSecurityHeaderOptions, buildPermissionsPolicyHeader, PermissionsPolicyConfig } from './security.helper';
16
+ import {
17
+ buildDefaultSecurityHeaderOptions,
18
+ buildPermissionsPolicyHeader,
19
+ PermissionsPolicyConfig,
20
+ SolidSecurityOptions,
21
+ } from './security.helper';
16
22
  import { parseBooleanEnv } from './environment.helper';
17
23
 
18
24
  // ---- Shared process handlers ----
@@ -50,8 +56,20 @@ export interface SolidBootstrapOptions {
50
56
  swagger?: SolidSwaggerOptions | false;
51
57
  /** Permissions-Policy header overrides (merged with defaults). */
52
58
  permissionsPolicyOverrides?: Partial<PermissionsPolicyConfig>;
59
+ /** Security header overrides, including iframe frame-ancestor allowlists. */
60
+ security?: SolidSecurityOptions;
53
61
  /** Show full NestJS init logs during bootstrap (route mapping, module deps, pollers). Defaults to false. */
54
62
  verboseBootstrap?: boolean;
63
+ /**
64
+ * Max request body size for JSON and urlencoded payloads, as a `bytes`-compatible
65
+ * string (e.g. '10mb', '512kb'). Defaults to '10mb'.
66
+ *
67
+ * Does NOT apply to multipart/form-data uploads: body-parser matches on Content-Type,
68
+ * so multipart requests skip these parsers entirely and are handled by Multer. Raise
69
+ * this only for large structured JSON (e.g. /bulk arrays), bearing in mind the parsed
70
+ * object graph can retain several times the raw byte size.
71
+ */
72
+ bodyLimit?: string;
55
73
  }
56
74
 
57
75
  /**
@@ -64,19 +82,30 @@ export interface SolidBootstrapOptions {
64
82
  * // main.ts
65
83
  * bootstrapSolidApp(() => AppModule.forRoot(), {
66
84
  * swagger: { title: 'My API', description: 'My API description' },
85
+ * security: { frameAncestors: ["'self'", 'https://portal.example.com'] },
67
86
  * });
68
87
  */
69
88
  export async function bootstrapSolidApp(
70
89
  appModuleFactory: () => Promise<any>,
71
90
  options: SolidBootstrapOptions = {},
72
- ): Promise<void> {
91
+ ): Promise<NestExpressApplication> {
73
92
  registerGlobalProcessHandlers();
74
93
 
75
- const { globalPrefix = 'api', swagger = {}, permissionsPolicyOverrides = {}, verboseBootstrap = false } = options;
94
+ const {
95
+ globalPrefix = 'api',
96
+ swagger = {},
97
+ permissionsPolicyOverrides = {},
98
+ security = {},
99
+ verboseBootstrap = false,
100
+ bodyLimit = '10mb',
101
+ } = options;
76
102
 
77
103
  const startTime = Date.now();
78
104
  const appModule = await appModuleFactory();
79
- const app = await NestFactory.create(appModule, {
105
+ const app = await NestFactory.create<NestExpressApplication>(appModule, {
106
+ // Nest's default parsers are registered during init() with a 100kb limit, ahead of
107
+ // any module middleware. Disable them so the parsers below own the limit outright.
108
+ bodyParser: false,
80
109
  logger: WinstonModule.createLogger({ ...createWinstonLoggerConfig(), level: verboseBootstrap ? 'debug' : 'error' }),
81
110
  });
82
111
 
@@ -87,7 +116,7 @@ export async function bootstrapSolidApp(
87
116
  app
88
117
  .get(WINSTON_MODULE_NEST_PROVIDER)
89
118
  .log('API server disabled via API_ENABLED=false. Skipping HTTP listen.', 'Bootstrap');
90
- return;
119
+ return app;
91
120
  }
92
121
 
93
122
  // Health check at root path
@@ -95,7 +124,7 @@ export async function bootstrapSolidApp(
95
124
  server.get('/', (_req, res) => res.status(200).send('SOLID OK'));
96
125
 
97
126
  // Security headers
98
- app.use(helmet(buildDefaultSecurityHeaderOptions()));
127
+ app.use(helmet(buildDefaultSecurityHeaderOptions(security)));
99
128
 
100
129
  // Nest's Swagger UI HTML injects inline styles; keep CSP strict elsewhere.
101
130
  const isSwaggerPath = (path: string) =>
@@ -136,6 +165,12 @@ export async function bootstrapSolidApp(
136
165
  next();
137
166
  });
138
167
 
168
+ // Body parsers — replaces Nest's default 100kb parsers (disabled via bodyParser: false).
169
+ // Registered here so the middleware order Nest applied by default is preserved:
170
+ // helmet -> CSP -> Permissions-Policy -> qs -> body parsers -> routes.
171
+ app.useBodyParser('json', { limit: bodyLimit });
172
+ app.useBodyParser('urlencoded', { limit: bodyLimit, extended: true });
173
+
139
174
  // Global ValidationPipe
140
175
  app.useGlobalPipes(
141
176
  new ValidationPipe({
@@ -195,6 +230,8 @@ export async function bootstrapSolidApp(
195
230
 
196
231
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
197
232
  process.stdout.write(`\x1b[32mServer started on port ${port} in ${elapsed}s\x1b[0m\n`);
233
+
234
+ return app;
198
235
  }
199
236
 
200
237
  // ---- CLI bootstrap ----