@friggframework/core 2.0.0-next.9 → 2.0.0-next.91

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 (309) hide show
  1. package/CLAUDE.md +702 -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 +271 -0
  7. package/application/commands/scheduler-commands.js +263 -0
  8. package/application/commands/user-commands.js +283 -0
  9. package/application/index.js +73 -0
  10. package/assertions/index.js +0 -3
  11. package/core/CLAUDE.md +690 -0
  12. package/core/Worker.js +60 -24
  13. package/core/create-handler.js +84 -6
  14. package/credential/repositories/credential-repository-documentdb.js +304 -0
  15. package/credential/repositories/credential-repository-factory.js +54 -0
  16. package/credential/repositories/credential-repository-interface.js +98 -0
  17. package/credential/repositories/credential-repository-mongo.js +269 -0
  18. package/credential/repositories/credential-repository-postgres.js +287 -0
  19. package/credential/repositories/credential-repository.js +300 -0
  20. package/credential/use-cases/get-credential-for-user.js +25 -0
  21. package/credential/use-cases/update-authentication-status.js +15 -0
  22. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  23. package/database/adapters/lambda-invoker.js +97 -0
  24. package/database/config.js +154 -0
  25. package/database/documentdb-encryption-service.js +330 -0
  26. package/database/documentdb-utils.js +136 -0
  27. package/database/encryption/README.md +839 -0
  28. package/database/encryption/documentdb-encryption-service.md +3575 -0
  29. package/database/encryption/encryption-schema-registry.js +401 -0
  30. package/database/encryption/field-encryption-service.js +254 -0
  31. package/database/encryption/logger.js +79 -0
  32. package/database/encryption/prisma-encryption-extension.js +230 -0
  33. package/database/index.js +21 -21
  34. package/database/prisma.js +182 -0
  35. package/database/repositories/health-check-repository-documentdb.js +138 -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/migration-status-repository-s3.js +137 -0
  41. package/database/use-cases/check-database-health-use-case.js +29 -0
  42. package/database/use-cases/check-database-state-use-case.js +81 -0
  43. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  44. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  45. package/database/use-cases/get-migration-status-use-case.js +93 -0
  46. package/database/use-cases/run-database-migration-use-case.js +139 -0
  47. package/database/use-cases/test-encryption-use-case.js +253 -0
  48. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  49. package/database/utils/mongodb-collection-utils.js +94 -0
  50. package/database/utils/mongodb-schema-init.js +108 -0
  51. package/database/utils/prisma-runner.js +477 -0
  52. package/database/utils/prisma-schema-parser.js +182 -0
  53. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  54. package/encrypt/Cryptor.js +34 -168
  55. package/encrypt/index.js +1 -2
  56. package/errors/client-safe-error.js +26 -0
  57. package/errors/fetch-error.js +15 -7
  58. package/errors/index.js +2 -0
  59. package/generated/prisma-mongodb/client.d.ts +1 -0
  60. package/generated/prisma-mongodb/client.js +4 -0
  61. package/generated/prisma-mongodb/default.d.ts +1 -0
  62. package/generated/prisma-mongodb/default.js +4 -0
  63. package/generated/prisma-mongodb/edge.d.ts +1 -0
  64. package/generated/prisma-mongodb/edge.js +335 -0
  65. package/generated/prisma-mongodb/index-browser.js +317 -0
  66. package/generated/prisma-mongodb/index.d.ts +22955 -0
  67. package/generated/prisma-mongodb/index.js +360 -0
  68. package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  69. package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  70. package/generated/prisma-mongodb/package.json +183 -0
  71. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  72. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  73. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  74. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  75. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  76. package/generated/prisma-mongodb/runtime/library.js +146 -0
  77. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  78. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  79. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  80. package/generated/prisma-mongodb/schema.prisma +368 -0
  81. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  82. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  83. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  84. package/generated/prisma-mongodb/wasm.js +342 -0
  85. package/generated/prisma-postgresql/client.d.ts +1 -0
  86. package/generated/prisma-postgresql/client.js +4 -0
  87. package/generated/prisma-postgresql/default.d.ts +1 -0
  88. package/generated/prisma-postgresql/default.js +4 -0
  89. package/generated/prisma-postgresql/edge.d.ts +1 -0
  90. package/generated/prisma-postgresql/edge.js +357 -0
  91. package/generated/prisma-postgresql/index-browser.js +339 -0
  92. package/generated/prisma-postgresql/index.d.ts +25135 -0
  93. package/generated/prisma-postgresql/index.js +382 -0
  94. package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  95. package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  96. package/generated/prisma-postgresql/package.json +183 -0
  97. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  98. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  99. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  100. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  101. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  102. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  103. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  104. package/generated/prisma-postgresql/runtime/library.js +146 -0
  105. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  106. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  107. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  108. package/generated/prisma-postgresql/schema.prisma +351 -0
  109. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  110. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  111. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  112. package/generated/prisma-postgresql/wasm.js +364 -0
  113. package/handlers/WEBHOOKS.md +653 -0
  114. package/handlers/app-definition-loader.js +38 -0
  115. package/handlers/app-handler-helpers.js +57 -0
  116. package/handlers/backend-utils.js +297 -0
  117. package/handlers/database-migration-handler.js +227 -0
  118. package/handlers/integration-event-dispatcher.js +54 -0
  119. package/handlers/routers/HEALTHCHECK.md +342 -0
  120. package/handlers/routers/auth.js +15 -0
  121. package/handlers/routers/db-migration.handler.js +29 -0
  122. package/handlers/routers/db-migration.js +326 -0
  123. package/handlers/routers/health.js +518 -0
  124. package/handlers/routers/integration-defined-routers.js +117 -0
  125. package/handlers/routers/integration-webhook-routers.js +67 -0
  126. package/handlers/routers/user.js +63 -0
  127. package/handlers/routers/websocket.js +57 -0
  128. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  129. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  130. package/handlers/workers/db-migration.js +352 -0
  131. package/handlers/workers/dlq-processor.js +63 -0
  132. package/handlers/workers/integration-defined-workers.js +30 -0
  133. package/index.js +82 -46
  134. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  135. package/infrastructure/scheduler/index.js +33 -0
  136. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  137. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  138. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  139. package/integrations/EXTENSIONS.md +240 -0
  140. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  141. package/integrations/extension.js +254 -0
  142. package/integrations/index.js +20 -10
  143. package/integrations/integration-base.js +487 -55
  144. package/integrations/integration-router.js +396 -179
  145. package/integrations/options.js +1 -1
  146. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  147. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  148. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  149. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  150. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  151. package/integrations/repositories/integration-mapping-repository.js +156 -0
  152. package/integrations/repositories/integration-repository-documentdb.js +219 -0
  153. package/integrations/repositories/integration-repository-factory.js +51 -0
  154. package/integrations/repositories/integration-repository-interface.js +144 -0
  155. package/integrations/repositories/integration-repository-mongo.js +330 -0
  156. package/integrations/repositories/integration-repository-postgres.js +385 -0
  157. package/integrations/repositories/process-repository-documentdb.js +311 -0
  158. package/integrations/repositories/process-repository-factory.js +53 -0
  159. package/integrations/repositories/process-repository-interface.js +136 -0
  160. package/integrations/repositories/process-repository-mongo.js +262 -0
  161. package/integrations/repositories/process-repository-postgres.js +380 -0
  162. package/integrations/repositories/process-update-ops-shared.js +112 -0
  163. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  164. package/integrations/tests/doubles/dummy-integration-class.js +105 -0
  165. package/integrations/tests/doubles/test-integration-repository.js +112 -0
  166. package/integrations/use-cases/create-integration.js +83 -0
  167. package/integrations/use-cases/create-process.js +128 -0
  168. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  169. package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
  170. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
  171. package/integrations/use-cases/get-integration-for-user.js +78 -0
  172. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  173. package/integrations/use-cases/get-integration-instance.js +83 -0
  174. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  175. package/integrations/use-cases/get-possible-integrations.js +27 -0
  176. package/integrations/use-cases/get-process.js +87 -0
  177. package/integrations/use-cases/index.js +19 -0
  178. package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
  179. package/integrations/use-cases/load-integration-context.js +71 -0
  180. package/integrations/use-cases/update-integration-messages.js +44 -0
  181. package/integrations/use-cases/update-integration-status.js +32 -0
  182. package/integrations/use-cases/update-integration.js +92 -0
  183. package/integrations/use-cases/update-process-metrics.js +214 -0
  184. package/integrations/use-cases/update-process-state.js +158 -0
  185. package/integrations/utils/map-integration-dto.js +37 -0
  186. package/jest-global-setup-noop.js +3 -0
  187. package/jest-global-teardown-noop.js +3 -0
  188. package/logs/logger.js +0 -4
  189. package/{module-plugin → modules}/index.js +0 -10
  190. package/modules/module-factory.js +56 -0
  191. package/modules/module.js +274 -0
  192. package/modules/repositories/module-repository-documentdb.js +350 -0
  193. package/modules/repositories/module-repository-factory.js +40 -0
  194. package/modules/repositories/module-repository-interface.js +145 -0
  195. package/modules/repositories/module-repository-mongo.js +436 -0
  196. package/modules/repositories/module-repository-postgres.js +481 -0
  197. package/modules/repositories/module-repository.js +369 -0
  198. package/modules/requester/api-key.js +52 -0
  199. package/modules/requester/oauth-2.js +396 -0
  200. package/modules/requester/requester.js +280 -0
  201. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  202. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  203. package/modules/tests/doubles/test-module-factory.js +16 -0
  204. package/modules/tests/doubles/test-module-repository.js +39 -0
  205. package/modules/use-cases/get-entities-for-user.js +32 -0
  206. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  207. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  208. package/modules/use-cases/get-module-instance-from-type.js +34 -0
  209. package/modules/use-cases/get-module.js +74 -0
  210. package/modules/use-cases/process-authorization-callback.js +243 -0
  211. package/modules/use-cases/refresh-entity-options.js +72 -0
  212. package/modules/use-cases/test-module-auth.js +72 -0
  213. package/modules/utils/map-module-dto.js +18 -0
  214. package/package.json +82 -50
  215. package/prisma-mongodb/schema.prisma +368 -0
  216. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  217. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  218. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  219. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  220. package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
  221. package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
  222. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  223. package/prisma-postgresql/schema.prisma +351 -0
  224. package/queues/queuer-util.js +103 -21
  225. package/syncs/manager.js +468 -443
  226. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  227. package/syncs/repositories/sync-repository-factory.js +43 -0
  228. package/syncs/repositories/sync-repository-interface.js +109 -0
  229. package/syncs/repositories/sync-repository-mongo.js +239 -0
  230. package/syncs/repositories/sync-repository-postgres.js +319 -0
  231. package/syncs/sync.js +0 -1
  232. package/token/repositories/token-repository-documentdb.js +137 -0
  233. package/token/repositories/token-repository-factory.js +40 -0
  234. package/token/repositories/token-repository-interface.js +131 -0
  235. package/token/repositories/token-repository-mongo.js +219 -0
  236. package/token/repositories/token-repository-postgres.js +264 -0
  237. package/token/repositories/token-repository.js +219 -0
  238. package/types/associations/index.d.ts +0 -17
  239. package/types/core/index.d.ts +12 -4
  240. package/types/database/index.d.ts +10 -2
  241. package/types/encrypt/index.d.ts +5 -3
  242. package/types/integrations/index.d.ts +3 -8
  243. package/types/module-plugin/index.d.ts +17 -69
  244. package/types/syncs/index.d.ts +0 -17
  245. package/user/repositories/user-repository-documentdb.js +441 -0
  246. package/user/repositories/user-repository-factory.js +52 -0
  247. package/user/repositories/user-repository-interface.js +201 -0
  248. package/user/repositories/user-repository-mongo.js +308 -0
  249. package/user/repositories/user-repository-postgres.js +360 -0
  250. package/user/tests/doubles/test-user-repository.js +72 -0
  251. package/user/use-cases/authenticate-user.js +127 -0
  252. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  253. package/user/use-cases/create-individual-user.js +61 -0
  254. package/user/use-cases/create-organization-user.js +47 -0
  255. package/user/use-cases/create-token-for-user-id.js +30 -0
  256. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  257. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  258. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  259. package/user/use-cases/login-user.js +122 -0
  260. package/user/user.js +125 -0
  261. package/utils/backend-path.js +38 -0
  262. package/utils/index.js +6 -0
  263. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  264. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  265. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  266. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  267. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  268. package/websocket/repositories/websocket-connection-repository.js +161 -0
  269. package/assertions/is-equal.js +0 -17
  270. package/associations/model.js +0 -54
  271. package/database/models/IndividualUser.js +0 -76
  272. package/database/models/OrganizationUser.js +0 -29
  273. package/database/models/State.js +0 -9
  274. package/database/models/Token.js +0 -70
  275. package/database/models/UserModel.js +0 -7
  276. package/database/models/WebsocketConnection.js +0 -49
  277. package/database/mongo.js +0 -45
  278. package/database/mongoose.js +0 -5
  279. package/encrypt/Cryptor.test.js +0 -32
  280. package/encrypt/encrypt.js +0 -132
  281. package/encrypt/encrypt.test.js +0 -1069
  282. package/encrypt/test-encrypt.js +0 -107
  283. package/errors/base-error.test.js +0 -32
  284. package/errors/fetch-error.test.js +0 -79
  285. package/errors/halt-error.test.js +0 -11
  286. package/errors/validation-errors.test.js +0 -120
  287. package/integrations/create-frigg-backend.js +0 -31
  288. package/integrations/integration-factory.js +0 -251
  289. package/integrations/integration-mapping.js +0 -43
  290. package/integrations/integration-model.js +0 -46
  291. package/integrations/integration-user.js +0 -144
  292. package/integrations/test/integration-base.test.js +0 -144
  293. package/lambda/TimeoutCatcher.test.js +0 -68
  294. package/logs/logger.test.js +0 -76
  295. package/module-plugin/auther.js +0 -393
  296. package/module-plugin/credential.js +0 -22
  297. package/module-plugin/entity-manager.js +0 -70
  298. package/module-plugin/entity.js +0 -46
  299. package/module-plugin/manager.js +0 -169
  300. package/module-plugin/module-factory.js +0 -61
  301. package/module-plugin/requester/api-key.js +0 -36
  302. package/module-plugin/requester/oauth-2.js +0 -219
  303. package/module-plugin/requester/requester.js +0 -165
  304. package/module-plugin/requester/requester.test.js +0 -28
  305. package/module-plugin/test/auther.test.js +0 -97
  306. package/syncs/model.js +0 -62
  307. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  308. /package/{module-plugin → modules}/requester/basic.js +0 -0
  309. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,518 @@
