@friggframework/core 2.0.0-next.6 → 2.0.0-next.60

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 (285) hide show
  1. package/CLAUDE.md +694 -0
  2. package/README.md +959 -50
  3. package/application/commands/README.md +451 -0
  4. package/application/commands/credential-commands.js +245 -0
  5. package/application/commands/entity-commands.js +336 -0
  6. package/application/commands/integration-commands.js +210 -0
  7. package/application/commands/user-commands.js +283 -0
  8. package/application/index.js +69 -0
  9. package/core/CLAUDE.md +690 -0
  10. package/core/Worker.js +8 -21
  11. package/core/create-handler.js +14 -7
  12. package/credential/repositories/credential-repository-documentdb.js +304 -0
  13. package/credential/repositories/credential-repository-factory.js +54 -0
  14. package/credential/repositories/credential-repository-interface.js +98 -0
  15. package/credential/repositories/credential-repository-mongo.js +269 -0
  16. package/credential/repositories/credential-repository-postgres.js +291 -0
  17. package/credential/repositories/credential-repository.js +302 -0
  18. package/credential/use-cases/get-credential-for-user.js +25 -0
  19. package/credential/use-cases/update-authentication-status.js +15 -0
  20. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  21. package/database/adapters/lambda-invoker.js +97 -0
  22. package/database/config.js +154 -0
  23. package/database/documentdb-encryption-service.js +330 -0
  24. package/database/documentdb-utils.js +136 -0
  25. package/database/encryption/README.md +839 -0
  26. package/database/encryption/documentdb-encryption-service.md +3575 -0
  27. package/database/encryption/encryption-schema-registry.js +268 -0
  28. package/database/encryption/field-encryption-service.js +226 -0
  29. package/database/encryption/logger.js +79 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/index.js +61 -21
  32. package/database/models/WebsocketConnection.js +16 -10
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +182 -0
  35. package/database/repositories/health-check-repository-documentdb.js +134 -0
  36. package/database/repositories/health-check-repository-factory.js +48 -0
  37. package/database/repositories/health-check-repository-interface.js +82 -0
  38. package/database/repositories/health-check-repository-mongodb.js +89 -0
  39. package/database/repositories/health-check-repository-postgres.js +82 -0
  40. package/database/repositories/health-check-repository.js +108 -0
  41. package/database/repositories/migration-status-repository-s3.js +137 -0
  42. package/database/use-cases/check-database-health-use-case.js +29 -0
  43. package/database/use-cases/check-database-state-use-case.js +81 -0
  44. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  45. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  46. package/database/use-cases/get-migration-status-use-case.js +93 -0
  47. package/database/use-cases/run-database-migration-use-case.js +139 -0
  48. package/database/use-cases/test-encryption-use-case.js +253 -0
  49. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  50. package/database/utils/mongodb-collection-utils.js +91 -0
  51. package/database/utils/mongodb-schema-init.js +106 -0
  52. package/database/utils/prisma-runner.js +477 -0
  53. package/database/utils/prisma-schema-parser.js +182 -0
  54. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  55. package/encrypt/Cryptor.js +34 -168
  56. package/encrypt/index.js +1 -2
  57. package/encrypt/test-encrypt.js +0 -2
  58. package/errors/client-safe-error.js +26 -0
  59. package/errors/fetch-error.js +2 -1
  60. package/errors/index.js +2 -0
  61. package/generated/prisma-mongodb/client.d.ts +1 -0
  62. package/generated/prisma-mongodb/client.js +4 -0
  63. package/generated/prisma-mongodb/default.d.ts +1 -0
  64. package/generated/prisma-mongodb/default.js +4 -0
  65. package/generated/prisma-mongodb/edge.d.ts +1 -0
  66. package/generated/prisma-mongodb/edge.js +334 -0
  67. package/generated/prisma-mongodb/index-browser.js +316 -0
  68. package/generated/prisma-mongodb/index.d.ts +22903 -0
  69. package/generated/prisma-mongodb/index.js +359 -0
  70. package/generated/prisma-mongodb/package.json +183 -0
  71. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  72. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  73. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  74. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  75. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  76. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  77. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  78. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  79. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  80. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  81. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  82. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  83. package/generated/prisma-mongodb/schema.prisma +360 -0
  84. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  85. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  86. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  87. package/generated/prisma-mongodb/wasm.js +341 -0
  88. package/generated/prisma-postgresql/client.d.ts +1 -0
  89. package/generated/prisma-postgresql/client.js +4 -0
  90. package/generated/prisma-postgresql/default.d.ts +1 -0
  91. package/generated/prisma-postgresql/default.js +4 -0
  92. package/generated/prisma-postgresql/edge.d.ts +1 -0
  93. package/generated/prisma-postgresql/edge.js +356 -0
  94. package/generated/prisma-postgresql/index-browser.js +338 -0
  95. package/generated/prisma-postgresql/index.d.ts +25077 -0
  96. package/generated/prisma-postgresql/index.js +381 -0
  97. package/generated/prisma-postgresql/package.json +183 -0
  98. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  99. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  100. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  101. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  102. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  103. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  104. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  105. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  106. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  107. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  108. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  109. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  110. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  111. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  112. package/generated/prisma-postgresql/schema.prisma +343 -0
  113. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  114. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  115. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  116. package/generated/prisma-postgresql/wasm.js +363 -0
  117. package/handlers/WEBHOOKS.md +653 -0
  118. package/handlers/app-definition-loader.js +38 -0
  119. package/handlers/app-handler-helpers.js +56 -0
  120. package/handlers/backend-utils.js +186 -0
  121. package/handlers/database-migration-handler.js +227 -0
  122. package/handlers/integration-event-dispatcher.js +54 -0
  123. package/handlers/routers/HEALTHCHECK.md +342 -0
  124. package/handlers/routers/auth.js +15 -0
  125. package/handlers/routers/db-migration.handler.js +29 -0
  126. package/handlers/routers/db-migration.js +326 -0
  127. package/handlers/routers/health.js +516 -0
  128. package/handlers/routers/integration-defined-routers.js +45 -0
  129. package/handlers/routers/integration-webhook-routers.js +67 -0
  130. package/handlers/routers/user.js +63 -0
  131. package/handlers/routers/websocket.js +57 -0
  132. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  133. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  134. package/handlers/workers/db-migration.js +352 -0
  135. package/handlers/workers/integration-defined-workers.js +27 -0
  136. package/index.js +77 -22
  137. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  138. package/integrations/index.js +12 -10
  139. package/integrations/integration-base.js +326 -55
  140. package/integrations/integration-router.js +374 -179
  141. package/integrations/options.js +1 -1
  142. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  143. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  144. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  145. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  146. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  147. package/integrations/repositories/integration-mapping-repository.js +156 -0
  148. package/integrations/repositories/integration-repository-documentdb.js +210 -0
  149. package/integrations/repositories/integration-repository-factory.js +51 -0
  150. package/integrations/repositories/integration-repository-interface.js +127 -0
  151. package/integrations/repositories/integration-repository-mongo.js +303 -0
  152. package/integrations/repositories/integration-repository-postgres.js +352 -0
  153. package/integrations/repositories/process-repository-documentdb.js +243 -0
  154. package/integrations/repositories/process-repository-factory.js +53 -0
  155. package/integrations/repositories/process-repository-interface.js +90 -0
  156. package/integrations/repositories/process-repository-mongo.js +190 -0
  157. package/integrations/repositories/process-repository-postgres.js +217 -0
  158. package/integrations/tests/doubles/dummy-integration-class.js +83 -0
  159. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  160. package/integrations/use-cases/create-integration.js +83 -0
  161. package/integrations/use-cases/create-process.js +128 -0
  162. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  163. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  164. package/integrations/use-cases/get-integration-for-user.js +78 -0
  165. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  166. package/integrations/use-cases/get-integration-instance.js +83 -0
  167. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  168. package/integrations/use-cases/get-possible-integrations.js +27 -0
  169. package/integrations/use-cases/get-process.js +87 -0
  170. package/integrations/use-cases/index.js +19 -0
  171. package/integrations/use-cases/load-integration-context.js +71 -0
  172. package/integrations/use-cases/update-integration-messages.js +44 -0
  173. package/integrations/use-cases/update-integration-status.js +32 -0
  174. package/integrations/use-cases/update-integration.js +93 -0
  175. package/integrations/use-cases/update-process-metrics.js +201 -0
  176. package/integrations/use-cases/update-process-state.js +119 -0
  177. package/integrations/utils/map-integration-dto.js +37 -0
  178. package/jest-global-setup-noop.js +3 -0
  179. package/jest-global-teardown-noop.js +3 -0
  180. package/logs/logger.js +0 -4
  181. package/{module-plugin → modules}/entity.js +1 -1
  182. package/{module-plugin → modules}/index.js +0 -8
  183. package/modules/module-factory.js +56 -0
  184. package/modules/module.js +221 -0
  185. package/modules/repositories/module-repository-documentdb.js +307 -0
  186. package/modules/repositories/module-repository-factory.js +40 -0
  187. package/modules/repositories/module-repository-interface.js +129 -0
  188. package/modules/repositories/module-repository-mongo.js +377 -0
  189. package/modules/repositories/module-repository-postgres.js +426 -0
  190. package/modules/repositories/module-repository.js +316 -0
  191. package/modules/requester/api-key.js +52 -0
  192. package/{module-plugin → modules}/requester/requester.js +1 -0
  193. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  194. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  195. package/modules/tests/doubles/test-module-factory.js +16 -0
  196. package/modules/tests/doubles/test-module-repository.js +39 -0
  197. package/modules/use-cases/get-entities-for-user.js +32 -0
  198. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  199. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  200. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  201. package/modules/use-cases/get-module.js +74 -0
  202. package/modules/use-cases/process-authorization-callback.js +133 -0
  203. package/modules/use-cases/refresh-entity-options.js +72 -0
  204. package/modules/use-cases/test-module-auth.js +72 -0
  205. package/modules/utils/map-module-dto.js +18 -0
  206. package/package.json +82 -50
  207. package/prisma-mongodb/schema.prisma +360 -0
  208. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  209. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  210. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  211. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  212. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  213. package/prisma-postgresql/schema.prisma +343 -0
  214. package/queues/queuer-util.js +27 -22
  215. package/syncs/manager.js +468 -443
  216. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  217. package/syncs/repositories/sync-repository-factory.js +43 -0
  218. package/syncs/repositories/sync-repository-interface.js +109 -0
  219. package/syncs/repositories/sync-repository-mongo.js +239 -0
  220. package/syncs/repositories/sync-repository-postgres.js +319 -0
  221. package/syncs/sync.js +0 -1
  222. package/token/repositories/token-repository-documentdb.js +137 -0
  223. package/token/repositories/token-repository-factory.js +40 -0
  224. package/token/repositories/token-repository-interface.js +131 -0
  225. package/token/repositories/token-repository-mongo.js +219 -0
  226. package/token/repositories/token-repository-postgres.js +264 -0
  227. package/token/repositories/token-repository.js +219 -0
  228. package/types/core/index.d.ts +2 -2
  229. package/types/integrations/index.d.ts +2 -6
  230. package/types/module-plugin/index.d.ts +5 -59
  231. package/types/syncs/index.d.ts +0 -2
  232. package/user/repositories/user-repository-documentdb.js +441 -0
  233. package/user/repositories/user-repository-factory.js +52 -0
  234. package/user/repositories/user-repository-interface.js +201 -0
  235. package/user/repositories/user-repository-mongo.js +308 -0
  236. package/user/repositories/user-repository-postgres.js +360 -0
  237. package/user/tests/doubles/test-user-repository.js +72 -0
  238. package/user/use-cases/authenticate-user.js +127 -0
  239. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  240. package/user/use-cases/create-individual-user.js +61 -0
  241. package/user/use-cases/create-organization-user.js +47 -0
  242. package/user/use-cases/create-token-for-user-id.js +30 -0
  243. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  244. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  245. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  246. package/user/use-cases/login-user.js +122 -0
  247. package/user/user.js +125 -0
  248. package/utils/backend-path.js +38 -0
  249. package/utils/index.js +6 -0
  250. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  251. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  252. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  253. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  254. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  255. package/websocket/repositories/websocket-connection-repository.js +161 -0
  256. package/database/models/State.js +0 -9
  257. package/database/models/Token.js +0 -70
  258. package/database/mongo.js +0 -45
  259. package/encrypt/Cryptor.test.js +0 -32
  260. package/encrypt/encrypt.js +0 -132
  261. package/encrypt/encrypt.test.js +0 -1069
  262. package/errors/base-error.test.js +0 -32
  263. package/errors/fetch-error.test.js +0 -79
  264. package/errors/halt-error.test.js +0 -11
  265. package/errors/validation-errors.test.js +0 -120
  266. package/integrations/create-frigg-backend.js +0 -31
  267. package/integrations/integration-factory.js +0 -251
  268. package/integrations/integration-mapping.js +0 -43
  269. package/integrations/integration-model.js +0 -46
  270. package/integrations/integration-user.js +0 -144
  271. package/integrations/test/integration-base.test.js +0 -144
  272. package/lambda/TimeoutCatcher.test.js +0 -68
  273. package/logs/logger.test.js +0 -76
  274. package/module-plugin/auther.js +0 -393
  275. package/module-plugin/credential.js +0 -22
  276. package/module-plugin/entity-manager.js +0 -70
  277. package/module-plugin/manager.js +0 -169
  278. package/module-plugin/module-factory.js +0 -61
  279. package/module-plugin/requester/api-key.js +0 -36
  280. package/module-plugin/requester/requester.test.js +0 -28
  281. package/module-plugin/test/auther.test.js +0 -97
  282. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  283. /package/{module-plugin → modules}/requester/basic.js +0 -0
  284. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  285. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,360 @@
