@friggframework/core 2.0.0-next.5 → 2.0.0-next.50

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 (267) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +959 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +179 -0
  7. package/application/commands/user-commands.js +213 -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 +2 -7
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +307 -0
  15. package/credential/repositories/credential-repository-postgres.js +313 -0
  16. package/credential/repositories/credential-repository.js +302 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  20. package/database/adapters/lambda-invoker.js +97 -0
  21. package/database/config.js +154 -0
  22. package/database/encryption/README.md +684 -0
  23. package/database/encryption/encryption-schema-registry.js +141 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/logger.js +79 -0
  26. package/database/encryption/prisma-encryption-extension.js +222 -0
  27. package/database/index.js +25 -12
  28. package/database/models/WebsocketConnection.js +16 -10
  29. package/database/models/readme.md +1 -0
  30. package/database/prisma.js +222 -0
  31. package/database/repositories/health-check-repository-factory.js +43 -0
  32. package/database/repositories/health-check-repository-interface.js +87 -0
  33. package/database/repositories/health-check-repository-mongodb.js +91 -0
  34. package/database/repositories/health-check-repository-postgres.js +82 -0
  35. package/database/repositories/health-check-repository.js +108 -0
  36. package/database/repositories/migration-status-repository-s3.js +137 -0
  37. package/database/use-cases/check-database-health-use-case.js +29 -0
  38. package/database/use-cases/check-database-state-use-case.js +81 -0
  39. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  40. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  41. package/database/use-cases/get-migration-status-use-case.js +93 -0
  42. package/database/use-cases/run-database-migration-use-case.js +137 -0
  43. package/database/use-cases/test-encryption-use-case.js +253 -0
  44. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  45. package/database/utils/mongodb-collection-utils.js +91 -0
  46. package/database/utils/mongodb-schema-init.js +106 -0
  47. package/database/utils/prisma-runner.js +400 -0
  48. package/database/utils/prisma-schema-parser.js +182 -0
  49. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  50. package/encrypt/Cryptor.js +34 -168
  51. package/encrypt/index.js +1 -2
  52. package/encrypt/test-encrypt.js +0 -2
  53. package/generated/prisma-mongodb/client.d.ts +1 -0
  54. package/generated/prisma-mongodb/client.js +4 -0
  55. package/generated/prisma-mongodb/default.d.ts +1 -0
  56. package/generated/prisma-mongodb/default.js +4 -0
  57. package/generated/prisma-mongodb/edge.d.ts +1 -0
  58. package/generated/prisma-mongodb/edge.js +334 -0
  59. package/generated/prisma-mongodb/index-browser.js +316 -0
  60. package/generated/prisma-mongodb/index.d.ts +22898 -0
  61. package/generated/prisma-mongodb/index.js +359 -0
  62. package/generated/prisma-mongodb/package.json +183 -0
  63. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  64. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  65. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  66. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  67. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  68. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  69. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  70. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  71. package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
  72. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  73. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  74. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  75. package/generated/prisma-mongodb/schema.prisma +362 -0
  76. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  77. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  78. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  79. package/generated/prisma-mongodb/wasm.js +341 -0
  80. package/generated/prisma-postgresql/client.d.ts +1 -0
  81. package/generated/prisma-postgresql/client.js +4 -0
  82. package/generated/prisma-postgresql/default.d.ts +1 -0
  83. package/generated/prisma-postgresql/default.js +4 -0
  84. package/generated/prisma-postgresql/edge.d.ts +1 -0
  85. package/generated/prisma-postgresql/edge.js +356 -0
  86. package/generated/prisma-postgresql/index-browser.js +338 -0
  87. package/generated/prisma-postgresql/index.d.ts +25072 -0
  88. package/generated/prisma-postgresql/index.js +381 -0
  89. package/generated/prisma-postgresql/package.json +183 -0
  90. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  91. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  92. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  93. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  94. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  95. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  96. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  97. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  98. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  99. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  100. package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
  101. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  102. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  103. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  104. package/generated/prisma-postgresql/schema.prisma +345 -0
  105. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  106. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  107. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  108. package/generated/prisma-postgresql/wasm.js +363 -0
  109. package/handlers/WEBHOOKS.md +653 -0
  110. package/handlers/app-definition-loader.js +38 -0
  111. package/handlers/app-handler-helpers.js +56 -0
  112. package/handlers/backend-utils.js +180 -0
  113. package/handlers/database-migration-handler.js +227 -0
  114. package/handlers/integration-event-dispatcher.js +54 -0
  115. package/handlers/routers/HEALTHCHECK.md +342 -0
  116. package/handlers/routers/auth.js +15 -0
  117. package/handlers/routers/db-migration.handler.js +29 -0
  118. package/handlers/routers/db-migration.js +256 -0
  119. package/handlers/routers/health.js +519 -0
  120. package/handlers/routers/integration-defined-routers.js +45 -0
  121. package/handlers/routers/integration-webhook-routers.js +67 -0
  122. package/handlers/routers/user.js +63 -0
  123. package/handlers/routers/websocket.js +57 -0
  124. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  125. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  126. package/handlers/workers/db-migration.js +352 -0
  127. package/handlers/workers/integration-defined-workers.js +27 -0
  128. package/index.js +77 -22
  129. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  130. package/integrations/index.js +12 -10
  131. package/integrations/integration-base.js +296 -54
  132. package/integrations/integration-router.js +381 -182
  133. package/integrations/options.js +1 -1
  134. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  135. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  136. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  137. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  138. package/integrations/repositories/integration-mapping-repository.js +156 -0
  139. package/integrations/repositories/integration-repository-factory.js +44 -0
  140. package/integrations/repositories/integration-repository-interface.js +127 -0
  141. package/integrations/repositories/integration-repository-mongo.js +303 -0
  142. package/integrations/repositories/integration-repository-postgres.js +352 -0
  143. package/integrations/repositories/process-repository-factory.js +46 -0
  144. package/integrations/repositories/process-repository-interface.js +90 -0
  145. package/integrations/repositories/process-repository-mongo.js +190 -0
  146. package/integrations/repositories/process-repository-postgres.js +217 -0
  147. package/integrations/tests/doubles/dummy-integration-class.js +83 -0
  148. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  149. package/integrations/use-cases/create-integration.js +83 -0
  150. package/integrations/use-cases/create-process.js +128 -0
  151. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  152. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  153. package/integrations/use-cases/get-integration-for-user.js +78 -0
  154. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  155. package/integrations/use-cases/get-integration-instance.js +83 -0
  156. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  157. package/integrations/use-cases/get-possible-integrations.js +27 -0
  158. package/integrations/use-cases/get-process.js +87 -0
  159. package/integrations/use-cases/index.js +19 -0
  160. package/integrations/use-cases/load-integration-context.js +71 -0
  161. package/integrations/use-cases/update-integration-messages.js +44 -0
  162. package/integrations/use-cases/update-integration-status.js +32 -0
  163. package/integrations/use-cases/update-integration.js +93 -0
  164. package/integrations/use-cases/update-process-metrics.js +201 -0
  165. package/integrations/use-cases/update-process-state.js +119 -0
  166. package/integrations/utils/map-integration-dto.js +36 -0
  167. package/jest-global-setup-noop.js +3 -0
  168. package/jest-global-teardown-noop.js +3 -0
  169. package/logs/logger.js +0 -4
  170. package/{module-plugin → modules}/entity.js +1 -1
  171. package/{module-plugin → modules}/index.js +0 -8
  172. package/modules/module-factory.js +56 -0
  173. package/modules/module.js +221 -0
  174. package/modules/repositories/module-repository-factory.js +33 -0
  175. package/modules/repositories/module-repository-interface.js +129 -0
  176. package/modules/repositories/module-repository-mongo.js +377 -0
  177. package/modules/repositories/module-repository-postgres.js +426 -0
  178. package/modules/repositories/module-repository.js +316 -0
  179. package/{module-plugin → modules}/requester/requester.js +1 -0
  180. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  181. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  182. package/modules/tests/doubles/test-module-factory.js +16 -0
  183. package/modules/tests/doubles/test-module-repository.js +39 -0
  184. package/modules/use-cases/get-entities-for-user.js +32 -0
  185. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  186. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  187. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  188. package/modules/use-cases/get-module.js +55 -0
  189. package/modules/use-cases/process-authorization-callback.js +122 -0
  190. package/modules/use-cases/refresh-entity-options.js +59 -0
  191. package/modules/use-cases/test-module-auth.js +55 -0
  192. package/modules/utils/map-module-dto.js +18 -0
  193. package/package.json +82 -50
  194. package/prisma-mongodb/schema.prisma +362 -0
  195. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  196. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  197. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  198. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  199. package/prisma-postgresql/schema.prisma +345 -0
  200. package/queues/queuer-util.js +28 -15
  201. package/syncs/manager.js +468 -443
  202. package/syncs/repositories/sync-repository-factory.js +38 -0
  203. package/syncs/repositories/sync-repository-interface.js +109 -0
  204. package/syncs/repositories/sync-repository-mongo.js +239 -0
  205. package/syncs/repositories/sync-repository-postgres.js +319 -0
  206. package/syncs/sync.js +0 -1
  207. package/token/repositories/token-repository-factory.js +33 -0
  208. package/token/repositories/token-repository-interface.js +131 -0
  209. package/token/repositories/token-repository-mongo.js +212 -0
  210. package/token/repositories/token-repository-postgres.js +257 -0
  211. package/token/repositories/token-repository.js +219 -0
  212. package/types/core/index.d.ts +2 -2
  213. package/types/integrations/index.d.ts +2 -6
  214. package/types/module-plugin/index.d.ts +5 -59
  215. package/types/syncs/index.d.ts +0 -2
  216. package/user/repositories/user-repository-factory.js +46 -0
  217. package/user/repositories/user-repository-interface.js +198 -0
  218. package/user/repositories/user-repository-mongo.js +291 -0
  219. package/user/repositories/user-repository-postgres.js +350 -0
  220. package/user/tests/doubles/test-user-repository.js +72 -0
  221. package/user/use-cases/authenticate-user.js +127 -0
  222. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  223. package/user/use-cases/create-individual-user.js +61 -0
  224. package/user/use-cases/create-organization-user.js +47 -0
  225. package/user/use-cases/create-token-for-user-id.js +30 -0
  226. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  227. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  228. package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
  229. package/user/use-cases/login-user.js +122 -0
  230. package/user/user.js +93 -0
  231. package/utils/backend-path.js +38 -0
  232. package/utils/index.js +6 -0
  233. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  234. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  235. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  236. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  237. package/websocket/repositories/websocket-connection-repository.js +161 -0
  238. package/database/models/State.js +0 -9
  239. package/database/models/Token.js +0 -70
  240. package/database/mongo.js +0 -45
  241. package/encrypt/Cryptor.test.js +0 -32
  242. package/encrypt/encrypt.js +0 -132
  243. package/encrypt/encrypt.test.js +0 -1069
  244. package/errors/base-error.test.js +0 -32
  245. package/errors/fetch-error.test.js +0 -79
  246. package/errors/halt-error.test.js +0 -11
  247. package/errors/validation-errors.test.js +0 -120
  248. package/integrations/create-frigg-backend.js +0 -31
  249. package/integrations/integration-factory.js +0 -251
  250. package/integrations/integration-mapping.js +0 -43
  251. package/integrations/integration-model.js +0 -46
  252. package/integrations/integration-user.js +0 -144
  253. package/integrations/test/integration-base.test.js +0 -144
  254. package/lambda/TimeoutCatcher.test.js +0 -68
  255. package/logs/logger.test.js +0 -76
  256. package/module-plugin/auther.js +0 -393
  257. package/module-plugin/credential.js +0 -22
  258. package/module-plugin/entity-manager.js +0 -70
  259. package/module-plugin/manager.js +0 -169
  260. package/module-plugin/module-factory.js +0 -61
  261. package/module-plugin/requester/requester.test.js +0 -28
  262. package/module-plugin/test/auther.test.js +0 -97
  263. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  264. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  265. /package/{module-plugin → modules}/requester/basic.js +0 -0
  266. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  267. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -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 };