1
+ const { Router } = require('express');
2
+ const { createAppHandler } = require('./../app-handler-helpers');
3
+ const { loadAppDefinition } = require('./../app-definition-loader');
4
+ const { ModuleFactory } = require('../../modules/module-factory');
5
+ const {
6
+ getModulesDefinitionFromIntegrationClasses,
7
+ } = require('../../integrations/utils/map-integration-dto');
8
+ const {
9
+ createModuleRepository,
10
+ } = require('../../modules/repositories/module-repository-factory');
11
+ const {
12
+ createHealthCheckRepository,
13
+ } = require('../../database/repositories/health-check-repository-factory');
14
+ const { prisma } = require('../../database/prisma');
15
+ const {
16
+ TestEncryptionUseCase,
17
+ } = require('../../database/use-cases/test-encryption-use-case');
18
+ const {
19
+ CheckDatabaseHealthUseCase,
20
+ } = require('../../database/use-cases/check-database-health-use-case');
21
+ const {
22
+ CheckEncryptionHealthUseCase,
23
+ } = require('../../database/use-cases/check-encryption-health-use-case');
24
+ const {
25
+ CheckExternalApisHealthUseCase,
26
+ } = require('../use-cases/check-external-apis-health-use-case');
27
+ const {
28
+ CheckIntegrationsHealthUseCase,
29
+ } = require('../use-cases/check-integrations-health-use-case');
30
+
31
+ const router = Router();
32
+ const healthCheckRepository = createHealthCheckRepository({ prismaClient: prisma });
33
+
34
+ // Load integrations and create factories just like auth router does
35
+ // This verifies the system can properly load integrations
36
+ let moduleFactory, integrationClasses;
37
+ try {
38
+ const appDef = loadAppDefinition();
39
+ integrationClasses = appDef.integrations || [];
40
+
41
+ const moduleRepository = createModuleRepository();
42
+ const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrationClasses);
43
+
44
+ moduleFactory = new ModuleFactory({
45
+ moduleRepository,
46
+ moduleDefinitions,
47
+ });
48
+ } catch (error) {
49
+ console.error('Failed to load integrations for health check:', error.message);
50
+ // Factories will be undefined, health check will report unhealthy
51
+ moduleFactory = undefined;
52
+ integrationClasses = [];
53
+ }
54
+
55
+ const testEncryptionUseCase = new TestEncryptionUseCase({
56
+ healthCheckRepository,
57
+ });
58
+ const checkDatabaseHealthUseCase = new CheckDatabaseHealthUseCase({
59
+ healthCheckRepository,
60
+ });
61
+ const checkEncryptionHealthUseCase = new CheckEncryptionHealthUseCase({
62
+ testEncryptionUseCase,
63
+ });
64
+ const checkExternalApisHealthUseCase = new CheckExternalApisHealthUseCase();
65
+ const checkIntegrationsHealthUseCase = new CheckIntegrationsHealthUseCase({
66
+ moduleFactory,
67
+ integrationClasses,
68
+ });
69
+
70
+ const validateApiKey = (req, res, next) => {
71
+ const apiKey = req.headers['x-frigg-health-api-key'];
72
+
73
+ if (req.path === '/health') {
74
+ return next();
75
+ }
76
+
77
+ if (!apiKey || apiKey !== process.env.HEALTH_API_KEY) {
78
+ console.error('Unauthorized access attempt to health endpoint');
79
+ return res.status(401).json({
80
+ status: 'error',
81
+ message: 'Unauthorized - x-frigg-health-api-key header required',
82
+ });
83
+ }
84
+
85
+ next();
86
+ };
87
+
88
+ router.use(validateApiKey);
89
+
90
+ // Helper to detect VPC configuration
91
+ const detectVpcConfiguration = async () => {
92
+ const results = {
93
+ isInVpc: false,
94
+ hasInternetAccess: false,
95
+ canResolvePublicDns: false,
96
+ canConnectToAws: false,
97
+ vpcEndpoints: [],
98
+ };
99
+
100
+ try {
101
+ // Check if we're in a VPC by looking for VPC-specific environment
102
+ // Lambda in VPC has specific network interface configuration
103
+ const dns = require('dns').promises;
104
+
105
+ // Test 1: Can we resolve public DNS? (indicates DNS configuration)
106
+ try {
107
+ await Promise.race([
108
+ dns.resolve4('www.google.com'),
109
+ new Promise((_, reject) =>
110
+ setTimeout(() => reject(new Error('timeout')), 2000)
111
+ ),
112
+ ]);
113
+ results.canResolvePublicDns = true;
114
+ } catch (e) {
115
+ console.log('Public DNS resolution failed:', e.message);
116
+ }
117
+
118
+ // Test 2: Can we reach internet? (indicates NAT gateway)
119
+ try {
120
+ const https = require('https');
121
+ await new Promise((resolve, reject) => {
122
+ const req = https.get(
123
+ 'https://www.google.com',
124
+ { timeout: 2000 },
125
+ (res) => {
126
+ res.destroy();
127
+ resolve(true);
128
+ }
129
+ );
130
+ req.on('error', reject);
131
+ req.on('timeout', () => {
132
+ req.destroy();
133
+ reject(new Error('timeout'));
134
+ });
135
+ });
136
+ results.hasInternetAccess = true;
137
+ } catch (e) {
138
+ console.log('Internet connectivity test failed:', e.message);
139
+ }
140
+
141
+ // Test 3: Check for VPC endpoints by trying to resolve internal AWS endpoints
142
+ const region = process.env.AWS_REGION; // Lambda always provides this
143
+ const vpcEndpointDomains = [
144
+ `com.amazonaws.${region}.kms`,
145
+ `com.amazonaws.vpce.${region}`,
146
+ `kms.${region}.amazonaws.com`,
147
+ ];
148
+
149
+ for (const domain of vpcEndpointDomains) {
150
+ try {
151
+ const addresses = await Promise.race([
152
+ dns.resolve4(domain).catch(() => dns.resolve6(domain)),
153
+ new Promise((_, reject) =>
154
+ setTimeout(() => reject(new Error('timeout')), 1000)
155
+ ),
156
+ ]);
157
+ if (addresses && addresses.length > 0) {
158
+ // Check if it's a private IP (VPC endpoint indicator)
159
+ const isPrivateIp = addresses.some(
160
+ (ip) =>
161
+ ip.startsWith('10.') ||
162
+ ip.startsWith('172.') ||
163
+ ip.startsWith('192.168.')
164
+ );
165
+ if (isPrivateIp) {
166
+ results.vpcEndpoints.push(domain);
167
+ }
168
+ }
169
+ } catch (e) {
170
+ // Expected for non-existent endpoints
171
+ }
172
+ }
173
+
174
+ // Check if Lambda is in VPC using VPC_ENABLED env var set by infrastructure
175
+ results.isInVpc = process.env.VPC_ENABLED === 'true' ||
176
+ (!results.hasInternetAccess && results.canResolvePublicDns) ||
177
+ results.vpcEndpoints.length > 0;
178
+
179
+ results.canConnectToAws =
180
+ results.hasInternetAccess || results.vpcEndpoints.length > 0;
181
+ } catch (error) {
182
+ console.error('VPC detection error:', error.message);
183
+ }
184
+
185
+ return results;
186
+ };
187
+
188
+ // KMS decrypt capability check
189
+ const checkKmsDecryptCapability = async () => {
190
+ const start = Date.now();
191
+ const { KMS_KEY_ARN } = process.env;
192
+ if (!KMS_KEY_ARN) {
193
+ return {
194
+ status: 'skipped',
195
+ reason: 'KMS_KEY_ARN not configured',
196
+ };
197
+ }
198
+
199
+ // Log environment for debugging
200
+ console.log('KMS Check Debug:', {
201
+ hasKmsKeyArn: !!KMS_KEY_ARN,
202
+ kmsKeyArnPrefix: KMS_KEY_ARN?.substring(0, 30),
203
+ awsRegion: process.env.AWS_REGION,
204
+ hasDiscoveryKey: !!process.env.AWS_DISCOVERY_KMS_KEY_ID,
205
+ });
206
+
207
+ // First, detect VPC configuration
208
+ const vpcConfig = await detectVpcConfiguration();
209
+ console.log('VPC Configuration:', vpcConfig);
210
+
211
+ // Test DNS resolution for KMS endpoint
212
+ try {
213
+ const dns = require('dns').promises;
214
+ const region = process.env.AWS_REGION; // Lambda always provides this
215
+ const kmsEndpoint = `kms.${region}.amazonaws.com`;
216
+ console.log('Testing DNS resolution for:', kmsEndpoint);
217
+
218
+ // Wrap DNS resolution in a timeout
219
+ const dnsPromise = dns.resolve4(kmsEndpoint);
220
+ const timeoutPromise = new Promise((_, reject) =>
221
+ setTimeout(() => reject(new Error('DNS resolution timeout')), 3000)
222
+ );
223
+
224
+ const addresses = await Promise.race([dnsPromise, timeoutPromise]);
225
+ console.log('KMS endpoint resolved to:', addresses);
226
+
227
+ // Check if resolved to private IP (VPC endpoint)
228
+ const isVpcEndpoint = addresses.some(
229
+ (ip) =>
230
+ ip.startsWith('10.') ||
231
+ ip.startsWith('172.') ||
232
+ ip.startsWith('192.168.')
233
+ );
234
+
235
+ if (isVpcEndpoint) {
236
+ console.log(
237
+ 'KMS VPC Endpoint detected - using private connectivity'
238
+ );
239
+ }
240
+
241
+ // Test TCP connectivity to KMS (port 443)
242
+ const net = require('net');
243
+ const testConnection = () =>
244
+ new Promise((resolve) => {
245
+ const socket = new net.Socket();
246
+ const connectionTimeout = setTimeout(() => {
247
+ socket.destroy();
248
+ resolve({ connected: false, error: 'Connection timeout' });
249
+ }, 3000);
250
+
251
+ socket.on('connect', () => {
252
+ clearTimeout(connectionTimeout);
253
+ socket.destroy();
254
+ resolve({ connected: true });
255
+ });
256
+
257
+ socket.on('error', (err) => {
258
+ clearTimeout(connectionTimeout);
259
+ resolve({ connected: false, error: err.message });
260
+ });
261
+
262
+ // Try connecting to first resolved address on HTTPS port
263
+ socket.connect(443, addresses[0]);
264
+ });
265
+
266
+ const connResult = await testConnection();
267
+ console.log('TCP connectivity test:', connResult);
268
+
269
+ if (!connResult.connected) {
270
+ return {
271
+ status: 'unhealthy',
272
+ error: `Cannot connect to KMS endpoint: ${connResult.error}`,
273
+ dnsResolved: true,
274
+ tcpConnection: false,
275
+ vpcConfig,
276
+ latencyMs: Date.now() - start,
277
+ };
278
+ }
279
+ } catch (dnsError) {
280
+ console.error('DNS resolution failed:', dnsError.message);
281
+ return {
282
+ status: 'unhealthy',
283
+ error: `Cannot resolve KMS endpoint: ${dnsError.message}`,
284
+ dnsResolved: false,
285
+ vpcConfig,
286
+ latencyMs: Date.now() - start,
287
+ };
288
+ }
289
+
290
+ try {
291
+ // Use AWS SDK v3 for consistency with the rest of the codebase
292
+ // eslint-disable-next-line global-require
293
+ const {
294
+ KMSClient,
295
+ GenerateDataKeyCommand,
296
+ DecryptCommand,
297
+ } = require('@aws-sdk/client-kms');
298
+
299
+ // Lambda always provides AWS_REGION
300
+ const region = process.env.AWS_REGION;
301
+
302
+ const kms = new KMSClient({
303
+ region,
304
+ requestHandler: {
305
+ connectionTimeout: 10000, // 10 second connection timeout
306
+ requestTimeout: 25000, // 25 second timeout for slow VPC connections
307
+ },
308
+ maxAttempts: 1, // No retries on health checks
309
+ });
310
+
311
+ // Generate a data key (without plaintext logging) then immediately decrypt ciphertext to ensure decrypt perms.
312
+ const dataKeyResp = await kms.send(
313
+ new GenerateDataKeyCommand({
314
+ KeyId: KMS_KEY_ARN,
315
+ KeySpec: 'AES_256',
316
+ })
317
+ );
318
+ const decryptResp = await kms.send(
319
+ new DecryptCommand({ CiphertextBlob: dataKeyResp.CiphertextBlob })
320
+ );
321
+
322
+ const success = Boolean(
323
+ dataKeyResp.CiphertextBlob && decryptResp.Plaintext
324
+ );
325
+
326
+ return {
327
+ status: success ? 'healthy' : 'unhealthy',
328
+ kmsKeyArnSuffix: KMS_KEY_ARN.slice(-12),
329
+ vpcConfig,
330
+ latencyMs: Date.now() - start,
331
+ };
332
+ } catch (error) {
333
+ return {
334
+ status: 'unhealthy',
335
+ error: error.message,
336
+ vpcConfig,
337
+ latencyMs: Date.now() - start,
338
+ };
339
+ }
340
+ };
341
+
342
+ router.get('/health', async (_req, res) => {
343
+ const status = {
344
+ status: 'ok',
345
+ timestamp: new Date().toISOString(),
346
+ service: 'frigg-core-api',
347
+ };
348
+
349
+ res.status(200).json(status);
350
+ });
351
+
352
+ router.get('/health/detailed', async (_req, res) => {
353
+ console.log('Starting detailed health check');
354
+ const startTime = Date.now();
355
+
356
+ const response = {
357
+ service: 'frigg-core-api',
358
+ status: 'healthy',
359
+ timestamp: new Date().toISOString(),
360
+ checks: {},
361
+ };
362
+
363
+ console.log('Health Check Environment:', {
364
+ hasKmsKeyArn: !!process.env.KMS_KEY_ARN,
365
+ awsRegion: process.env.AWS_REGION,
366
+ awsDefaultRegion: process.env.AWS_DEFAULT_REGION,
367
+ nodeEnv: process.env.NODE_ENV,
368
+ stage: process.env.STAGE,
369
+ });
370
+
371
+ try {
372
+ console.log('Running network diagnostics...');
373
+ const networkStart = Date.now();
374
+ response.checks.network = await Promise.race([
375
+ detectVpcConfiguration(),
376
+ new Promise((_, reject) =>
377
+ setTimeout(
378
+ () => reject(new Error('Network diagnostics timeout')),
379
+ 5000
380
+ )
381
+ ),
382
+ ]);
383
+ response.checks.network.latencyMs = Date.now() - networkStart;
384
+ console.log('Network diagnostics completed:', response.checks.network);
385
+ } catch (error) {
386
+ response.checks.network = {
387
+ status: 'error',
388
+ error: error.message,
389
+ };
390
+ console.log('Network diagnostics error:', error.message);
391
+ }
392
+
393
+ try {
394
+ console.log('About to check KMS capability...');
395
+ const kmsCheckPromise = checkKmsDecryptCapability();
396
+ const kmsTimeoutPromise = new Promise((_, reject) =>
397
+ setTimeout(
398
+ () => reject(new Error('KMS check timeout after 25 seconds')),
399
+ 25000
400
+ )
401
+ );
402
+
403
+ response.checks.kms = await Promise.race([
404
+ kmsCheckPromise,
405
+ kmsTimeoutPromise,
406
+ ]);
407
+ if (response.checks.kms.status === 'unhealthy') {
408
+ response.status = 'unhealthy';
409
+ }
410
+ console.log('KMS check completed:', response.checks.kms);
411
+ } catch (error) {
412
+ response.checks.kms = { status: 'unhealthy', error: error.message };
413
+ response.status = 'unhealthy';
414
+ console.log('KMS check error:', error.message);
415
+ }
416
+
417
+ try {
418
+ response.checks.database = await checkDatabaseHealthUseCase.execute();
419
+ if (response.checks.database.status === 'unhealthy') {
420
+ response.status = 'unhealthy';
421
+ }
422
+ console.log('Database check completed:', response.checks.database);
423
+ } catch (error) {
424
+ response.checks.database = {
425
+ status: 'unhealthy',
426
+ error: error.message,
427
+ };
428
+ response.status = 'unhealthy';
429
+ console.log('Database check error:', error.message);
430
+ }
431
+
432
+ try {
433
+ response.checks.encryption = await checkEncryptionHealthUseCase.execute();
434
+ if (response.checks.encryption.status === 'unhealthy') {
435
+ response.status = 'unhealthy';
436
+ }
437
+ console.log('Encryption check completed:', response.checks.encryption);
438
+ } catch (error) {
439
+ response.checks.encryption = {
440
+ status: 'unhealthy',
441
+ error: error.message,
442
+ };
443
+ response.status = 'unhealthy';
444
+ console.log('Encryption check error:', error.message);
445
+ }
446
+
447
+ try {
448
+ const { apiStatuses, allReachable } = await checkExternalApisHealthUseCase.execute();
449
+ response.checks.externalApis = apiStatuses;
450
+ if (!allReachable) {
451
+ response.status = 'unhealthy';
452
+ }
453
+ console.log('External APIs check completed:', response.checks.externalApis);
454
+ } catch (error) {
455
+ response.checks.externalApis = {
456
+ status: 'unhealthy',
457
+ error: error.message,
458
+ };
459
+ response.status = 'unhealthy';
460
+ console.log('External APIs check error:', error.message);
461
+ }
462
+
463
+ try {
464
+ response.checks.integrations = checkIntegrationsHealthUseCase.execute();
465
+ console.log('Integrations check completed:', response.checks.integrations);
466
+ } catch (error) {
467
+ response.checks.integrations = {
468
+ status: 'unhealthy',
469
+ error: error.message,
470
+ };
471
+ response.status = 'unhealthy';
472
+ console.log('Integrations check error:', error.message);
473
+ }
474
+
475
+ response.responseTime = Date.now() - startTime;
476
+
477
+ const statusCode = response.status === 'healthy' ? 200 : 503;
478
+ res.status(statusCode).json(response);
479
+
480
+ console.log(
481
+ 'Final health status:',
482
+ response.status,
483
+ 'Response time:',
484
+ response.responseTime
485
+ );
486
+ });
487
+
488
+ router.get('/health/live', (_req, res) => {
489
+ res.status(200).json({
490
+ status: 'alive',
491
+ timestamp: new Date().toISOString(),
492
+ });
493
+ });
494
+
495
+ router.get('/health/ready', async (_req, res) => {
496
+ const dbHealth = await checkDatabaseHealthUseCase.execute();
497
+ const isDbReady = dbHealth.status === 'healthy';
498
+
499
+ const integrationsHealth = checkIntegrationsHealthUseCase.execute();
500
+ const areModulesReady = integrationsHealth.modules.count > 0;
501
+
502
+ const isReady = isDbReady && areModulesReady;
503
+
504
+ res.status(isReady ? 200 : 503).json({
505
+ ready: isReady,
506
+ timestamp: new Date().toISOString(),
507
+ checks: {
508
+ database: isDbReady,
509
+ modules: areModulesReady,
510
+ },
511
+ });
512
+ });
513
+
514
+ // DB-free: /health/ready probes the DB itself and degrades to 503. Eager-connect
515
+ // here would turn a DB outage into a 500, killing otherwise-healthy containers.
516
+ const handler = createAppHandler('HTTP Event: Health', router, false);
517
+
518
+ module.exports = { handler, router };
@@ -0,0 +1,117 @@
1
+ const { createAppHandler } = require('./../app-handler-helpers');
2
+ const {
3
+ loadAppDefinition,
4
+ } = require('../app-definition-loader');
5
+ const express = require('express');
6
+ const { Router } = express;
7
+ const { loadRouterFromObject } = require('../backend-utils');
8
+ const { getExtensionRoutes } = require('../../integrations/extension');
9
+
10
+ const handlers = {};
11
+ const { integrations: integrationClasses } = loadAppDefinition();
12
+
13
+ const routeKey = (method, path) => `${(method || 'ANY').toUpperCase()} ${path}`;
14
+
15
+ // Serverless function keys must be alphanumeric; binding keys are developer-chosen.
16
+ const sanitizeBindingKey = (name) => String(name).replace(/[^A-Za-z0-9]/g, '');
17
+
18
+ //todo: this should be in a use case class
19
+ for (const IntegrationClass of integrationClasses) {
20
+ const router = Router();
21
+ const basePath = `/api/${IntegrationClass.Definition.name}-integration`;
22
+ // Track (method, path) tuples to fail fast on conflicts between Definition.routes,
23
+ // extension routes, or two extensions claiming the same path.
24
+ const claimedRoutes = new Map();
25
+ const claim = (method, path, source) => {
26
+ const key = routeKey(method, path);
27
+ if (claimedRoutes.has(key)) {
28
+ const prev = claimedRoutes.get(key);
29
+ throw new Error(
30
+ `Integration "${IntegrationClass.Definition.name}" route conflict: ` +
31
+ `${key} declared by ${prev} and ${source}`
32
+ );
33
+ }
34
+ claimedRoutes.set(key, source);
35
+ };
36
+
37
+ console.log(`\n│ Configuring routes for ${IntegrationClass.Definition.name} Integration:`);
38
+
39
+ const routes = IntegrationClass.Definition.routes || [];
40
+ for (const routeDef of routes) {
41
+ if (typeof routeDef === 'function') {
42
+ router.use(basePath, routeDef(IntegrationClass));
43
+ console.log(`│ ANY ${basePath}/* (function handler)`);
44
+ } else if (routeDef instanceof express.Router) {
45
+ router.use(basePath, routeDef);
46
+ console.log(`│ ANY ${basePath}/* (express router)`);
47
+ } else if (typeof routeDef === 'object') {
48
+ claim(routeDef.method, routeDef.path, 'Definition.routes');
49
+ router.use(
50
+ basePath,
51
+ loadRouterFromObject(IntegrationClass, routeDef)
52
+ );
53
+ const method = (routeDef.method || 'ANY').toUpperCase();
54
+ const fullPath = `${basePath}${routeDef.path}`;
55
+ console.log(`│ ${method} ${fullPath}`);
56
+ }
57
+ }
58
+
59
+ // Each extension binding gets its own namespaced handler (/{bindingName}),
60
+ // so two modules' extensions can share a path like /webhooks without colliding.
61
+ const bindingGroups = new Map();
62
+ for (const extRoute of getExtensionRoutes(IntegrationClass)) {
63
+ const namespacedPath = `/${extRoute.bindingName}${extRoute.path}`;
64
+ claim(
65
+ extRoute.method,
66
+ namespacedPath,
67
+ `extension "${extRoute.extensionName}" (binding "${extRoute.bindingName}")`
68
+ );
69
+ if (!bindingGroups.has(extRoute.bindingName)) {
70
+ bindingGroups.set(extRoute.bindingName, {
71
+ router: Router(),
72
+ useDatabase: extRoute.useDatabase,
73
+ });
74
+ }
75
+ const group = bindingGroups.get(extRoute.bindingName);
76
+ group.router.use(
77
+ `${basePath}/${extRoute.bindingName}`,
78
+ loadRouterFromObject(IntegrationClass, extRoute)
79
+ );
80
+ console.log(
81
+ `│ ${extRoute.method.toUpperCase()} ${basePath}/${extRoute.bindingName}${extRoute.path} (extension: ${extRoute.extensionName}, useDatabase: ${extRoute.useDatabase})`
82
+ );
83
+ }
84
+ console.log('│');
85
+
86
+ handlers[`${IntegrationClass.Definition.name}`] = {
87
+ handler: createAppHandler(
88
+ `HTTP Event: ${IntegrationClass.Definition.name}`,
89
+ router
90
+ ),
91
+ };
92
+
93
+ for (const [bindingName, group] of bindingGroups) {
94
+ // Wire contract: integration-builder.js (devtools) derives the identical
95
+ // function key for the serverless config. Keep both in sync.
96
+ const fnKey = `${IntegrationClass.Definition.name}__${sanitizeBindingKey(
97
+ bindingName
98
+ )}`;
99
+ // Distinct binding keys can sanitize to the same fnKey — fail loud rather than overwrite.
100
+ if (Object.prototype.hasOwnProperty.call(handlers, fnKey)) {
101
+ throw new Error(
102
+ `Integration "${IntegrationClass.Definition.name}" extension handler conflict: ` +
103
+ `binding "${bindingName}" sanitizes to "${fnKey}", which is already taken. ` +
104
+ `Use binding keys that are distinct after stripping non-alphanumeric characters.`
105
+ );
106
+ }
107
+ handlers[fnKey] = {
108
+ handler: createAppHandler(
109
+ `HTTP Event: ${IntegrationClass.Definition.name} extension ${bindingName}`,
110
+ group.router,
111
+ group.useDatabase
112
+ ),
113
+ };
114
+ }
115
+ }
116
+
117
+ module.exports = { handlers };
@@ -0,0 +1,67 @@
1
+ const { createAppHandler } = require('./../app-handler-helpers');
2
+ const { loadAppDefinition } = require('../app-definition-loader');
3
+ const { Router } = require('express');
4
+ const { IntegrationEventDispatcher } = require('../integration-event-dispatcher');
5
+
6
+ const handlers = {};
7
+ const { integrations: integrationClasses } = loadAppDefinition();
8
+
9
+ for (const IntegrationClass of integrationClasses) {
10
+ const webhookConfig = IntegrationClass.Definition.webhooks;
11
+
12
+ // Skip if webhooks not enabled
13
+ if (!webhookConfig || (typeof webhookConfig === 'object' && !webhookConfig.enabled)) {
14
+ continue;
15
+ }
16
+
17
+ const router = Router();
18
+ const basePath = `/api/${IntegrationClass.Definition.name}-integration/webhooks`;
19
+
20
+ console.log(`\n│ Configuring webhook routes for ${IntegrationClass.Definition.name}:`);
21
+
22
+ // General webhook route (no integration ID)
23
+ router.post(basePath, async (req, res, next) => {
24
+ try {
25
+ const integrationInstance = new IntegrationClass();
26
+ const dispatcher = new IntegrationEventDispatcher(integrationInstance);
27
+ await dispatcher.dispatchHttp({
28
+ event: 'WEBHOOK_RECEIVED',
29
+ req,
30
+ res,
31
+ next,
32
+ });
33
+ } catch (error) {
34
+ next(error);
35
+ }
36
+ });
37
+ console.log(`│ POST ${basePath}`);
38
+
39
+ // Integration-specific webhook route (with integration ID)
40
+ router.post(`${basePath}/:integrationId`, async (req, res, next) => {
41
+ try {
42
+ const integrationInstance = new IntegrationClass();
43
+ const dispatcher = new IntegrationEventDispatcher(integrationInstance);
44
+ await dispatcher.dispatchHttp({
45
+ event: 'WEBHOOK_RECEIVED',
46
+ req,
47
+ res,
48
+ next,
49
+ });
50
+ } catch (error) {
51
+ next(error);
52
+ }
53
+ });
54
+ console.log(`│ POST ${basePath}/:integrationId`);
55
+ console.log('│');
56
+
57
+ handlers[`${IntegrationClass.Definition.name}Webhook`] = {
58
+ handler: createAppHandler(
59
+ `HTTP Event: ${IntegrationClass.Definition.name} Webhook`,
60
+ router,
61
+ false // shouldUseDatabase = false
62
+ ),
63
+ };
64
+ }
65
+
66
+ module.exports = { handlers };
67
+