1
+ const bcrypt = require('bcryptjs');
2
+ const { prisma } = require('../../database/prisma');
3
+ const {
4
+ createTokenRepository,
5
+ } = require('../../token/repositories/token-repository-factory');
6
+ const { UserRepositoryInterface } = require('./user-repository-interface');
7
+ const { ClientSafeError } = require('../../errors');
8
+
9
+ /**
10
+ * PostgreSQL User Repository Adapter
11
+ * Handles user operations with discriminator pattern support
12
+ *
13
+ * PostgreSQL-specific characteristics:
14
+ * - Uses Int IDs with autoincrement
15
+ * - Requires ID conversion: String (app layer) ↔ Int (database)
16
+ * - All returned IDs are converted to strings for application layer consistency
17
+ */
18
+ class UserRepositoryPostgres extends UserRepositoryInterface {
19
+ constructor() {
20
+ super();
21
+ this.prisma = prisma;
22
+ this.tokenRepository = createTokenRepository();
23
+ }
24
+
25
+ /**
26
+ * Convert string ID to integer for PostgreSQL queries
27
+ * @private
28
+ * @param {string|number|null|undefined} id - ID to convert
29
+ * @returns {number|null|undefined} Integer ID or null/undefined
30
+ * @throws {Error} If ID cannot be converted to integer
31
+ */
32
+ _convertId(id) {
33
+ if (id === null || id === undefined) return id;
34
+ const parsed = parseInt(id, 10);
35
+ if (isNaN(parsed)) {
36
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
37
+ }
38
+ return parsed;
39
+ }
40
+
41
+ /**
42
+ * Convert user object IDs to strings
43
+ * @private
44
+ * @param {Object|null} user - User object from database
45
+ * @returns {Object|null} User with string IDs
46
+ */
47
+ _convertUserIds(user) {
48
+ if (!user) return user;
49
+ return {
50
+ ...user,
51
+ id: user.id?.toString(),
52
+ organizationId: user.organizationId?.toString(),
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Get session token from base64 buffer token
58
+ * Delegates to TokenRepository
59
+ *
60
+ * @param {string} token - Base64 buffer token
61
+ * @returns {Promise<Object>} Session token object with string IDs
62
+ */
63
+ async getSessionToken(token) {
64
+ const jsonToken =
65
+ this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
66
+ const sessionToken = await this.tokenRepository.validateAndGetToken(
67
+ jsonToken
68
+ );
69
+ return sessionToken;
70
+ }
71
+
72
+ /**
73
+ * Find organization user by ID
74
+ * Replaces: OrganizationUser.findById(userId)
75
+ *
76
+ * @param {string} userId - User ID (string from application layer)
77
+ * @returns {Promise<Object|null>} User object with string IDs or null
78
+ */
79
+ async findOrganizationUserById(userId) {
80
+ const intId = this._convertId(userId);
81
+ const user = await this.prisma.user.findFirst({
82
+ where: {
83
+ id: intId,
84
+ type: 'ORGANIZATION',
85
+ },
86
+ });
87
+ return this._convertUserIds(user);
88
+ }
89
+
90
+ /**
91
+ * Find individual user by ID
92
+ * Replaces: IndividualUser.findById(userId)
93
+ *
94
+ * @param {string} userId - User ID (string from application layer)
95
+ * @returns {Promise<Object|null>} User object with string IDs or null
96
+ */
97
+ async findIndividualUserById(userId) {
98
+ const intId = this._convertId(userId);
99
+ const user = await this.prisma.user.findFirst({
100
+ where: {
101
+ id: intId,
102
+ type: 'INDIVIDUAL',
103
+ },
104
+ });
105
+ return this._convertUserIds(user);
106
+ }
107
+
108
+ /**
109
+ * Create token with expiration
110
+ * Delegates to TokenRepository
111
+ *
112
+ * @param {string} userId - User ID (string from application layer)
113
+ * @param {string} rawToken - Raw unhashed token
114
+ * @param {number} minutes - Minutes until expiration (default 120)
115
+ * @returns {Promise<string>} Base64 buffer token
116
+ */
117
+ async createToken(userId, rawToken, minutes = 120) {
118
+ const createdToken = await this.tokenRepository.createTokenWithExpire(
119
+ userId,
120
+ rawToken,
121
+ minutes
122
+ );
123
+ return this.tokenRepository.createBase64BufferToken(
124
+ createdToken,
125
+ rawToken
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Create individual user
131
+ * Replaces: IndividualUser.create(params)
132
+ *
133
+ * @param {Object} params - User creation parameters (with string IDs from application layer)
134
+ * @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
135
+ * @returns {Promise<Object>} Created user object with string IDs
136
+ */
137
+ async createIndividualUser(params) {
138
+ const data = {
139
+ type: 'INDIVIDUAL',
140
+ email: params.email,
141
+ username: params.username,
142
+ appUserId: params.appUserId,
143
+ organizationId: this._convertId(
144
+ params.organization || params.organizationId
145
+ ),
146
+ };
147
+
148
+ if (
149
+ params.hashword !== undefined &&
150
+ params.hashword !== null &&
151
+ params.hashword !== ''
152
+ ) {
153
+ if (typeof params.hashword !== 'string') {
154
+ throw new ClientSafeError('Password must be a string', 400);
155
+ }
156
+
157
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
158
+ if (params.hashword.startsWith('$2')) {
159
+ throw new Error(
160
+ 'Password appears to be already hashed. Pass plain text password only.'
161
+ );
162
+ }
163
+
164
+ data.hashword = await bcrypt.hash(params.hashword, 10);
165
+ }
166
+
167
+ const user = await this.prisma.user.create({ data });
168
+ return this._convertUserIds(user);
169
+ }
170
+
171
+ /**
172
+ * Create organization user
173
+ * Replaces: OrganizationUser.create(params)
174
+ *
175
+ * @param {Object} params - Organization creation parameters
176
+ * @returns {Promise<Object>} Created organization object with string IDs
177
+ */
178
+ async createOrganizationUser(params) {
179
+ const user = await this.prisma.user.create({
180
+ data: {
181
+ type: 'ORGANIZATION',
182
+ appOrgId: params.appOrgId,
183
+ name: params.name,
184
+ },
185
+ });
186
+ return this._convertUserIds(user);
187
+ }
188
+
189
+ /**
190
+ * Find individual user by username
191
+ * Replaces: IndividualUser.findOne({ username })
192
+ *
193
+ * @param {string} username - Username to search for
194
+ * @returns {Promise<Object|null>} User object with string IDs or null
195
+ */
196
+ async findIndividualUserByUsername(username) {
197
+ const user = await this.prisma.user.findFirst({
198
+ where: {
199
+ type: 'INDIVIDUAL',
200
+ username,
201
+ },
202
+ });
203
+ return this._convertUserIds(user);
204
+ }
205
+
206
+ /**
207
+ * Find individual user by app user ID
208
+ * Replaces: IndividualUser.getUserByAppUserId(appUserId)
209
+ *
210
+ * @param {string} appUserId - App user ID to search for
211
+ * @returns {Promise<Object|null>} User object with string IDs or null
212
+ */
213
+ async findIndividualUserByAppUserId(appUserId) {
214
+ const user = await this.prisma.user.findFirst({
215
+ where: {
216
+ type: 'INDIVIDUAL',
217
+ appUserId,
218
+ },
219
+ });
220
+ return this._convertUserIds(user);
221
+ }
222
+
223
+ /**
224
+ * Find organization user by app org ID
225
+ * Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
226
+ *
227
+ * @param {string} appOrgId - App organization ID to search for
228
+ * @returns {Promise<Object|null>} User object with string IDs or null
229
+ */
230
+ async findOrganizationUserByAppOrgId(appOrgId) {
231
+ const user = await this.prisma.user.findFirst({
232
+ where: {
233
+ type: 'ORGANIZATION',
234
+ appOrgId,
235
+ },
236
+ });
237
+ return this._convertUserIds(user);
238
+ }
239
+
240
+ /**
241
+ * Find individual user by email
242
+ * @param {string} email - Email to search for
243
+ * @returns {Promise<Object|null>} User object with string IDs or null
244
+ */
245
+ async findIndividualUserByEmail(email) {
246
+ const user = await this.prisma.user.findFirst({
247
+ where: {
248
+ type: 'INDIVIDUAL',
249
+ email,
250
+ },
251
+ });
252
+ return this._convertUserIds(user);
253
+ }
254
+
255
+ /**
256
+ * Update individual user
257
+ * @param {string} userId - User ID (string from application layer)
258
+ * @param {Object} updates - Fields to update (with string IDs from application layer)
259
+ * @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
260
+ * @returns {Promise<Object>} Updated user object with string IDs
261
+ */
262
+ async updateIndividualUser(userId, updates) {
263
+ const intId = this._convertId(userId);
264
+
265
+ const data = { ...updates };
266
+
267
+ if (data.organizationId !== undefined) {
268
+ data.organizationId = this._convertId(data.organizationId);
269
+ }
270
+ if (data.organization !== undefined) {
271
+ data.organizationId = this._convertId(data.organization);
272
+ delete data.organization;
273
+ }
274
+
275
+ if (
276
+ data.hashword !== undefined &&
277
+ data.hashword !== null &&
278
+ data.hashword !== ''
279
+ ) {
280
+ if (typeof data.hashword !== 'string') {
281
+ throw new ClientSafeError('Password must be a string', 400);
282
+ }
283
+
284
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
285
+ if (data.hashword.startsWith('$2')) {
286
+ throw new Error(
287
+ 'Password appears to be already hashed. Pass plain text password only.'
288
+ );
289
+ }
290
+
291
+ data.hashword = await bcrypt.hash(data.hashword, 10);
292
+ }
293
+
294
+ const user = await this.prisma.user.update({
295
+ where: { id: intId },
296
+ data,
297
+ });
298
+ return this._convertUserIds(user);
299
+ }
300
+
301
+ /**
302
+ * Update organization user
303
+ * @param {string} userId - User ID (string from application layer)
304
+ * @param {Object} updates - Fields to update
305
+ * @returns {Promise<Object>} Updated user object with string IDs
306
+ */
307
+ async updateOrganizationUser(userId, updates) {
308
+ const intId = this._convertId(userId);
309
+ const user = await this.prisma.user.update({
310
+ where: { id: intId },
311
+ data: updates,
312
+ });
313
+ return this._convertUserIds(user);
314
+ }
315
+
316
+ /**
317
+ * Delete user by ID
318
+ * @param {string} userId - User ID to delete (string from application layer)
319
+ * @returns {Promise<boolean>} True if deleted successfully
320
+ */
321
+ async deleteUser(userId) {
322
+ try {
323
+ const intId = this._convertId(userId);
324
+ await this.prisma.user.delete({
325
+ where: { id: intId },
326
+ });
327
+ return true;
328
+ } catch (error) {
329
+ if (error.code === 'P2025') {
330
+ // Record not found
331
+ return false;
332
+ }
333
+ throw error;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Link an individual user to an organization user
339
+ * @param {string} individualUserId - Individual user ID (string from application layer)
340
+ * @param {string} organizationUserId - Organization user ID (string from application layer)
341
+ * @returns {Promise<Object>} Updated individual user with string IDs
342
+ */
343
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
344
+ const intIndividualId = this._convertId(individualUserId);
345
+ const intOrganizationId = this._convertId(organizationUserId);
346
+
347
+ const user = await this.prisma.user.update({
348
+ where: {
349
+ id: intIndividualId,
350
+ type: 'INDIVIDUAL',
351
+ },
352
+ data: {
353
+ organizationId: intOrganizationId,
354
+ },
355
+ });
356
+ return this._convertUserIds(user);
357
+ }
358
+ }
359
+
360
+ module.exports = { UserRepositoryPostgres };
@@ -0,0 +1,72 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { User } = require('../../user');
3
+
4
+ class TestUserRepository {
5
+ constructor({ userConfig }) {
6
+ this.individualUsers = new Map();
7
+ this.organizationUsers = new Map();
8
+ this.tokens = new Map();
9
+ this.userConfig = userConfig;
10
+ }
11
+
12
+ async getSessionToken(token) {
13
+ return this.tokens.get(token);
14
+ }
15
+
16
+ async findOrganizationUserById(userId) {
17
+ return this.organizationUsers.get(userId);
18
+ }
19
+
20
+ async findIndividualUserById(userId) {
21
+ return this.individualUsers.get(userId);
22
+ }
23
+
24
+ async createToken(userId, rawToken, minutes = 120) {
25
+ const token = `token-for-${userId}-for-${minutes}-mins`;
26
+ this.tokens.set(token, { user: userId, rawToken });
27
+ return token;
28
+ }
29
+
30
+ async createIndividualUser(params) {
31
+ const individualUserData = { id: `individual-${Date.now()}`, ...params };
32
+ this.individualUsers.set(individualUserData.id, individualUserData);
33
+ return individualUserData;
34
+ }
35
+
36
+ async createOrganizationUser(params) {
37
+ const orgUserData = { ...params, id: `org-${Date.now()}` };
38
+ this.organizationUsers.set(orgUserData.id, orgUserData);
39
+ return orgUserData;
40
+ }
41
+
42
+ async findIndividualUserByUsername(username) {
43
+ for (const userDoc of this.individualUsers.values()) {
44
+ if (userDoc.username === username) {
45
+ return userDoc;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ async findIndividualUserByAppUserId(appUserId) {
52
+ if (!appUserId) return null;
53
+ for (const userDoc of this.individualUsers.values()) {
54
+ if (userDoc.appUserId === appUserId) {
55
+ return userDoc;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+
61
+ async findOrganizationUserByAppOrgId(appOrgId) {
62
+ if (!appOrgId) return null;
63
+ for (const userDoc of this.organizationUsers.values()) {
64
+ if (userDoc.appOrgId === appOrgId) {
65
+ return userDoc;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ }
71
+
72
+ module.exports = { TestUserRepository };
@@ -0,0 +1,127 @@
1
+ const Boom = require('@hapi/boom');
2
+
3
+ /**
4
+ * Use case for authenticating a user using multiple authentication strategies.
5
+ *
6
+ * Supports three authentication modes in priority order:
7
+ * 1. Shared Secret (backend-to-backend with x-frigg-api-key + x-frigg headers)
8
+ * 2. Adopter JWT (custom JWT authentication)
9
+ * 3. Frigg Native Token (bearer token from /user/login)
10
+ *
11
+ * x-frigg-appUserId and x-frigg-appOrgId headers are automatically supported
12
+ * for user identification with any auth mode. When present with JWT or Frigg
13
+ * tokens, they are validated to match the authenticated user.
14
+ *
15
+ * @class AuthenticateUser
16
+ */
17
+ class AuthenticateUser {
18
+ /**
19
+ * Creates a new AuthenticateUser instance.
20
+ * @param {Object} params - Configuration parameters.
21
+ * @param {import('./get-user-from-bearer-token').GetUserFromBearerToken} params.getUserFromBearerToken - Use case for bearer token auth.
22
+ * @param {import('./get-user-from-x-frigg-headers').GetUserFromXFriggHeaders} params.getUserFromXFriggHeaders - Use case for x-frigg header auth.
23
+ * @param {import('./get-user-from-adopter-jwt').GetUserFromAdopterJwt} params.getUserFromAdopterJwt - Use case for adopter JWT auth.
24
+ * @param {import('./authenticate-with-shared-secret').AuthenticateWithSharedSecret} params.authenticateWithSharedSecret - Use case for validating shared secret.
25
+ * @param {Object} params.userConfig - The user config in the app definition.
26
+ */
27
+ constructor({
28
+ getUserFromBearerToken,
29
+ getUserFromXFriggHeaders,
30
+ getUserFromAdopterJwt,
31
+ authenticateWithSharedSecret,
32
+ userConfig,
33
+ }) {
34
+ this.getUserFromBearerToken = getUserFromBearerToken;
35
+ this.getUserFromXFriggHeaders = getUserFromXFriggHeaders;
36
+ this.getUserFromAdopterJwt = getUserFromAdopterJwt;
37
+ this.authenticateWithSharedSecret = authenticateWithSharedSecret;
38
+ this.userConfig = userConfig;
39
+ }
40
+
41
+ /**
42
+ * Executes the use case.
43
+ * @async
44
+ * @param {Object} req - Express request object with headers.
45
+ * @returns {Promise<import('../user').User>} The authenticated user object.
46
+ * @throws {Boom} Unauthorized if no valid authentication provided.
47
+ * @throws {Boom} Forbidden if x-frigg headers don't match authenticated user.
48
+ */
49
+ async execute(req) {
50
+ const authModes = this.userConfig.authModes || { friggToken: true };
51
+ const appUserId = req.headers['x-frigg-appuserid'];
52
+ const appOrgId = req.headers['x-frigg-apporgid'];
53
+ let user = null;
54
+
55
+ // Priority 1: Shared Secret (backend-to-backend with API key)
56
+ if (authModes.sharedSecret !== false) {
57
+ const apiKey = req.headers['x-frigg-api-key'];
58
+ if (apiKey) {
59
+ // Validate the API key (authentication)
60
+ await this.authenticateWithSharedSecret.execute(apiKey);
61
+ // Get user from x-frigg headers (authorization)
62
+ return await this.getUserFromXFriggHeaders.execute(
63
+ appUserId,
64
+ appOrgId
65
+ );
66
+ }
67
+ }
68
+
69
+ // Priority 2: Adopter JWT (if enabled)
70
+ if (
71
+ authModes.adopterJwt === true &&
72
+ req.headers.authorization?.startsWith('Bearer ')
73
+ ) {
74
+ const token = req.headers.authorization.split(' ')[1];
75
+ // Detect JWT format (3 parts separated by dots)
76
+ if (token && token.split('.').length === 3) {
77
+ user = await this.getUserFromAdopterJwt.execute(token);
78
+ // Validate x-frigg headers match JWT claims if present
79
+ if (appUserId || appOrgId) {
80
+ this.validateUserMatch(user, appUserId, appOrgId);
81
+ }
82
+ return user;
83
+ }
84
+ }
85
+
86
+ // Priority 3: Frigg native token (default)
87
+ if (authModes.friggToken !== false && req.headers.authorization) {
88
+ user = await this.getUserFromBearerToken.execute(
89
+ req.headers.authorization
90
+ );
91
+ // Validate x-frigg headers match token user if present
92
+ if (appUserId || appOrgId) {
93
+ this.validateUserMatch(user, appUserId, appOrgId);
94
+ }
95
+ return user;
96
+ }
97
+
98
+ throw Boom.unauthorized('No valid authentication provided');
99
+ }
100
+
101
+ /**
102
+ * Validates that x-frigg headers match authenticated user if provided.
103
+ * This ensures that when both authentication (via token/JWT) and
104
+ * x-frigg headers are present, they refer to the same user.
105
+ *
106
+ * @param {import('../user').User} user - The authenticated user
107
+ * @param {string} [appUserId] - The x-frigg-appuserid header value
108
+ * @param {string} [appOrgId] - The x-frigg-apporgid header value
109
+ * @throws {Boom} 403 Forbidden if headers don't match user
110
+ */
111
+ validateUserMatch(user, appUserId, appOrgId) {
112
+ if (appUserId && user.getAppUserId() !== appUserId) {
113
+ throw Boom.forbidden(
114
+ 'x-frigg-appuserid header does not match authenticated user'
115
+ );
116
+ }
117
+ if (appOrgId && user.getAppOrgId() !== appOrgId) {
118
+ throw Boom.forbidden(
119
+ 'x-frigg-apporgid header does not match authenticated user'
120
+ );
121
+ }
122
+ }
123
+ }
124
+
125
+ module.exports = { AuthenticateUser };
126
+
127
+
@@ -0,0 +1,48 @@
1
+ const Boom = require('@hapi/boom');
2
+
3
+ /**
4
+ * Use case for authenticating requests with shared secret API key.
5
+ * This use case ONLY validates the authenticity of the request via API key.
6
+ * It does NOT retrieve user data - that's handled by GetUserFromXFriggHeaders.
7
+ *
8
+ * Used for backend-to-backend communication where the secret proves
9
+ * the request is legitimate, but user identification comes from x-frigg headers.
10
+ *
11
+ * @class AuthenticateWithSharedSecret
12
+ */
13
+ class AuthenticateWithSharedSecret {
14
+ /**
15
+ * Creates a new AuthenticateWithSharedSecret instance.
16
+ * @param {Object} params - Configuration parameters (none needed currently, but kept for consistency).
17
+ */
18
+ constructor() {
19
+ // No dependencies needed - just validates against env var
20
+ }
21
+
22
+ /**
23
+ * Validates the provided shared secret against FRIGG_API_KEY.
24
+ * @async
25
+ * @param {string} providedSecret - Secret from x-frigg-api-key header
26
+ * @returns {Promise<boolean>} True if valid (or throws error if invalid)
27
+ * @throws {Boom} 500 if FRIGG_API_KEY not configured
28
+ * @throws {Boom} 401 if provided secret doesn't match
29
+ */
30
+ async execute(providedSecret) {
31
+ // Validate secret
32
+ const expectedSecret = process.env.FRIGG_API_KEY;
33
+ if (!expectedSecret) {
34
+ throw Boom.badImplementation(
35
+ 'FRIGG_API_KEY environment variable is not configured. ' +
36
+ 'Set FRIGG_API_KEY to enable shared secret authentication.'
37
+ );
38
+ }
39
+
40
+ if (!providedSecret || providedSecret !== expectedSecret) {
41
+ throw Boom.unauthorized('Invalid API key');
42
+ }
43
+
44
+ return true;
45
+ }
46
+ }
47
+
48
+ module.exports = { AuthenticateWithSharedSecret };
@@ -0,0 +1,61 @@
1
+ const { get } = require('../../assertions');
2
+ const Boom = require('@hapi/boom');
3
+ const { User } = require('../user');
4
+
5
+ /**
6
+ * Use case for creating an individual user.
7
+ * @class CreateIndividualUser
8
+ */
9
+ class CreateIndividualUser {
10
+ /**
11
+ * Creates a new CreateIndividualUser instance.
12
+ * @param {Object} params - Configuration parameters.
13
+ * @param {import('../user-repository-interface').UserRepositoryInterface} params.userRepository - Repository for user data operations.
14
+ * @param {Object} params.userConfig - The user properties inside of the app definition.
15
+ */
16
+ constructor({ userRepository, userConfig }) {
17
+ this.userRepository = userRepository;
18
+ this.userConfig = userConfig;
19
+ }
20
+
21
+ /**
22
+ * Executes the use case.
23
+ * @async
24
+ * @param {Object} params - The parameters for creating the user.
25
+ * @returns {Promise<import('../user').User>} The newly created user object.
26
+ */
27
+ async execute(params) {
28
+ let hashword;
29
+ if (this.userConfig.usePassword) {
30
+ hashword = get(params, 'password');
31
+ }
32
+
33
+ const email = get(params, 'email', null);
34
+ const username = get(params, 'username', null);
35
+ if (!email && !username) {
36
+ throw Boom.badRequest('email or username is required');
37
+ }
38
+
39
+ const appUserId = get(params, 'appUserId', null);
40
+ const organizationUserId = get(params, 'organizationUserId', null);
41
+
42
+ const individualUserData = await this.userRepository.createIndividualUser({
43
+ email,
44
+ username,
45
+ hashword,
46
+ appUserId,
47
+ organizationUser: organizationUserId,
48
+ });
49
+
50
+ return new User(
51
+ individualUserData,
52
+ null,
53
+ this.userConfig.usePassword,
54
+ this.userConfig.primary,
55
+ this.userConfig.individualUserRequired,
56
+ this.userConfig.organizationUserRequired
57
+ );
58
+ }
59
+ }
60
+
61
+ module.exports = { CreateIndividualUser };