@@ -0,0 +1,47 @@
1
+ const { get } = require('../../assertions');
2
+ const { User } = require('../user');
3
+
4
+ /**
5
+ * Use case for creating an organization user.
6
+ * @class CreateOrganizationUser
7
+ */
8
+ class CreateOrganizationUser {
9
+ /**
10
+ * Creates a new CreateOrganizationUser instance.
11
+ * @param {Object} params - Configuration parameters.
12
+ * @param {import('../user-repository-interface').UserRepositoryInterface} params.userRepository - Repository for user data operations.
13
+ * @param {Object} params.userConfig - The user properties inside of the app definition.
14
+ */
15
+ constructor({ userRepository, userConfig }) {
16
+ this.userRepository = userRepository;
17
+ this.userConfig = userConfig;
18
+ }
19
+
20
+ /**
21
+ * Executes the use case.
22
+ * @async
23
+ * @param {Object} params - The parameters for creating the user.
24
+ * @returns {Promise<import('../user').User>} The newly created user object.
25
+ */
26
+ async execute(params) {
27
+ const name = get(params, 'name');
28
+ const appOrgId = get(params, 'appOrgId');
29
+
30
+ const organizationUserData =
31
+ await this.userRepository.createOrganizationUser({
32
+ name,
33
+ appOrgId,
34
+ });
35
+
36
+ return new User(
37
+ null,
38
+ organizationUserData,
39
+ this.userConfig.usePassword,
40
+ this.userConfig.primary,
41
+ this.userConfig.individualUserRequired,
42
+ this.userConfig.organizationUserRequired
43
+ );
44
+ }
45
+ }
46
+
47
+ module.exports = { CreateOrganizationUser };
@@ -0,0 +1,30 @@
1
+ const crypto = require('crypto');
2
+
3
+ /**
4
+ * Use case for creating a token for a user ID.
5
+ * @class CreateTokenForUserId
6
+ */
7
+ class CreateTokenForUserId {
8
+ /**
9
+ * Creates a new CreateTokenForUserId instance.
10
+ * @param {Object} params - Configuration parameters.
11
+ * @param {import('../user-repository-interface').UserRepositoryInterface} params.userRepository - Repository for user data operations.
12
+ */
13
+ constructor({ userRepository }) {
14
+ this.userRepository = userRepository;
15
+ }
16
+
17
+ /**
18
+ * Executes the use case.
19
+ * @async
20
+ * @param {string} userId - The ID of the user to create a token for.
21
+ * @param {number} minutes - The number of minutes until the token expires.
22
+ * @returns {Promise<string>} The user token.
23
+ */
24
+ async execute(userId, minutes) {
25
+ const rawToken = crypto.randomBytes(20).toString('hex');
26
+ return this.userRepository.createToken(userId, rawToken, minutes);
27
+ }
28
+ }
29
+
30
+ module.exports = { CreateTokenForUserId };
@@ -0,0 +1,149 @@
1
+ const Boom = require('@hapi/boom');
2
+
3
+ /**
4
+ * STUB: Use case for retrieving a user from adopter-provided JWT token.
5
+ *
6
+ * This is a stub implementation for future JWT authentication support.
7
+ * When implemented, this will allow adopters to use their own JWT tokens
8
+ * for authentication instead of Frigg's native token system.
9
+ *
10
+ * FUTURE IMPLEMENTATION REQUIREMENTS:
11
+ * - Validate JWT signature using jwtConfig.secret from app definition
12
+ * - Support configurable signing algorithms (HS256, HS384, HS512, RS256, RS384, RS512)
13
+ * - Extract user identifiers from JWT claims based on jwtConfig.userIdClaim and jwtConfig.orgIdClaim
14
+ * - Find or create user based on extracted claim values
15
+ * - Handle token expiration and validation errors
16
+ * - Support refresh tokens (optional)
17
+ * - Validate user ID conflicts if both individual and org IDs present in JWT
18
+ *
19
+ * RECOMMENDED IMPLEMENTATION:
20
+ * - Use 'jsonwebtoken' package for JWT parsing and validation
21
+ * - Cache JWT public keys for RS* algorithms
22
+ * - Add comprehensive error handling for invalid tokens
23
+ * - Log authentication attempts for security auditing
24
+ *
25
+ * @todo Implement JWT validation with jsonwebtoken package
26
+ * @todo Add unit tests for JWT parsing and claim extraction
27
+ * @todo Document adopter JWT integration guide in Frigg docs
28
+ * @todo Add support for JWT refresh tokens
29
+ * @todo Implement JWT public key caching for RS* algorithms
30
+ *
31
+ * @class GetUserFromAdopterJwt
32
+ */
33
+ class GetUserFromAdopterJwt {
34
+ /**
35
+ * Creates a new GetUserFromAdopterJwt instance.
36
+ * @param {Object} params - Configuration parameters.
37
+ * @param {import('../repositories/user-repository-interface').UserRepositoryInterface} params.userRepository - Repository for user data operations.
38
+ * @param {Object} params.userConfig - The user config in the app definition.
39
+ */
40
+ constructor({ userRepository, userConfig }) {
41
+ this.userRepository = userRepository;
42
+ this.userConfig = userConfig;
43
+ }
44
+
45
+ /**
46
+ * Executes the use case.
47
+ * @async
48
+ * @param {string} jwtToken - The JWT token from the Authorization header.
49
+ * @returns {Promise<import('../user').User>} The authenticated user object.
50
+ * @throws {Boom} 501 Not Implemented - This feature is not yet available.
51
+ */
52
+ async execute(jwtToken) {
53
+ throw Boom.notImplemented(
54
+ 'Adopter JWT authentication is not yet implemented. ' +
55
+ 'This feature is planned for a future Frigg release. ' +
56
+ 'Please use one of the supported authentication modes instead: ' +
57
+ 'friggToken (native bearer token) or xFriggHeaders (backend-to-backend with x-frigg-appUserId/appOrgId headers).'
58
+ );
59
+
60
+ /* FUTURE IMPLEMENTATION PSEUDOCODE:
61
+
62
+ const jwt = require('jsonwebtoken');
63
+
64
+ // Validate JWT configuration exists
65
+ if (!this.userConfig.jwtConfig || !this.userConfig.jwtConfig.secret) {
66
+ throw Boom.badImplementation('JWT configuration is required when adopterJwt auth mode is enabled');
67
+ }
68
+
69
+ try {
70
+ // Verify and decode JWT
71
+ const decoded = jwt.verify(jwtToken, this.userConfig.jwtConfig.secret, {
72
+ algorithms: [this.userConfig.jwtConfig.algorithm || 'HS256']
73
+ });
74
+
75
+ // Extract user identifiers from claims
76
+ const appUserId = decoded[this.userConfig.jwtConfig.userIdClaim || 'sub'];
77
+ const appOrgId = decoded[this.userConfig.jwtConfig.orgIdClaim || 'org_id'];
78
+
79
+ // At least one identifier required
80
+ if (!appUserId && !appOrgId) {
81
+ throw Boom.badRequest('JWT must contain user or organization identifier claims');
82
+ }
83
+
84
+ // Find existing users
85
+ let individualUserData = null;
86
+ let organizationUserData = null;
87
+
88
+ if (appUserId) {
89
+ individualUserData = await this.userRepository.findIndividualUserByAppUserId(appUserId);
90
+ }
91
+
92
+ if (appOrgId) {
93
+ organizationUserData = await this.userRepository.findOrganizationUserByAppOrgId(appOrgId);
94
+ }
95
+
96
+ // Validate no conflicts if both IDs present
97
+ if (appUserId && appOrgId && individualUserData && organizationUserData) {
98
+ const individualOrgId = individualUserData.organizationUser?.toString();
99
+ const expectedOrgId = organizationUserData.id?.toString();
100
+
101
+ if (individualOrgId !== expectedOrgId) {
102
+ throw Boom.badRequest(
103
+ 'User ID mismatch: JWT claims refer to different users. ' +
104
+ 'Individual and organization IDs must belong to the same user.'
105
+ );
106
+ }
107
+ }
108
+
109
+ // Auto-create if not found
110
+ if (!individualUserData && !organizationUserData) {
111
+ if (appUserId) {
112
+ individualUserData = await this.userRepository.createIndividualUser({
113
+ appUserId,
114
+ username: `jwt-user-${appUserId}`,
115
+ email: decoded.email || `${appUserId}@jwt.local`,
116
+ });
117
+ } else {
118
+ organizationUserData = await this.userRepository.createOrganizationUser({
119
+ appOrgId,
120
+ });
121
+ }
122
+ }
123
+
124
+ return new User(
125
+ individualUserData,
126
+ organizationUserData,
127
+ this.userConfig.usePassword,
128
+ this.userConfig.primary,
129
+ this.userConfig.individualUserRequired,
130
+ this.userConfig.organizationUserRequired
131
+ );
132
+
133
+ } catch (error) {
134
+ if (error.name === 'TokenExpiredError') {
135
+ throw Boom.unauthorized('JWT token has expired');
136
+ } else if (error.name === 'JsonWebTokenError') {
137
+ throw Boom.unauthorized('Invalid JWT token');
138
+ } else if (error.isBoom) {
139
+ throw error;
140
+ }
141
+ throw Boom.unauthorized('JWT authentication failed');
142
+ }
143
+ */
144
+ }
145
+ }
146
+
147
+ module.exports = { GetUserFromAdopterJwt };
148
+
149
+
@@ -0,0 +1,77 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { User } = require('../user');
3
+
4
+ /**
5
+ * Use case for retrieving a user from a bearer token.
6
+ * @class GetUserFromBearerToken
7
+ */
8
+ class GetUserFromBearerToken {
9
+ /**
10
+ * Creates a new GetUserFromBearerToken instance.
11
+ * @param {Object} params - Configuration parameters.
12
+ * @param {import('../user-repository-interface').UserRepositoryInterface} params.userRepository - Repository for user data operations.
13
+ * @param {Object} params.userConfig - The user config in the app definition.
14
+ */
15
+ constructor({ userRepository, userConfig }) {
16
+ this.userRepository = userRepository;
17
+ this.userConfig = userConfig;
18
+ }
19
+
20
+ /**
21
+ * Executes the use case.
22
+ * @async
23
+ * @param {string} bearerToken - The bearer token from the authorization header.
24
+ * @returns {Promise<import('../user').User>} The authenticated user object.
25
+ * @throws {Boom} 401 Unauthorized if the token is missing, malformed, or invalid.
26
+ */
27
+ async execute(bearerToken) {
28
+ if (!bearerToken) {
29
+ throw Boom.unauthorized('Missing Authorization Header');
30
+ }
31
+
32
+ const token = bearerToken.split(' ')[1]?.trim();
33
+ if (!token) {
34
+ throw Boom.unauthorized('Invalid Token Format');
35
+ }
36
+
37
+ const sessionToken = await this.userRepository.getSessionToken(token);
38
+
39
+ if (!sessionToken) {
40
+ throw Boom.unauthorized('Session Token Not Found');
41
+ }
42
+
43
+ if (this.userConfig.primary === 'organization') {
44
+ const organizationUserData = await this.userRepository.findOrganizationUserById(sessionToken.user);
45
+
46
+ if (!organizationUserData) {
47
+ throw Boom.unauthorized('Organization User Not Found');
48
+ }
49
+
50
+ return new User(
51
+ null,
52
+ organizationUserData,
53
+ this.userConfig.usePassword,
54
+ this.userConfig.primary,
55
+ this.userConfig.individualUserRequired,
56
+ this.userConfig.organizationUserRequired
57
+ );
58
+ }
59
+
60
+ const individualUserData = await this.userRepository.findIndividualUserById(sessionToken.user);
61
+
62
+ if (!individualUserData) {
63
+ throw Boom.unauthorized('Individual User Not Found');
64
+ }
65
+
66
+ return new User(
67
+ individualUserData,
68
+ null,
69
+ this.userConfig.usePassword,
70
+ this.userConfig.primary,
71
+ this.userConfig.individualUserRequired,
72
+ this.userConfig.organizationUserRequired
73
+ );
74
+ }
75
+ }
76
+
77
+ module.exports = { GetUserFromBearerToken };
@@ -0,0 +1,106 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { User } = require('../user');
3
+
4
+ /**
5
+ * Use case for retrieving or creating a user from x-frigg header identifiers.
6
+ * Supports backend-to-backend API communication using application user IDs.
7
+ *
8
+ * @class GetUserFromXFriggHeaders
9
+ */
10
+ class GetUserFromXFriggHeaders {
11
+ /**
12
+ * Creates a new GetUserFromXFriggHeaders instance.
13
+ * @param {Object} params - Configuration parameters.
14
+ * @param {import('../repositories/user-repository-interface').UserRepositoryInterface} params.userRepository - Repository for user data operations.
15
+ * @param {Object} params.userConfig - The user config in the app definition.
16
+ */
17
+ constructor({ userRepository, userConfig }) {
18
+ this.userRepository = userRepository;
19
+ this.userConfig = userConfig;
20
+ }
21
+
22
+ /**
23
+ * Executes the use case.
24
+ * @async
25
+ * @param {string} [appUserId] - The app user ID from x-frigg-appUserId header.
26
+ * @param {string} [appOrgId] - The app organization ID from x-frigg-appOrgId header.
27
+ * @returns {Promise<import('../user').User>} The authenticated user object.
28
+ * @throws {Boom} 400 Bad Request if neither ID is provided or if both IDs are provided but belong to different users.
29
+ */
30
+ async execute(appUserId, appOrgId) {
31
+ // At least one header must be provided
32
+ if (!appUserId && !appOrgId) {
33
+ throw Boom.badRequest(
34
+ 'At least one of x-frigg-appUserId or x-frigg-appOrgId headers is required for backend-to-backend authentication'
35
+ );
36
+ }
37
+
38
+ // Find users by both IDs if both are provided
39
+ let individualUserData = null;
40
+ let organizationUserData = null;
41
+
42
+ if (appUserId && this.userConfig.individualUserRequired !== false) {
43
+ individualUserData =
44
+ await this.userRepository.findIndividualUserByAppUserId(
45
+ appUserId
46
+ );
47
+ }
48
+
49
+ if (appOrgId && this.userConfig.organizationUserRequired) {
50
+ organizationUserData =
51
+ await this.userRepository.findOrganizationUserByAppOrgId(
52
+ appOrgId
53
+ );
54
+ }
55
+
56
+ // VALIDATION: If both IDs provided and both users exist, verify they match
57
+ if (
58
+ appUserId &&
59
+ appOrgId &&
60
+ individualUserData &&
61
+ organizationUserData
62
+ ) {
63
+ // Check if individual user is linked to the org user
64
+ const individualOrgId =
65
+ individualUserData.organizationUser?.toString();
66
+ const expectedOrgId = organizationUserData.id?.toString();
67
+
68
+ if (individualOrgId !== expectedOrgId) {
69
+ throw Boom.badRequest(
70
+ 'User ID mismatch: x-frigg-appUserId and x-frigg-appOrgId refer to different users. ' +
71
+ 'Provide only one identifier or ensure they belong to the same user.'
72
+ );
73
+ }
74
+ }
75
+
76
+ // Auto-create user if not found
77
+ if (!individualUserData && !organizationUserData) {
78
+ if (appUserId) {
79
+ individualUserData =
80
+ await this.userRepository.createIndividualUser({
81
+ appUserId,
82
+ username: `app-user-${appUserId}`,
83
+ email: `${appUserId}@app.local`,
84
+ });
85
+ } else {
86
+ organizationUserData =
87
+ await this.userRepository.createOrganizationUser({
88
+ appOrgId,
89
+ });
90
+ }
91
+ }
92
+
93
+ return new User(
94
+ individualUserData,
95
+ organizationUserData,
96
+ this.userConfig.usePassword,
97
+ this.userConfig.primary,
98
+ this.userConfig.individualUserRequired,
99
+ this.userConfig.organizationUserRequired
100
+ );
101
+ }
102
+ }
103
+
104
+ module.exports = { GetUserFromXFriggHeaders };
105
+
106
+